-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement a recursive DataAnnotations validator
- Loading branch information
Showing
5 changed files
with
204 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
Example1-ValidateDataAnnotations/src/Example1Api/Extensions/ObjectExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
...dateDataAnnotations/src/Example1Api/Extensions/OptionsBuilderDataAnnotationsExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
...taAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidateOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
...dateDataAnnotations/src/Example1Api/OptionsValidators/RecursiveDataAnnotationValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |