DataAnnotations: Recursively validating an entire object graph

31,010

Solution 1

Here's an alternative to the opt-in attribute approach. I believe this will traverse the object-graph properly and validate everything.

public bool TryValidateObjectRecursive<T>(T obj, List<ValidationResult> results) {

bool result = TryValidateObject(obj, results);

var properties = obj.GetType().GetProperties().Where(prop => prop.CanRead 
    && !prop.GetCustomAttributes(typeof(SkipRecursiveValidation), false).Any() 
    && prop.GetIndexParameters().Length == 0).ToList();

foreach (var property in properties)
{
    if (property.PropertyType == typeof(string) || property.PropertyType.IsValueType) continue;

    var value = obj.GetPropertyValue(property.Name);

    if (value == null) continue;

    var asEnumerable = value as IEnumerable;
    if (asEnumerable != null)
    {
        foreach (var enumObj in asEnumerable)
        {
            var nestedResults = new List<ValidationResult>();
            if (!TryValidateObjectRecursive(enumObj, nestedResults))
            {
                result = false;
                foreach (var validationResult in nestedResults)
                {
                    PropertyInfo property1 = property;
                    results.Add(new ValidationResult(validationResult.ErrorMessage, validationResult.MemberNames.Select(x => property1.Name + '.' + x)));
                }
            };
        }
    }
    else
    {
        var nestedResults = new List<ValidationResult>();
        if (!TryValidateObjectRecursive(value, nestedResults))
        {
            result = false;
            foreach (var validationResult in nestedResults)
            {
                PropertyInfo property1 = property;
                results.Add(new ValidationResult(validationResult.ErrorMessage, validationResult.MemberNames.Select(x => property1.Name + '.' + x)));
            }
        }
    }
}

return result;
}

Most up-to-date code: https://github.com/reustmd/DataAnnotationsValidatorRecursive

Package: https://www.nuget.org/packages/DataAnnotationsValidator/

Also, I have updated this solution to handle cyclical object graphs. Thanks for the feedback.

Solution 2

You can extend the default validation behavior, making the class you want to validate implement the IValidatableObject interface

public class Employee : IValidatableObject
{
    [Required]
    public string Name { get; set; }

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

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();

        Validator.TryValidateObject(Address, new ValidationContext(Address), results, validateAllProperties: true);

        return results;
    }
}

public class Address
{
    [Required]
    public string Line1 { get; set; }

    public string Line2 { get; set; }

    [Required]
    public string Town { get; set; }

    [Required]
    public string PostalCode { get; set; }
}

And validate it using the Validator class in one of these ways

Validator.ValidateObject(employee, new ValidationContext(employee), validateAllProperties: true);

or

var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(employee, new ValidationContext(employee), validationResults, validateAllProperties: true);

Solution 3

I found this issue while searching for a similar problem I had with Blazor. Seeing as Blazor is becoming increasingly more popular I figured this would be a good place to mention how I solved this problem.

Firstly, install the following package using your package manager console: Install-Package Microsoft.AspNetCore.Components.DataAnnotations.Validation -Version 3.2.0-rc1.20223.4

Alternatively you can also add it manually in your .csproj file:

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" Version="3.2.0-rc1.20223.4" />
</ItemGroup>

Having added and installed this package one can simply add the following data annotation to any object to indicate that it is a complex type. Using the example OP provided:

public class Employee
{
    [Required]
    public string Name { get; set; }

    [ValidateComplexType]
    public Address Address { get; set; }
}

public class Address
{
    [Required]
    public string Line1 { get; set; }

    public string Line2 { get; set; }

    [Required]
    public string Town { get; set; }

    [Required]
    public string PostalCode { get; set; }
}

Take note of the [ValidateComplexType] annotation above the Address reference.

For the ones that also found this post when using Blazor: make sure your EditForm uses this AnnotationValidator instead of the normal one:

<ObjectGraphDataAnnotationsValidator />

Source: https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-3.1#blazor-data-annotations-validation-package

Share:
31,010
Neil Barnwell
Author by

Neil Barnwell

I'm an Application Developer and Software Architect for a wide variety of software solutions including websites and desktop applications. Most recently focussed on a bespoke warehouse management system using C# 3.5 and SQL Server 2008. Since my move to .NET I've become active in the community, attending monthly usergroup meetings and various conferences. I've even made a foray into speaking at usergroups about topics I am passionate about. While I love experimenting with new tech and have a hobby project hosted on CodePlex, my current focus is less on specific new technologies and more on good principles and techniques. When not at work I'm a family man, biker, amateur photographer, guitarist and of course, software developer.

Updated on July 05, 2022

Comments

  • Neil Barnwell
    Neil Barnwell almost 2 years

    I have an object graph sprinkled with DataAnnotation attributes, where some properties of objects are classes which themselves have validation attributes, and so on.

    In the following scenario:

    public class Employee
    {
        [Required]
        public string Name { get; set; }
    
        [Required]
        public Address Address { get; set; }
    }
    
    public class Address
    {
        [Required]
        public string Line1 { get; set; }
    
        public string Line2 { get; set; }
    
        [Required]
        public string Town { get; set; }
    
        [Required]
        public string PostalCode { get; set; }
    }
    

    If I try to validate an Employee's Address with no value for PostalCode, then I would like (and expect) an exception, but I get none. Here's how I'm doing it:

    var employee = new Employee
    {
        Name = "Neil Barnwell",
        Address = new Address
        {
            Line1 = "My Road",
            Town = "My Town",
            PostalCode = "" // <- INVALID!
        }
    };
    
    Validator.ValidateObject(employee, new ValidationContext(employee, null, null));
    

    What other options do I have with Validator that would ensure all properties are validated recursively?

  • Jorrit Schippers
    Jorrit Schippers about 12 years
    I like this solution, but be beware of infinite loops when the object graph contains cycles.
  • rogersillito
    rogersillito almost 8 years
    The code sample above has a few issues compared to the git version - so definitely follow the link if you're looking to implement this (or Install-Package dataannotationsvalidator via nuget!)
  • reustmd
    reustmd almost 8 years
    @rogersillito Updated the code and added links to the answer. Thanks!
  • Hans Vonn
    Hans Vonn almost 7 years
    I agree with @JorritSchippers about the infinite loops. This tool would be a preferable solution if this was addressed.
  • reustmd
    reustmd almost 7 years
    @HansVonn I have update the code to handle cyclical object graphs. I'm working on getting it published in nuget, but there's a separate owner so it may take a bit.
  • reustmd
    reustmd almost 7 years
    @JorritSchippers I have update the code to handle cyclical object graphs. I'm working on getting it published in nuget, but there's a separate owner so it may take a bit.
  • Rick M.
    Rick M. over 5 years
    Welcome to SO! Although this might answer the question, adding explanation at necessary steps and supporting your claims with suitable links will add weight to the answer and be more helpful!
  • Shimmy Weitzhandler
    Shimmy Weitzhandler over 4 years
    Can you reflect the validation in the UI in this manner (plain DA attributes)?
  • mrdnk
    mrdnk about 4 years
    This is an answer to a very old post. In my opinion, this is the wrong way of approaching a fairly simple problem, that actually needs to be very open and explicit. You essentially want to approach this as domain or request validation question. I'd personally implement a customer Validator class.