diff --git a/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/IServiceCollectionExtensions.cs b/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/IServiceCollectionExtensions.cs index 6a4ea21..85e4684 100644 --- a/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/IServiceCollectionExtensions.cs +++ b/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/IServiceCollectionExtensions.cs @@ -20,7 +20,7 @@ IConfiguration configuration services.AddOptions() .Bind(configurationSection) - .ValidateDataAnnotations() + .RecursivelyValidateDataAnnotations() ; return configurationSection.Get(); diff --git a/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/ObjectExtensions.cs b/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..d211122 --- /dev/null +++ b/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/ObjectExtensions.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/OptionsBuilderDataAnnotationsExtensions.cs b/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/OptionsBuilderDataAnnotationsExtensions.cs new file mode 100644 index 0000000..c383d24 --- /dev/null +++ b/Example1-ValidateDataAnnotations/src/Example1Api/Extensions/OptionsBuilderDataAnnotationsExtensions.cs @@ -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 RecursivelyValidateDataAnnotations( + this OptionsBuilder optionsBuilder + ) where TOptions : class + { + optionsBuilder.Services.AddSingleton>( + new RecursiveDataAnnotationValidateOptions(optionsBuilder.Name + )); + return optionsBuilder; + } + } +} \ No newline at end of file diff --git a/Example1-ValidateDataAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidateOptions.cs b/Example1-ValidateDataAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidateOptions.cs new file mode 100644 index 0000000..e227053 --- /dev/null +++ b/Example1-ValidateDataAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidateOptions.cs @@ -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 + : IValidateOptions + 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(); + 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; } + } +} \ No newline at end of file diff --git a/Example1-ValidateDataAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidator.cs b/Example1-ValidateDataAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidator.cs new file mode 100644 index 0000000..2c067ae --- /dev/null +++ b/Example1-ValidateDataAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidator.cs @@ -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 validationResults, + IDictionary validationContextItems = null + ) + { + return Validator.TryValidateObject( + obj, + new ValidationContext( + obj, + null, + validationContextItems + ), + validationResults, + true + ); + } + + public static bool TryValidateObjectRecursive( + T obj, + ValidationContext validationContext, + List validationResults + ) where T : class + { + return TryValidateObjectRecursive( + obj, + validationResults, + validationContext.Items + ); + } + + private static bool TryValidateObjectRecursive( + T obj, + List validationResults, + IDictionary validationContextItems = null + ) + { + return TryValidateObjectRecursive( + obj, + validationResults, + new HashSet(), + validationContextItems + ); + } + + private static bool TryValidateObjectRecursive( + T obj, + ICollection validationResults, + ISet validatedObjects, + IDictionary 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 nestedResults; + switch (value) + { + case null: + continue; + case IEnumerable asEnumerable: + foreach (var enumObj in asEnumerable) + { + if (enumObj == null) continue; + nestedResults = new List(); + 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(); + 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; + } + } +} \ No newline at end of file