【问题标题】:How can I tell the Data Annotations validator to also validate complex child properties?如何告诉数据注释验证器也验证复杂的子属性?
【发布时间】:2011-01-30 10:19:48
【问题描述】:

我可以在验证父对象时自动验证复杂的子对象并将结果包含在填充的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.


我为更好的子对象验证解决方案提供了赏金,但理想情况下没有得到任何接受者

  • 验证子对象到任意深度
  • 处理每个对象的多个错误
  • 正确识别子对象字段上的验证错误。

我仍然对框架不支持这一点感到惊讶。

【问题讨论】:

标签: c# validation data-annotations


【解决方案1】:

问题 - 模型绑定顺序

不幸的是,这是Validator.TryValidateObject 的标准行为

不递归验证对象的属性值

正如 Jeff Handley 在Validating Object and Properties with the Validator 上的文章中所指出的,默认情况下,验证器会按顺序进行验证:

  1. 属性级属性
  2. 对象级属性
  3. 模型级实现IValidatableObject

问题是,在每一步...

如果任何验证器无效,Validator.ValidateObject 将中止验证并返回失败

问题 - 模型绑定器字段

另一个可能的问题是模型绑定器只会对它决定绑定的对象运行验证。例如,如果您不为模型的复杂类型中的字段提供输入,则模型绑定器根本不需要检查这些属性,因为它没有调用这些对象的构造函数。根据 Brad Wilson 在Input Validation vs. Model Validation in ASP.NET MVC 上的精彩文章:

我们不递归地“深入”到 Address 对象的原因是表单中没有任何东西可以绑定 Address 内的任何值。

解决方案 - 与属性同时验证对象

解决此问题的一种方法是将对象级验证转换为属性级验证,方法是向属性添加自定义验证属性,该属性将返回对象本身的验证结果。

Josh Carroll 在Recursive Validation Using DataAnnotations 上的文章提供了一种此类策略的实现(最初在this SO question 中)。如果我们想验证一个复杂的类型(比如 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>) 添加我们想要的任何错误。正如How to force MVC to Validate 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()); 提供您自己的自定义模型绑定器实现来编写一次代码。有很好的实现herehere

【讨论】:

  • 很好的答案。尽管如此,它真的很糟糕,它不能像设置一个选项那样开箱即用。
  • 能否修改此解决方案以使用不显眼的 javascript?
  • @JasonlPrice,学究式地,我确定它可能是,但我不确定您遇到了哪些极端情况。您可以在新问题中发布测试用例和MCVE 吗?
  • @KyleMit 我在以下链接stackoverflow.com/questions/41064842/… 创建了一个问题
【解决方案2】:

我也遇到了这个,找到了这个帖子。这是第一关:

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

    /// <summary>
    /// Attribute class used to validate child properties.
    /// </summary>
    /// <remarks>
    /// See: http://stackoverflow.com/questions/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;
        }
    }
}

【讨论】:

    【解决方案3】:

    您需要创建自己的验证器属性(例如,[CompositeField])来验证子属性。

    【讨论】:

    • 这是我正在尝试的方法并且它有效,但我对属性只能返回 1 个 ValidationResult 的事实感到困惑。如果孩子有多个验证错误,这是一个问题。我可以将错误消息填充到一个字符串中,但它看起来很混乱和错误。
    猜你喜欢
    • 2011-08-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多