Skip to content

Commit

Permalink
Implement a recursive DataAnnotations validator
Browse files Browse the repository at this point in the history
  • Loading branch information
tgharold committed Mar 5, 2020
1 parent a189145 commit b1dac68
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ IConfiguration configuration

services.AddOptions<T>()
.Bind(configurationSection)
.ValidateDataAnnotations()
.RecursivelyValidateDataAnnotations()
;

return configurationSection.Get<T>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Example1Api.Extensions
{
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Example1Api.OptionsValidators;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Example1Api.Extensions
{
public static class OptionsBuilderDataAnnotationsExtensions
{
public static OptionsBuilder<TOptions> RecursivelyValidateDataAnnotations<TOptions>(
this OptionsBuilder<TOptions> optionsBuilder
) where TOptions : class
{
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
new RecursiveDataAnnotationValidateOptions<TOptions>(optionsBuilder.Name
));
return optionsBuilder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Extensions.Options;

namespace Example1Api.OptionsValidators
{
public class RecursiveDataAnnotationValidateOptions<TOptions>
: IValidateOptions<TOptions>
where TOptions : class
{
public RecursiveDataAnnotationValidateOptions(string optionsBuilderName)
{
Name = optionsBuilderName;
}

public ValidateOptionsResult Validate(string name, TOptions options)
{
if (Name != null && name != Name) return ValidateOptionsResult.Skip;

var validationResults = new List<ValidationResult>();
if (RecursiveDataAnnotationValidator.TryValidateObjectRecursive(
options,
new ValidationContext(options, serviceProvider: null, items: null),
validationResults
))
{
return ValidateOptionsResult.Success;
}

var errors = validationResults
.Select(r =>
$"Validation failed for members: '{string.Join(",", r.MemberNames)}' with the error: '{r.ErrorMessage}'."
)
.ToList();
return ValidateOptionsResult.Fail(errors);
}

public string Name { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Collections;
using Example1Api.Extensions;

namespace Example1Api.OptionsValidators
{
/* References:
* - https://github.com/ovation22/DataAnnotationsValidatorRecursive/blob/master/DataAnnotationsValidator/DataAnnotationsValidator/DataAnnotationsValidator.cs
* - https://www.nuget.org/packages/DataAnnotationsValidator/
*/

public static class RecursiveDataAnnotationValidator
{
private static bool TryValidateObject(
object obj,
ICollection<ValidationResult> validationResults,
IDictionary<object, object> validationContextItems = null
)
{
return Validator.TryValidateObject(
obj,
new ValidationContext(
obj,
null,
validationContextItems
),
validationResults,
true
);
}

public static bool TryValidateObjectRecursive<T>(
T obj,
ValidationContext validationContext,
List<ValidationResult> validationResults
) where T : class
{
return TryValidateObjectRecursive(
obj,
validationResults,
validationContext.Items
);
}

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

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

validatedObjects.Add(obj);
var result = TryValidateObject(obj, validationResults, validationContextItems);

var properties = obj.GetType().GetProperties().Where(prop => prop.CanRead
//TODO: && !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);

List<ValidationResult> nestedResults;
switch (value)
{
case null:
continue;
case IEnumerable asEnumerable:
foreach (var enumObj in asEnumerable)
{
if (enumObj == null) continue;
nestedResults = new List<ValidationResult>();
if (!TryValidateObjectRecursive(enumObj, nestedResults, validatedObjects, validationContextItems))
{
result = false;
foreach (var validationResult in nestedResults)
{
var property1 = property;
validationResults.Add(new ValidationResult(validationResult.ErrorMessage, validationResult.MemberNames.Select(x => property1.Name + '.' + x)));
}
}
}

break;
default:
nestedResults = new List<ValidationResult>();
if (!TryValidateObjectRecursive(value, nestedResults, validatedObjects, validationContextItems))
{
result = false;
foreach (var validationResult in nestedResults)
{
var property1 = property;
validationResults.Add(new ValidationResult(validationResult.ErrorMessage, validationResult.MemberNames.Select(x => property1.Name + '.' + x)));
}
}
break;
}
}

return result;
}
}
}

0 comments on commit b1dac68

Please sign in to comment.