FluentValidation in ASP.NET Core - Clean, Flexible Model Validation for Modern .NET Apps
Simplify your API model validation using FluentValidation clean, maintainable, and testable
📌 Introduction
FluentValidation is a popular .NET library that makes it easy to check if your objects have valid data. It’s like a set of rules you create to make sure your objects follow the correct format. The best part? It uses a clean and readable style that even beginners can understand.
In this guide, we’ll walk you through how to use FluentValidation step-by-step, using a practical User Registration example. By the end, you’ll see how it simplifies input validation, keeps your code clean, and improves maintainability in your ASP.NET Core applications.
📘 What is FluentValidation?
Think of FluentValidation as a rulebook for your objects.
Imagine you're building a library system. You need to ensure:
The library has a name
Each book has a valid ID, title, and author
The library has a valid address, phone number, and at least five books
Instead of writing lots of messy if
conditions, FluentValidation allows you to define all these rules in one place — using a clean, fluent syntax. This approach keeps your logic centralized, testable, and easier to maintain as your application grows.
📁 Step 1: Install FluentValidation Packages
Run the following commands:
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
👴 The Traditional Way – Using Data Annotations
Here’s how user registration validation might look with Data Annotations:
public class UserRegistrationRequest
{
[Required]
[MinLength(4)]
public string? FirstName { get; set; }
[Required]
[MaxLength(10)]
public string? LastName { get; set; }
[Required]
[EmailAddress]
public string? Email { get; set; }
[Required]
public string? Password { get; set; }
[Compare("Password")]
public string? ConfirmPassword { get; set; }
}
🔴 While it works, it tightly couples validation to your model and lacks flexibility for complex rules or testing.
✅ The FluentValidation Way – Clean & Flexible
Step 2: Define the 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 the Validator
using FluentValidation;
public class UserRegistrationValidator : AbstractValidator<UserRegistrationRequest>
{
public UserRegistrationValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required.")
.MinimumLength(4).WithMessage("First name must be at least 4 characters long.");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required.")
.MaximumLength(10).WithMessage("Last name cannot exceed 10 characters.");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required.")
.EmailAddress().WithMessage("{PropertyName} is invalid! Please check!");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required.");
RuleFor(x => x.ConfirmPassword)
.NotEmpty().WithMessage("Please confirm your password.")
.Equal(x => x.Password).WithMessage("Passwords do not match!");
}
}
Step 4: Register FluentValidation in Program.cs
// Add services to the container.
builder.Services.AddControllers(options =>
{
options.Filters.Add<FluentValidationActionFilter>();
});
// Register validators from assembly
builder.Services.AddValidatorsFromAssemblyContaining<UserRegistrationValidator>();
Step 5: Custom Validation Filter
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace FluentValidationSample.ActionFilter {
public class FluentValidationActionFilter: IAsyncActionFilter {
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 = context.HttpContext.RequestServices.GetService(validatorType) as IValidator;
if (validator != null) {
var validationResult = await validator.ValidateAsync(new ValidationContext < object > (argument));
if (!validationResult.IsValid) {
foreach(var error in validationResult.Errors) {
context.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
context.Result = new BadRequestObjectResult(context.ModelState);
return;
}
}
}
await next();
}
}
}
Step 6: Controller Example
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(UserRegistrationRequest userRegistration)
{
return Ok(new
{
Message = "User registration data is valid.",
Data = userRegistration
});
}
}
📤 Sample Request
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"password": "secret123",
"confirmPassword": "secret123"
}
❌ Example Validation Response
{
"FirstName": ["First name is required."],
"Email": ["Email is invalid! Please check!"],
"ConfirmPassword": ["Passwords do not match!"]
}
📚 Built-in Validators in FluentValidation (Line-by-Line)
Here are commonly used validators:
NotNull
Ensures the value is notnull
.RuleFor(x => x.Name).NotNull();
NotEmpty
Ensures the value is notnull
, empty, or whitespace.RuleFor(x => x.Name).NotEmpty();
Equal / NotEqual
Match or avoid matching a specific value or another property.RuleFor(x => x.Role).Equal("Admin");
RuleFor(x => x.Name).NotEqual(x => x.Nickname);
Length / MinimumLength / MaximumLength
Validate string length.RuleFor(x => x.Username).Length(4, 20);
RuleFor(x => x.Password).MinimumLength(6);
RuleFor(x => x.LastName).MaximumLength(10);
LessThan / LessThanOrEqualTo
Numeric validation.RuleFor(x => x.Age).LessThan(60);
RuleFor(x => x.Score).LessThanOrEqualTo(100);
GreaterThan / GreaterThanOrEqualTo
RuleFor(x => x.Age).GreaterThan(18);
RuleFor(x => x.Salary).GreaterThanOrEqualTo(5000);
InclusiveBetween / ExclusiveBetween
Range validation.RuleFor(x => x.Rating).InclusiveBetween(1, 5);
RuleFor(x => x.Id).ExclusiveBetween(1, 10);
Matches
Regex pattern match.RuleFor(x => x.Email).Matches(@"^\S+@\S+\.\S+$");
Must
Custom logic.RuleFor(x => x.Name).Must(name => name.StartsWith("A"));
EmailAddress / CreditCard
Format validations.RuleFor(x => x.Email).EmailAddress();
RuleFor(x => x.CardNumber).CreditCard();
IsInEnum / IsEnumName
Enum validations.RuleFor(x => x.Level).IsInEnum();
RuleFor(x => x.LevelName).IsEnumName(typeof(Level));
Empty / Null
Opposites of NotEmpty/NotNull.RuleFor(x => x.Remarks).Empty();
RuleFor(x => x.MiddleName).Null();
PrecisionScale
Decimal precision and scale.RuleFor(x => x.Amount).PrecisionScale(5, 2, false);
🧠 Why Use FluentValidation?
✅ Clean and readable syntax
🔁 Reusable, testable validation logic
📦 Keeps validation separate from business logic
🌍 Supports localization
🧪 Great for unit testing
⚠️ Important: Changes After FluentValidation 11.x
Starting from FluentValidation v11, the automatic integration with ASP.NET Core's validation pipeline was removed.
🔄 What Changed?
❌
FluentValidation.AspNetCore
is deprecated❌
AddFluentValidation()
is no longer available✅ Manual wiring is required using
ActionFilter
andModelState
handling
✅ What You Should Do Instead
Install the main FluentValidation package:
dotnet add package FluentValidation
Register validators manually in
Program.cs
:
builder.Services.AddValidatorsFromAssemblyContaining<UserRegistrationValidator>();
Use a custom
FluentValidationActionFilter
to validate models and return structured errors:
// Add services to the container.
builder.Services.AddControllers(options =>
{
options.Filters.Add<FluentValidationActionFilter>();
});
This approach gives you full control over how validation errors are handled and avoids any hidden behaviors from the framework.
✅ Summary
FluentValidation gives you precise control over model validation in ASP.NET Core — all while keeping your code clean and maintainable. Whether you're validating user registration forms or complex objects, FluentValidation's rich API and built-in validators make it a powerful choice.
📁 GitHub Example
👉 Full working code available at:
🔗 https://github.com/KanaiyaKatarmal/FluentValidationSample
I hope you found this guide helpful and informative.
Thanks for reading!
If you enjoyed this article, feel free to share it and follow me for more practical, developer-friendly content like this.