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?

34032 次浏览

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.

Code:

public class DataAnnotationsValidator : IDataAnnotationsValidator
{
public bool TryValidateObject(object obj, ICollection<ValidationResult> results, IDictionary<object, object> validationContextItems = null)
{
return Validator.TryValidateObject(obj, new ValidationContext(obj, null, validationContextItems), results, true);
}


public bool TryValidateObjectRecursive<T>(T obj, List<ValidationResult> results, IDictionary<object, object> validationContextItems = null)
{
return TryValidateObjectRecursive(obj, results, new HashSet<object>(), validationContextItems);
}


private bool TryValidateObjectRecursive<T>(T obj, List<ValidationResult> results, ISet<object> validatedObjects, IDictionary<object, object> validationContextItems = null)
{
//short-circuit to avoid infinite loops on cyclic object graphs
if (validatedObjects.Contains(obj))
{
return true;
}


validatedObjects.Add(obj);
bool result = TryValidateObject(obj, results, validationContextItems);


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)
{
if ( enumObj != null) {
var nestedResults = new List<ValidationResult>();
if (!TryValidateObjectRecursive(enumObj, nestedResults, validatedObjects, validationContextItems))
{
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, validatedObjects, validationContextItems))
{
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;
}
}






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 ValidateCollectionAttribute : ValidationAttribute
{
public Type ValidationType { get; set; }


protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var collectionResults = new CompositeValidationResult(String.Format("Validation for {0} failed!",
validationContext.DisplayName));
var enumerable = value as IEnumerable;


var validators = GetValidators().ToList();


if (enumerable != null)
{
var index = 0;


foreach (var val in enumerable)
{
var results = new List<ValidationResult>();
var context = new ValidationContext(val, validationContext.ServiceContainer, null);


if (ValidationType != null)
{
Validator.TryValidateValue(val, context, results, validators);
}
else
{
Validator.TryValidateObject(val, context, results, true);
}


if (results.Count != 0)
{
var compositeResults =
new CompositeValidationResult(String.Format("Validation for {0}[{1}] failed!",
validationContext.DisplayName, index));


results.ForEach(compositeResults.AddResult);


collectionResults.AddResult(compositeResults);
}


index++;
}
}


if (collectionResults.Results.Any())
{
return collectionResults;
}


return ValidationResult.Success;
}


private IEnumerable<ValidationAttribute> GetValidators()
{
if (ValidationType == null) yield break;


yield return (ValidationAttribute)Activator.CreateInstance(ValidationType);
}
}


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




public interface IDataAnnotationsValidator
{
bool TryValidateObject(object obj, ICollection<ValidationResult> results, IDictionary<object, object> validationContextItems = null);
bool TryValidateObjectRecursive<T>(T obj, List<ValidationResult> results, IDictionary<object, object> validationContextItems = null);
}


public static class ObjectExtensions
{
public static object GetPropertyValue(this object o, string propertyName)
{
object objValue = string.Empty;


var propertyInfo = o.GetType().GetProperty(propertyName);
if (propertyInfo != null)
objValue = propertyInfo.GetValue(o, null);


return objValue;
}
}


public class SkipRecursiveValidation : Attribute
{
}


public class SaveValidationContextAttribute : ValidationAttribute
{
public static IList<ValidationContext> SavedContexts = new List<ValidationContext>();


protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
SavedContexts.Add(validationContext);
return ValidationResult.Success;
}
}

I cleaned the code from j_freyre a little. The "this.serviceProvider" can be replaced with "null" if you dont have one.

    /// <summary>
/// Validates given <paramref name="obj"/>
/// </summary>
/// <param name="obj"></param>
/// <param name="items">optional items</param>
/// <param name="validationResults">optional list of <see cref="ValidationResult"/></param>
public bool TryValidateObject(object obj, Dictionary<object, object> items, List<ValidationResult> validationResults)
{
// create validation context
ValidationContext validationContext = new ValidationContext(obj, this.serviceProvider, items);


// do validation
if (validationResults == null)
validationResults = new List<ValidationResult>();
bool result = true;


if (!Validator.TryValidateObject(obj, validationContext, validationResults, true))
result = false;


// do validation of nested objects
if (obj == null)
return result;




// get properties that can be validated
List<PropertyInfo> properties = obj.GetType()
.GetProperties()
.Where(prop => prop.CanRead && prop.GetIndexParameters().Length == 0)
.Where(prop => CanTypeBeValidated(prop.PropertyType))
.ToList();


// loop over each property
foreach (PropertyInfo property in properties)
{
// get and check value
var value = property.GetValue(obj);
if (value == null)
continue;


// check whether its an enumerable - if not, put the value in a new enumerable
IEnumerable<object> valueEnumerable = value as IEnumerable<object>;
if (valueEnumerable == null)
{
valueEnumerable = new object[] { value };
}


// validate values in enumerable
foreach (var valueToValidate in valueEnumerable)
{
List<ValidationResult> nestedValidationResults = new List<ValidationResult>();
if (!TryValidateObject(valueToValidate, items, nestedValidationResults))
{
result = false;


// add nested results to this results (so the member names are correct)
foreach (var validationResult in nestedValidationResults)
{
validationResults.Add(new ValidationResult(validationResult.ErrorMessage, validationResult.MemberNames.Select(x => property.Name + '.' + x)));
}
}
}
}




return result;
}


/// <summary>
/// Returns whether the given <paramref name="type"/> can be validated
/// </summary>
private bool CanTypeBeValidated(Type type)
{
if (type == null)
return false;
if (type == typeof(string))
return false;
if (type.IsValueType)
return false;


if (type.IsArray && type.HasElementType)
{
Type elementType = type.GetElementType();
return CanTypeBeValidated(elementType);
}


return true;
}

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

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

Fluent Validation library provides excellent support for validating complex nested objects. So, say in your application you have Customer's, and Customer's can have collections of Orders, like this:

public class Customer
{
public List<Order> Orders { get; set; } = new List<Order>();
}


public class Order
{
public double Total { get; set; }
}

Now, to validate Customer's, all you need is to set orders validator for each Order:

public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(x => x.Total).GreaterThan(0);
}
}


public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleForEach(x => x.Orders).SetValidator(new OrderValidator());
}
}