DataAnnotations: Recursively validating an entire object graph
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.
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
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;
}
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);