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 standard IValidatorFactory only returns a single validator.