Skip to content

FluentValidation

Developer UtilitiesValidationC#

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 FluentValidation

Getting started

The smallest useful thing you can do with it, and what each part means.

A validator with conditions
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
    }
}
Injecting the repository is the advantage over attributes: uniqueness checks belong with the other rules, not scattered into the handler.

Advanced usage

Where the library earns its place over a simpler alternative.

Rule sets, child validators and ASP.NET integration
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(...);
});
ToDictionary produces the shape ValidationProblem expects, so errors come back as a standard RFC 7807 payload without manual mapping.

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.