FluentValidation · Additional notes
2 min readRapid overview
Additional notes
Integration with ASP.NET Core
- Register validators in DI and enable automatic model validation.
services.AddControllers()
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<CreateOrderDtoValidator>());
- You can also register
IValidator<T>explicitly:
services.AddTransient<IValidator<CreateOrderDto>, CreateOrderDtoValidator>();
- By default FluentValidation integrates with ASP.NET Core's model validation pipeline. Customize behavior with
FluentValidationModelValidatorProvider.Configure(...)or by disabling automatic validation and invoking validators manually.
Senior-level best practices
- Keep validators thin: validation expresses rules, not business processes. Avoid embedding heavy domain logic or side effects in validators.
- Prefer composition: extract reusable rule sets and nested validators via
SetValidatoror separateAbstractValidator<T>types. - Use
CascadeMode.Stopwhen you want to short-circuit rules to reduce noise and unnecessary checks. - Use
When/Unlesssparingly for conditional validation; prefer explicit DTOs per use-case if the validation surface differs greatly. - For cross-field validation, use
DependentRulesorMuston a root object:
RuleFor(x => x).Must(x => IsValidCombination(x.Some, x.Other));
- Validate externally for expensive checks (network/db) and consider running them asynchronously with
MustAsync. - Use
RuleForEachfor collection items andIncludeto reuse other validators.
Testing validators
- Unit test validators directly — instantiating the validator and calling
Validate/ValidateAsyncis fast and deterministic.
var validator = new CreateOrderDtoValidator();
var result = validator.Validate(dto);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.PropertyName == "Amount");
- Mock external dependencies for async rules using test doubles or extract the dependency behind an interface passed into the validator's constructor.
Error mapping and API responses
- FluentValidation produces
ValidationFailureobjects containingPropertyName,ErrorMessage, andAttemptedValue. - Map failures to API error response models consistently (problem details, field errors list).
- Consider grouping errors by field and return a compact payload for clients.
Performance considerations
- Avoid expensive synchronous work in rule delegates — prefer async variants.
- If validators call DB or service methods, ensure they are async and avoid N+1 patterns; batch checks where possible (e.g., prefetch referenced ids before validation).
- Keep
CascadeModebehavior in mind; short-circuit can reduce extra checks.
Advanced topics (senior-level)
- Custom property validators: implement
IPropertyValidatorfor reusable complex checks. - Interceptors: use
IValidatorInterceptorto hook into validation execution for logging or transformation. - Validators as filters: using validators within a MediatR pipeline behavior to validate requests before handlers run.
Example MediatR pipeline registration:
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any()) throw new ValidationException(failures);
return await next();
}
}
- Localization: FluentValidation supports localized messages. Avoid composing error messages in validators; prefer resource keys.
- Using
RuleSetto group validations by scenario (e.g.,Create,Update) and call specific rule sets when required.
Checklist for senior devs when reviewing validation
- Are validators isolated and focused on rules only?
- Are expensive checks async and stubbed in tests?
- Is composition used instead of duplication (Include/SetValidator)?
- Are messages localizable and consistent?
- Are validators registered and resolved correctly in DI (right lifetime)?
- Are validators used in the pipeline (MediatR/Controller) consistently?
- Are cross-field validations explicit and tested?
Quick code snippets
Register validators:
services.AddFluentValidationAutoValidation();
services.AddValidatorsFromAssemblyContaining<CreateOrderDtoValidator>();
Conditional rule example:
RuleFor(x => x.Discount)
.GreaterThan(0)
.When(x => x.HasDiscount);
Async rule example:
RuleFor(x => x.CustomerId)
.MustAsync(async (id, ct) => await _customerService.Exists(id))
.WithMessage("Customer not found");