FluentValidation is one of the cleanest and most maintainable ways to validate incoming API requests in ASP.NET Core. But instead of calling validators manually inside controllers, the best practice is to build a custom validation pipeline that runs automatically before the controller executes.
This article shows a complete, step-by-step implementation with real-world patterns used in enterprise applications.
1. Why Build a Custom Validation Pipeline?
A validation pipeline helps you:
Remove validation logic from controllers
Enforce consistent validation across all endpoints
Automatically validate every request DTO
Return clean, structured validation errors
Add logging and audit in one place
2. Install FluentValidation
dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore
3. Create Your Request DTO
public class CreateUserRequest
{
public string FullName { get; set; }
public string Email { get; set; }
public int? Age { get; set; }
}
4. Create a FluentValidator for the DTO
using FluentValidation;
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserRequestValidator()
{
RuleFor(x => x.FullName)
.NotEmpty()
.MaximumLength(50);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
RuleFor(x => x.Age)
.NotNull()
.InclusiveBetween(18, 60);
}
}
5. Build a Custom Validation Pipeline (Middleware/Filter)
Option: Use an Action Filter (cleaner and recommended)
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public class ValidationFilter : IAsyncActionFilter
{
private readonly IServiceProvider _serviceProvider;
public ValidationFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
foreach (var argument in context.ActionArguments.Values)
{
if (argument == null) continue;
var validatorType = typeof(IValidator<>).MakeGenericType(argument.GetType());
var validator = _serviceProvider.GetService(validatorType) as IValidator;
if (validator == null) continue;
ValidationResult result = await validator.ValidateAsync(new ValidationContext<object>(argument));
if (!result.IsValid)
{
var errors = result.Errors
.Select(e => new { field = e.PropertyName, message = e.ErrorMessage });
context.Result = new BadRequestObjectResult(new
{
Status = "ValidationFailed",
Errors = errors
});
return;
}
}
await next();
}
}

Join the conversation! Your thoughts help the community grow.