Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DataAnnotations: Recursively validating an entire object graph

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?

like image 347
Neil Barnwell Avatar asked Oct 05 '11 15:10

Neil Barnwell


3 Answers

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.

like image 131
reustmd Avatar answered Oct 21 '22 09:10

reustmd


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);
like image 5
Simone Devoti Avatar answered Oct 21 '22 10:10

Simone Devoti


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://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-3.1#blazor-data-annotations-validation-package

like image 1
Nijenhof Avatar answered Oct 21 '22 09:10

Nijenhof