web-dev-qa-db-ja.com

複雑な子プロパティも検証するようにデータアノテーションバリデーターに指示するにはどうすればよいですか?

親オブジェクトを検証するときに複雑な子オブジェクトを自動的に検証して、その結果を入力済みの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.


私はより良い子オブジェクト検証ソリューションの賞金を提供しましたが、理想的には何も受け取れませんでした

  • 子オブジェクトを任意の深さに検証する
  • オブジェクトごとに複数のエラーを処理する
  • 子オブジェクトフィールドの検証エラーを正しく識別します。

フレームワークがこれをサポートしていないことにまだ驚いています。

40
GWB

独自のバリデーター属性を作成する必要があります(例:[CompositeField])は子プロパティを検証します。

6
SLaks

問題-モデルバインダーの注文

残念ながら、これは _Validator.TryValidateObject_ の標準的な動作です。

オブジェクトのプロパティ値を再帰的に検証しません

Validating Object and Properties with the Validator に関するJeff Handleyの記事で指摘されているように、デフォルトでは、バリデーターは次の順序で検証されます。

  1. プロパティレベルの属性
  2. オブジェクトレベルの属性
  3. モデルレベルの実装IValidatableObject

問題は、方法の各ステップで...

バリデーターが無効な場合、_Validator.ValidateObject_は検証を中止し、失敗を返します

問題-モデルバインダーフィールド

別の考えられる問題は、モデルバインダーが、バインドすることを決定したオブジェクトに対してのみ検証を実行することです。たとえば、モデルの複合型内のフィールドに入力を提供しない場合、モデルバインダーはそれらのオブジェクトのコンストラクターを呼び出していないため、これらのプロパティをチェックする必要はまったくありません。 ASP.NET MVC での入力の検証とモデルの検証に関するBrad Wilsonの素晴らしい記事によると:

Addressオブジェクトを再帰的に調べない理由は、Address内の値をバインドするフォームには何もなかったためです。

ソリューション-プロパティと同時にオブジェクトを検証する

この問題を解決する1つの方法は、オブジェクト自体の検証結果とともに返されるプロパティにカスタム検証属性を追加して、オブジェクトレベルの検証をプロパティレベルの検証に変換することです。

Josh Carrollの DataAnnotations を使用した再帰的検証に関する記事では、このような戦略の1つを実装しています(最初は 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(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);
    }
}
_

また、よりエレガントなソリューションが必要な場合は、ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());。良い実装があります ここここ

30
KyleMit

私もこれに遭遇し、このスレッドを見つけました。これが最初のパスです:

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;
        }
    }
}
10
Aaron Daniels