Как я могу сказать валидатору аннотаций данных также проверять сложные дочерние свойства? - PullRequest
36 голосов
/ 22 марта 2010

Можно ли автоматически проверять сложные дочерние объекты при проверке родительского объекта и включать результаты в заполненный 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.


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

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

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

Ответы [ 3 ]

26 голосов
/ 10 февраля 2015

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

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

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

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

  1. Атрибуты уровня свойства
  2. Атрибуты уровня объекта
  3. Реализация уровня модели IValidatableObject

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

Если какие-либо валидаторы недействительны, Validator.ValidateObject прервет валидацию и выдаст ошибку (и)

Проблема - поля связующего элемента модели

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

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

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

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

Статья Джоша Кэрролла о Рекурсивная проверка с использованием DataAnnotations обеспечивает реализациюодной такой стратегии (первоначально в этот вопрос ).Если мы хотим проверить сложный тип (например, Address), мы можем добавить в свойство пользовательский атрибут 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(<em>field</em>, <em>error</em>).Как указано в Как заставить 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());.Есть хорошие реализации здесь и здесь

9 голосов
/ 25 февраля 2012

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

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

    /// <summary>
    /// Attribute class used to validate child properties.
    /// </summary>
    /// <remarks>
    /// See: /2251595/kak-ya-mogu-skazat-validatoru-annotatsii-dannyh-takzhe-proveryat-slozhnye-dochernie-svoistva
    /// 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;
        }
    }
}
5 голосов
/ 22 марта 2010

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

...