How can I tell the Data Annotations validator to also validate complex child properties?

GWB picture GWB · Mar 22, 2010 · Viewed 28.6k times · Source

Can I automatically validate complex child objects when validating a parent object and include the results in the populated ICollection<ValidationResult>?

If I run the following code:

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

I get the following output:

False
The Name field is required.

But I was expecting something similar to:

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


I offered a bounty for a better child object validation solution but didn't get any takers, ideally

  • validating child objects to an arbitrary depth
  • handling multiple errors per object
  • correctly identifying the validation errors on the child object fields.

I'm still surprised the framework doesn't support this.

Answer

KyleMit picture KyleMit · Feb 10, 2015

Issue - Model Binder Order

This is, unfortunately, the standard behavior of Validator.TryValidateObject which

does not recursively validate the property values of the object

As pointed out in Jeff Handley's article on Validating Object and Properties with the Validator, by default, the validator will validate in order:

  1. Property-Level Attributes
  2. Object-Level Attributes
  3. Model-Level implementation IValidatableObject

The problem is, at each step of the way...

If any validators are invalid, Validator.ValidateObject will abort validation and return the failure(s)

Issue - Model Binder Fields

Another possible issue is that the model binder will only run validation on objects that it has decided to bind. For example, if you don't provide inputs for fields within complex types on your model, the model binder won't need to check those properties at all because it hasn't called the constructor on those objects. According to Brad Wilson's great article on Input Validation vs. Model Validation in ASP.NET MVC:

The reason we don't "dive" into the Address object recursively is that there was nothing in the form that bound any values inside of Address.

Solution - Validate Object at the same time as Properties

One way to solve this problem is to convert object-level validations to property level validation by adding a custom validation attribute to the property that will return with the validation result of the object itself.

Josh Carroll's article on Recursive Validation Using DataAnnotations provides an implementation of one such strategy (originally in this SO question). If we want to validate a complex type (like Address), we can add a custom ValidateObject attribute to the property, so it is evaluated on the first step

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

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

You'll need to add the following ValidateObjectAttribute implementation:

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

Solution - Validate Model at the Same time as Properties

For objects that implement IValidatableObject, when we check the ModelState, we can also check to see if the model itself is valid before returning the list of errors. We can add any errors we want by calling ModelState.AddModelError(field, error). As specified in How to force MVC to Validate IValidatableObject, we can do it like this:

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

Also, if you want a more elegant solution, you can write the code once by providing your own custom model binder implementation in Application_Start() with ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());. There are good implementations here and here