How to Use FluentValidation in ASP.NET Core — Write Powerful and Clean Validations
Learn how to use FluentValidation in ASP.NET Core to build clean and powerful validation logic. Understand why it’s better than Data Annotations with real examples and step-by-step setup.
If you’ve ever built an ASP.NET Core Web API, you already know how crucial it is to validate user input.
Most developers start with Data Annotations — it’s easy, quick, and built right into .NET. But as your project grows, it can start feeling messy and restrictive.
In this guide, we’ll see why Data Annotations aren’t always the best choice and how to use FluentValidation, a powerful library that gives you cleaner, more flexible, and maintainable validation in ASP.NET Core.
🚦 Why Validation Matters
Validation is one of the simplest yet most important steps in building secure, reliable applications. Without proper validation, even the most robust API can break or be exploited.
Here’s why validation is so essential:
- 🧩 Data Integrity: Ensures user input follows required formats and constraints, maintaining consistent and accurate data in your system.
- 🔒 Security: Prevents attacks like SQL Injection, XSS, or CSRF by sanitizing user inputs.
- 💡 Better UX: Helps users fix mistakes faster by showing clear and instant feedback.
- ⚙️ Business Logic Enforcement: Makes sure the data aligns with your business rules (like matching passwords or valid emails).
⚠️ Why Data Annotations Aren’t Always Ideal
Let’s start with what most developers already use — Data Annotations.
public class UserRegistrationRequest
{
[Required(ErrorMessage = "First name is required.")]
public string? FirstName { get; set; }
[Required(ErrorMessage = "Last name is required.")]
public string? LastName { get; set; }
[Required(ErrorMessage = "Email is required.")]
[EmailAddress(ErrorMessage = "Invalid email address.")]
public string? Email { get; set; }
[Required(ErrorMessage = "Password is required.")]
[MinLength(6, ErrorMessage = "Password must be at least 6 characters.")]
public string? Password { get; set; }
[Compare("Password", ErrorMessage = "Passwords do not match.")]
public string? ConfirmPassword { get; set; }
}
This works fine for small apps or quick demos.
But as soon as your project grows, you start mixing validation logic with your model, violating the Separation of Concerns principle.
Here’s the problem:
- Validation rules are tightly coupled to your models.
- It’s hard to reuse or maintain validation logic.
- Large models become cluttered with attributes.
So what’s the alternative?
🚀 Meet FluentValidation — A Cleaner Way to Validate
FluentValidation is an open-source .NET library that makes your validation:
- Easier to read and maintain.
- Separated from your domain models.
- Highly customizable and testable.
It uses a fluent API to define rules, so you can write validations that read like English sentences.
For small apps, Data Annotations are fine. But for enterprise-level or cleanly architected projects, FluentValidation is a game changer.
⚙️ Setting Up FluentValidation in ASP.NET Core
Let’s walk through a real example using .NET 8 Web API and Visual Studio 2022.
Step 1: Install the NuGet Packages
Open the Package Manager Console and run:
Install-Package FluentValidation
Install-Package FluentValidation.DependencyInjectionExtensions
⚠️ Note: The
FluentValidation.AspNetCorepackage is now deprecated.
Step 2: Create a Request Model
public class UserRegistrationRequest
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public string? ConfirmPassword { get; set; }
}
Step 3: Create a Validator
Create a new folder named Validators and add a class UserRegistrationValidator.cs:
using FluentValidation;
public class UserRegistrationValidator : AbstractValidator<UserRegistrationRequest>
{
public UserRegistrationValidator()
{
RuleFor(x => x.Email).EmailAddress();
}
}
Step 4: Register the Validator in Program.cs
You can register validators manually or automatically.
Option 1: Register a single validator
builder.Services.AddScoped<IValidator<UserRegistrationRequest>, UserRegistrationValidator>();
Option 2: Register all validators in an assembly
builder.Services.AddValidatorsFromAssemblyContaining<UserRegistrationValidator>();
🧠 Validating Requests in an API Endpoint
Here’s a simple minimal API example:
app.MapPost("/register", async (UserRegistrationRequest request, IValidator<UserRegistrationRequest> validator) =>
{
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
return Results.Accepted();
});
Now, if a user sends an invalid email, you’ll get a clean validation error response:
{
"errors": {
"Email": ["'Email' is not a valid email address."]
}
}
💬 Adding More Validation Rules
Let’s add more logic to make it practical.
public class UserRegistrationValidator : AbstractValidator<UserRegistrationRequest>
{
public UserRegistrationValidator()
{
RuleFor(x => x.FirstName).NotEmpty().MinimumLength(4);
RuleFor(x => x.LastName).NotEmpty().MaximumLength(10);
RuleFor(x => x.Email).EmailAddress().WithMessage("{PropertyName} is invalid! Please check!");
RuleFor(x => x.Password).Equal(x => x.ConfirmPassword).WithMessage("Passwords do not match!");
}
}
Each rule is expressive and easy to read:
- ✅
FirstNamemust not be empty and at least 4 characters long. - ✅
LastNamemust not exceed 10 characters. - ✅ Custom message for invalid email.
- ✅ Password and ConfirmPassword must match.
🧩 Custom Validation Logic
You can even define custom validation functions:
private bool IsValidName(string name) => name.All(Char.IsLetter);
RuleFor(x => x.FirstName)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.MinimumLength(4)
.Must(IsValidName).WithMessage("{PropertyName} should contain only letters.");
Now if someone enters 1234 as a name, they’ll get a clear message:
"First Name should contain only letters."
🧾 Summary
FluentValidation is a must-have library for any modern ASP.NET Core project that values clean code and maintainability.
In summary:
- Data Annotations = simple, quick, but limited.
- FluentValidation = flexible, clean, and scalable.
- Perfect for teams following SOLID and clean architecture principles.
✅ Grab the Source Code: (Click here)
