What it is
FluentValidation defines validation rules in a strongly-typed fluent API, separate from the model itself.
Write a validator class per model. Rules are chained, support conditions and custom predicates, and can be injected with dependencies such as a repository.
Installation
dotnet add package FluentValidationGetting started
The smallest useful thing you can do with it, and what each part means.
csharp
public class CreateBookValidator : AbstractValidator<CreateBookRequest>
{
public CreateBookValidator(IBookRepository repo)
{
RuleFor(x => x.Title)
.NotEmpty()
.MaximumLength(200);
RuleFor(x => x.Year)
.InclusiveBetween(1400, DateTime.UtcNow.Year + 1)
.WithMessage("Year must be between 1400 and next year.");
RuleFor(x => x.Isbn)
.Must(BeValidIsbn).WithMessage("Not a valid ISBN.")
.MustAsync(async (isbn, ct) => !await repo.ExistsAsync(isbn, ct))
.WithMessage("That ISBN already exists.")
.When(x => !string.IsNullOrEmpty(x.Isbn)); // optional field
}
}Advanced usage
Where the library earns its place over a simpler alternative.
csharp
RuleSet("Create", () => RuleFor(x => x.Id).Empty());
RuleSet("Update", () => RuleFor(x => x.Id).NotEmpty());
RuleForEach(x => x.Chapters).SetValidator(new ChapterValidator());
// Registration
builder.Services.AddValidatorsFromAssemblyContaining<CreateBookValidator>();
// In a minimal API endpoint
app.MapPost("/books", async (CreateBookRequest req, IValidator<CreateBookRequest> validator) =>
{
var result = await validator.ValidateAsync(req);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
return Results.Created(...);
});Errors and fixes
The failures you are most likely to hit, and what actually resolves them.
- Async rule throws in a synchronous Validate call
- A validator containing MustAsync must be invoked with ValidateAsync.
- Nested object rules do not run
- Child validators are not automatic. Wire them with SetValidator or RuleForEach.
Best practices
- Keep validators in their own classes, one per request model.
- Use MustAsync for checks that touch a database, and keep them off the hot path where possible.
- Use When for conditional rules rather than nesting ifs inside a custom validator.
- Return ValidationProblem so clients get a consistent, standard error shape.
Background
Why it exists, and what it was reacting to.
Created by Jeremy Skinner as an alternative to data annotations, which mix validation into the model and cannot express conditional or cross-field rules cleanly.
