Reputation: 794
Reading the documentation it appears that in .NET Core you can do implicit child properties validation with:
services.AddMvc().AddFluentValidation(fv => {
fv.ImplicitlyValidateChildProperties = true;
});
Unfortunately I'm not working with MVC directly so I have to call Validate
myself (IValidator<T>
instances are registered by myself with Scrutor). Is there a similar setting for manually validating nested fields as well as top-level fields (using .NET Core DI)?
Upvotes: 3
Views: 1528
Reputation: 4677
Another approach would be to define a base class for your validators, which itself handles the recursive validation.
I needed this specifically so that the validators run in a nested way, and the errors follow the structure of the object (e.g. request.account.name: 'Name' must not be empty
).
This is the implementation:
using System.Linq.Expressions;
using System.Reflection;
using FluentValidation;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Example.Validation;
public class RecursiveValidator<T> : AbstractValidator<T>
{
private readonly IServiceProvider serviceProvider;
public RecursiveValidator(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public void ValidateMembers(Type allowedType)
{
foreach (var propertyInfo in typeof(T).GetProperties()) {
if (allowedType.IsAssignableFrom(propertyInfo.PropertyType)) {
ValidateMember(propertyInfo.PropertyType, propertyInfo.Name, allowedType);
}
}
}
private void ValidateMember(Type fieldType, string propertyName, Type allowedType)
{
var lambdaParameter = Expression.Parameter(typeof(T), "request");
var lambdaBody = Expression.Property(lambdaParameter, propertyName);
var propertyExpression = Expression.Lambda(lambdaBody, lambdaParameter);
typeof(RecursionHelper)
.GetMethod(nameof(RecursionHelper.ValidateMember), BindingFlags.Public | BindingFlags.Static)!
.MakeGenericMethod(typeof(T), fieldType)
.Invoke(obj: null, new object[] { this, serviceProvider, propertyExpression, allowedType });
}
private static class RecursionHelper
{
[UsedImplicitly]
public static void ValidateMember<TMessage, TProperty>(
AbstractValidator<TMessage> validator,
IServiceProvider serviceProvider,
Expression<Func<TMessage, TProperty>> expression,
Type allowedType
)
{
// Discovers and adds registered validators for the given type
var fieldValidators = serviceProvider.GetServices<IValidator<TProperty>>().ToList();
var rule = validator.RuleFor(expression);
foreach (var fieldValidator in fieldValidators) {
rule.SetValidator(fieldValidator);
}
// If no validators are found, create one and let it recurse
if (fieldValidators.Count == 0) {
var recursiveValidator = new RecursiveValidator<TProperty>(serviceProvider);
recursiveValidator.ValidateMembers(allowedType);
rule.SetValidator(recursiveValidator);
}
}
}
}
To use it, inherit from RecursiveValidator<T>
instead of ValidateMembers<T>
, and .
Then call ValidateMembers(Type allowedType)
when you want it to validate all members.
class UserValidator : RecursiveValidator<User> {
public UserValidator(IServiceProvider services) : base(services) {
// Recurse on specific types
ValidateMembers(typeof(Address));
ValidateMembers(typeof(PhoneNumber));
// Recurse on all members of a baseclass, e.g. IMessage
ValidateMembers(typeof(IMessage));
RuleFor(user => user.Name).NotEmpty();
}
}
Note, recursion will go only 1 level. All other validators should call
ValidateMembers(typeof(SomeType))
manually.
If you want all validators to recurse automatically, then create another base class that calls all required ValidateMembers
in the constructor.
public class DefaultValidator<T>: RecursiveValidator<T> {
public DefaultValidator<T>(IServiceProvider services): base(services) {
ValidateMembers(typeof(User));
ValidateMembers(typeof(Address));
ValidateMembers(typeof(Project));
ValidateMembers(typeof(PhoneNumber));
// Or just a single base class
ValidateMembers(typeof(BaseEntity));
}
}
Note: This implementation assumes you may have multiple validators for each type, and you want to run all of them. This is why the
IServiceProvider
is required. The standardIValidatorFactory
only returns a single validator.
Upvotes: 0
Reputation: 731
If you check the FluentValidation source code you will notice that the ImplicitlyValidateChildProperties
option uses the Asp.Net mechanism of validating nested properties so it won't work for manual validation.
You can write a helper or extension method that uses reflection and the IValidatorFactory
service to do nested validation:
public static async Task<ValidationResult> ValidateImplicitAsync<TReq>(this TReq request, IValidatorFactory factory)
where TReq: class
{
var result = new ValidationResult();
await ValidateRecursively(request, factory, result);
return result;
}
static async Task ValidateRecursively(
object obj,
IValidatorFactory factory,
ValidationResult result)
{
if(obj == null) { return; }
Type t = obj.GetType();
IValidator validator = factory.GetValidator(t);
if(validator == null) { return; }
ValidationResult r = await validator.ValidateAsync(new ValidationContext<object>(obj));
foreach(var error in r.Errors)
{
result.Errors.Add(error);
}
foreach(var prop in t.GetProperties())
{
object childValue = prop.GetValue(obj, null);
await ValidateRecursively(childValue, factory, result);
}
}
Then you could call it like this:
public class MyService {
readonly IValidatorFactory factory;
public MyService(IValidatorFactory factory){
this.factory = factory;
}
public async Task Handle(MyModel model){
ValidationResult validationResult = await model.ValidateImplicitAsync(factory);
// ... etc...
}
}
Upvotes: 2