Validation is one of those problems that starts simple and quietly gets out of hand. At first a couple of if statements in your controller or service do the job. Before long you have scattered checks, duplicated logic, and error messages hard-coded all over the place. FluentValidation solves this by separating validation rules from your business objects into dedicated, testable validator classes.
In this article, you will learn how to use FluentValidation in C# to build cleaner, more maintainable validation — from your first validator to full ASP.NET Core integration.
What is FluentValidation?
FluentValidation is a popular .NET library for building strongly-typed validation rules. Instead of sprinkling validation logic through your code, you create a validator class that inherits from AbstractValidator<T>, where T is the type you want to validate. Rules are defined in the constructor using a fluent, readable syntax.
At a high level:
- You define a validator for each model class.
- Each rule targets a property via
RuleForand a lambda expression. - Calling
Validatereturns aValidationResultyou can inspect. - Validators compose, so complex objects reuse simpler ones.
This keeps your models as plain data containers and moves validation into focused, reusable units.
Why use FluentValidation?
FluentValidation is a great fit for .NET projects because it encourages a clean separation of concerns and removes a whole class of boilerplate. Key benefits:
- No more scattered checks – validation lives in one place per model, not inline in every handler.
- Fluent, readable syntax – rules read like sentences, making intent obvious.
- Compile-time safety –
RuleForuses expressions, so refactoring a property name breaks at build time, not runtime. - Easy to test – validators are plain classes you can unit-test in isolation.
- Reusable and composable – child validators handle nested objects cleanly.
- Async support – rules can validate against databases or external services.
- Plays well with DI – validators register with the service provider and inject into controllers or endpoints.
Getting started
Add the package to your project:
dotnet add package FluentValidation If you plan to use dependency injection, also add the extensions package:
dotnet add package FluentValidation.DependencyInjectionExtensions Creating your first validator
Imagine you have a Customer class:
public class Customer
{
public int Id { get; set; }
public string Surname { get; set; }
public string Forename { get; set; }
public decimal Discount { get; set; }
public string Address { get; set; }
} Define a validator by inheriting from AbstractValidator<Customer> and describing the rules in the constructor:
using FluentValidation;
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.Surname).NotNull();
RuleFor(customer => customer.Forename).NotNull();
}
} To run the validator, instantiate it and call Validate:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
ValidationResult result = validator.Validate(customer); The ValidationResult exposes two useful properties:
IsValid– a boolean indicating whether validation succeeded.Errors– a collection ofValidationFailureobjects with the details.
Write any failures to the console like this:
if (!result.IsValid)
{
foreach (var failure in result.Errors)
{
Console.WriteLine($"Property {failure.PropertyName} failed validation. Error was: {failure.ErrorMessage}");
}
} You can also combine all messages with ToString, passing a custom separator if you like:
string allMessages = result.ToString("~"); Chaining validators
Multiple rules for the same property can be chained together in one expression:
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.Surname)
.NotNull()
.NotEqual("foo");
}
} This ensures the surname is not null and is not equal to the string foo.
Throwing exceptions
Instead of inspecting the result yourself, you can tell FluentValidation to throw when validation fails using ValidateAndThrow:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
validator.ValidateAndThrow(customer); This throws a ValidationException containing the error messages in its Errors property. ValidateAndThrow is an extension method, so make sure you have using FluentValidation; at the top of your file. It is a convenience wrapper around the options API:
validator.Validate(customer, options => options.ThrowOnFailures()); Reusing validators for complex properties
Validators compose beautifully. Consider a Customer that owns an Address:
public class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string Line1 { get; set; }
public string Line2 { get; set; }
public string Town { get; set; }
public string Country { get; set; }
public string Postcode { get; set; }
} Define an AddressValidator:
public class AddressValidator : AbstractValidator<Address>
{
public AddressValidator()
{
RuleFor(address => address.Postcode).NotNull();
RuleFor(address => address.Town).NotNull();
}
} Then reuse it inside CustomerValidator with SetValidator:
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.Name).NotNull();
RuleFor(customer => customer.Address).SetValidator(new AddressValidator());
}
} When you validate a customer, FluentValidation runs both validators and combines the results into a single ValidationResult. If the child property is null, the child validator is skipped.
You can also define child rules inline:
RuleFor(customer => customer.Address.Postcode).NotNull(); Note that this does not null-check Address automatically, so add a condition explicitly:
RuleFor(customer => customer.Address.Postcode)
.NotNull()
.When(customer => customer.Address != null); Integrating with ASP.NET Core
FluentValidation fits into ASP.NET Core in two main ways: manual validation and automatic validation.
Registering validators with DI
Register each validator with the service provider:
builder.Services.AddScoped<IValidator<Person>, PersonValidator>(); Or, using the FluentValidation.DependencyInjectionExtensions package, register every validator in an assembly in one go:
builder.Services.AddValidatorsFromAssemblyContaining<PersonValidator>(); Manual validation in a controller
With manual validation, inject the validator and invoke it against your model:
[ApiController]
[Route("api/[controller]")]
public class PeopleController : ControllerBase
{
private readonly IValidator<Person> _validator;
public PeopleController(IValidator<Person> validator)
{
_validator = validator;
}
[HttpPost]
public async Task<IActionResult> Create(Person person)
{
ValidationResult result = await _validator.ValidateAsync(person);
if (!result.IsValid)
{
return BadRequest(result.Errors);
}
// Save the person...
return Ok();
}
} Because the validator is registered with the service provider, it is injected via the constructor and can even contain asynchronous rules.
Manual validation with Minimal APIs
For Minimal APIs, inject the validator directly into the endpoint:
app.MapPost("/person", async (IValidator<Person> validator, Person person) =>
{
ValidationResult result = await validator.ValidateAsync(person);
if (!result.IsValid)
{
return Results.ValidationProblem(result.ToDictionary());
}
// Save the person...
return Results.Created($"/{person.Id}", person);
}); The ToDictionary method (available from FluentValidation 11.1) converts the failures into the shape ASP.NET expects for ValidationProblem.
Automatic validation
FluentValidation can also plug into ASP.NET’s validation pipeline so models are validated before your action runs. The legacy FluentValidation.AspNetCore package provides this, but it is no longer recommended for new projects: it is MVC-only, does not support asynchronous rules, and is harder to debug. If you want automatic validation in a modern, async-friendly way, a filter-based approach such as the third-party SharpGrip.FluentValidation.AutoValidation package is a better fit.
Summary
FluentValidation takes the pain out of validation in .NET. By defining rules in dedicated validator classes, you keep models clean, reduce duplication, and make validation logic easy to test and reuse. Chaining rules, composing child validators, and integrating with dependency injection make it a natural fit for ASP.NET Core — whether you prefer manual validation in controllers and Minimal APIs, or a filter-based automatic approach.
Have you tried FluentValidation in your projects? The full documentation is a great next step: docs.fluentvalidation.net.