Hoe kan ik de validator van gegevensannotaties vertellen om ook complexe onderliggende eigenschappen te valideren?

Kan ik complexe onderliggende objecten automatisch valideren bij het valideren van een bovenliggend object en de resultaten opnemen in de ingevulde ICollection<ValidationResult>?

Als ik de volgende code uitvoer:

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);
        }
    }
}

Ik krijg de volgende uitvoer:

False
The Name field is required.

Maar ik verwachtte iets soortgelijks als:

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


Ik bood een premie aan voor een betere oplossing voor het valideren van onderliggende objecten, maar kreeg idealiter geen afnemers

  • onderliggende objecten valideren tot een willekeurige diepte
  • meerdere fouten per object afhandelen
  • het correct identificeren van de validatiefouten op de velden van het onderliggende object.

Het verbaast me nog steeds dat het framework dit niet ondersteunt.


Antwoord 1, autoriteit 100%

Probleem – Bestelling modelmap

Dit is helaas het standaardgedrag van Validator.TryValidateObjectwelke

valideert de eigenschapswaarden van het object niet recursief

Zoals vermeld in het artikel van Jeff Handley over Object en eigenschappen valideren met de validator, standaard valideert de validator in de volgende volgorde:

  1. Kenmerken op eigendomsniveau
  2. Kenmerken op objectniveau
  3. Implementatie op modelniveau IValidatableObject

Het probleem is dat bij elke stap…

Als validators ongeldig zijn, zal Validator.ValidateObjectde validatie afbreken en de fout(en) retourneren

Probleem – Modelbindervelden

Een ander mogelijk probleem is dat de modelbinder alleen validatie uitvoert op objecten die besloten hebben te binden. Als u bijvoorbeeld geen invoer opgeeft voor velden binnen complexe typen op uw model, hoeft de modelbinder die eigenschappen helemaal niet te controleren omdat hij de constructor voor die objecten niet heeft aangeroepen. Volgens het geweldige artikel van Brad Wilson op Invoervalidatie versus modelvalidatie in ASP.NET MVC:

De reden dat we niet recursief in het Address-object “duiken”, is dat er niets in de vorm was dat enige waarde binnen Address begrensde.

Oplossing – Valideer object tegelijk met eigenschappen

Een manier om dit probleem op te lossen is om validaties op objectniveau om te zetten in validatie op eigenschapsniveau door een aangepast validatiekenmerk toe te voegen aan de eigenschap die zal terugkeren met het validatieresultaat van het object zelf.

Artikel van Josh Carroll over Recursieve validatie met behulp van DataAnnotationsbiedt een implementatie van een dergelijke strategie (oorspronkelijk in deze SO-vraag). Als we een complex type (zoals Address) willen valideren, kunnen we een aangepast ValidateObject-attribuut aan de eigenschap toevoegen, zodat het bij de eerste stap wordt geëvalueerd

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

U moet de volgende ValidateObjectAttribute-implementatie toevoegen:

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);
   }
}

Oplossing – Model tegelijkertijd valideren als eigenschappen

Voor objecten die IValidatableObjectimplementeren, kunnen we, wanneer we de ModelState controleren, ook controleren of het model zelf geldig is voordat we de lijst met fouten retourneren. We kunnen alle gewenste fouten toevoegen door ModelState.AddModelError(field, error)aan te roepen. Zoals gespecificeerd in Hoe MVC te dwingen om IValidatableObject te valideren, kunnen we het als volgt doen:

[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);
    }
}

Ook, als u een elegantere oplossing wilt, kunt u de code één keer schrijven door uw eigen aangepaste modelbinderimplementatie op te geven in Application_Start() met ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());. Er zijn hieren hier


Antwoord 2, autoriteit 27%

Ik kwam dit ook tegen en vond dit draadje. Hier is een eerste pas:

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;
        }
    }
}

Antwoord 3, autoriteit 18%

U moet uw eigen validatorkenmerk maken (bijv. [CompositeField]) dat de onderliggende eigenschappen valideert.

Other episodes