Как я могу проверить валидатор данных аннотации, чтобы также проверить сложные дочерние свойства?

Могу ли я автоматически проверять сложные дочерние объекты при проверке родительского объекта и включать результаты в заполненный ICollection<ValidationResult>?

Если я запустил следующий код:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ConsoleApplication1
{
    public class Person
    {
        [Required]
        public string Name { get; set; }

        public Address Address { get; set; }
    }

    public class Address
    {
        [Required]
        public string Street { get; set; }

        [Required]
        public string City { get; set; }

        [Required]
        public string State { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person
            {
                Name = null,
                Address = new Address
                {
                    Street = "123 Any St",
                    City = "New York",
                    State = null
                }
            };

            var validationContext = new ValidationContext(person, null, null);
            var validationResults = new List<ValidationResult>();

            var isValid = Validator.TryValidateObject(person, validationContext, validationResults);

            Console.WriteLine(isValid);

            validationResults.ForEach(r => Console.WriteLine(r.ErrorMessage));

            Console.ReadKey(true);
        }
    }
}

Я получаю следующий вывод:

False
The Name field is required.

Но я ожидал чего-то подобного:

False
The Name field is required.
The State field is required.


Я предложил щедрость для лучшего решения для проверки дочерних объектов, но не получил ни одного получателя, в идеале

  • проверка дочерних объектов на произвольную глубину
  • обработка нескольких ошибок на объект
  • правильная идентификация ошибок проверки в полях дочерних объектов.

Я все еще удивлен, что фреймворк не поддерживает это.

Ответ 1

Вам нужно будет создать свой собственный атрибут проверки (например, [CompositeField]), который проверяет дочерние свойства.

Ответ 2

Проблема - заказ привязки модели

Это, к сожалению, стандартное поведение Validator.TryValidateObject, которое

не рекурсивно проверяет значения свойств объекта

Как указано в статье Джеффа Хандли о Проверка объекта и свойств с помощью Validator, по умолчанию валидатор будет проверять в порядке:

  • Атрибуты уровня собственности
  • Атрибуты уровня объекта
  • Реализация уровня модели IValidatableObject

Проблема в том, что на каждом шагу пути...

Если какие-либо валидаторы недействительны, Validator.ValidateObject отменяет проверку и возвращает сбой (-ы)

Проблема - поля привязки модели

Другая возможная проблема заключается в том, что привязка модели будет выполнять проверку только на объектах, которые она решила связать. Например, если вы не предоставляете входы для полей в сложных типах вашей модели, связующее устройство модели не обязательно будет проверять эти свойства вообще, потому что оно не вызвало конструктор на этих объектах. Согласно Брэду Уилсону, хорошая статья о Проверка ввода в сравнении с проверкой модели в ASP.NET MVC:

Причина, по которой мы не "погружаемся" в объект Address рекурсивно, состоит в том, что в форме не было ничего, что связывало любые значения внутри.

Решение. Проверяйте объект одновременно с Свойствами

Одним из способов решения этой проблемы является преобразование проверки уровня объекта на проверку уровня свойства путем добавления специального атрибута проверки к свойству, которое вернется с результатом проверки самого объекта.

Статья Джоша Кэрролла о Рекурсивная проверка с использованием DataAnnotations обеспечивает реализацию одной из таких стратегий (первоначально в этот вопрос SO). Если мы хотим проверить сложный тип (например, "Адрес" ), мы можем добавить собственный атрибут ValidateObject к свойству, поэтому он оценивается на первом шаге

public class Person {
  [Required]
  public String Name { get; set; }

  [Required, ValidateObject]
  public Address Address { get; set; }
}

Вам нужно добавить следующую реализацию ValidateObjectAttribute:

public class ValidateObjectAttribute: ValidationAttribute {
   protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
      var results = new List<ValidationResult>();
      var context = new ValidationContext(value, null, null);

      Validator.TryValidateObject(value, context, results, true);

      if (results.Count != 0) {
         var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
         results.ForEach(compositeResults.AddResult);

         return compositeResults;
      }

      return ValidationResult.Success;
   }
}

public class CompositeValidationResult: ValidationResult {
   private readonly List<ValidationResult> _results = new List<ValidationResult>();

   public IEnumerable<ValidationResult> Results {
      get {
         return _results;
      }
   }

   public CompositeValidationResult(string errorMessage) : base(errorMessage) {}
   public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) {}
   protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {}

   public void AddResult(ValidationResult validationResult) {
      _results.Add(validationResult);
   }
}

Решение. Подтвердите модель одновременно. Свойства

Для объектов, которые реализуют IValidatableObject, когда мы проверяем ModelState, мы также можем проверить, действительно ли сама модель верна, прежде чем возвращать список ошибок. Мы можем добавить любые ошибки, которые мы хотим, вызывая ModelState.AddModelError(field, error). Как указано в Как заставить MVC проверять объект IValidatableObject, мы можем сделать это следующим образом:

[HttpPost]
public ActionResult Create(Model model) {
    if (!ModelState.IsValid) {
        var errors = model.Validate(new ValidationContext(model, null, null));
        foreach (var error in errors)                                 
            foreach (var memberName in error.MemberNames)
                ModelState.AddModelError(memberName, error.ErrorMessage);

        return View(post);
    }
}

Также, если вы хотите более элегантное решение, вы можете написать код один раз, предоставив свою собственную реализацию привязки модели в Application_Start() с помощью ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());. Существуют хорошие варианты здесь и здесь

Ответ 3

Я также столкнулся с этим и нашел этот поток. Здесь первый проход:

namespace Foo
{
    using System.ComponentModel.DataAnnotations;
    using System.Linq;

    /// <summary>
    /// Attribute class used to validate child properties.
    /// </summary>
    /// <remarks>
    /// See: http://stackoverflow.com/info/2493800/how-can-i-tell-the-data-annotations-validator-to-also-validate-complex-child-pro
    /// Apparently the Data Annotations validator does not validate complex child properties.
    /// To do so, slap this attribute on a your property (probably a nested view model) 
    /// whose type has validation attributes on its properties.
    /// This will validate until a nested <see cref="System.ComponentModel.DataAnnotations.ValidationAttribute" /> 
    /// fails. The failed validation result will be returned. In other words, it will fail one at a time. 
    /// </remarks>
    public class HasNestedValidationAttribute : ValidationAttribute
    {
        /// <summary>
        /// Validates the specified value with respect to the current validation attribute.
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
        /// </returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var isValid = true;
            var result = ValidationResult.Success;

            var nestedValidationProperties = value.GetType().GetProperties()
                .Where(p => IsDefined(p, typeof(ValidationAttribute)))
                .OrderBy(p => p.Name);//Not the best order, but at least known and repeatable.

            foreach (var property in nestedValidationProperties)
            {
                var validators = GetCustomAttributes(property, typeof(ValidationAttribute)) as ValidationAttribute[];

                if (validators == null || validators.Length == 0) continue;

                foreach (var validator in validators)
                {
                    var propertyValue = property.GetValue(value, null);

                    result = validator.GetValidationResult(propertyValue, new ValidationContext(value, null, null));
                    if (result == ValidationResult.Success) continue;

                    isValid = false;
                    break;
                }

                if (!isValid)
                {
                    break;
                }
            }
            return result;
        }
    }
}