FluentValidation · Additional notes

2 min read
Mid-level6 min read
Rapid 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 SetValidator or separate AbstractValidator<T> types.
  • Use CascadeMode.Stop when you want to short-circuit rules to reduce noise and unnecessary checks.
  • Use When/Unless sparingly for conditional validation; prefer explicit DTOs per use-case if the validation surface differs greatly.
  • For cross-field validation, use DependentRules or Must on 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 RuleForEach for collection items and Include to reuse other validators.

Testing validators

  • Unit test validators directly — instantiating the validator and calling Validate/ValidateAsync is 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 ValidationFailure objects containing PropertyName, ErrorMessage, and AttemptedValue.
  • 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 CascadeMode behavior in mind; short-circuit can reduce extra checks.

Advanced topics (senior-level)

  • Custom property validators: implement IPropertyValidator for reusable complex checks.
  • Interceptors: use IValidatorInterceptor to 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 RuleSet to 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");

See also