Tutorials ASP.NET Core MVC Mastery
Fluent Validation
On this page
FluentValidation in ASP.NET Core — Enterprise-Grade Validation
Data Annotations (like [Required] and [StringLength]) are great for simple apps, but they pollute your domain models and crumble under complex business logic. FluentValidation is the industry standard for .NET enterprise applications, separating validation rules from classes using a clean, fluent interface with full Dependency Injection support.
1. WHY FluentValidation Over Data Annotations?
- Separation of Concerns: Domain models stay clean. Validation logic lives in separate
Validatorclasses. - Complex Cross-Property Rules: "EndDate must be greater than StartDate" is incredibly painful with Data Annotations, but trivial in FluentValidation.
- Conditional Validation: "CreditCardNumber is required ONLY IF PaymentMethod == 'CreditCard'".
- Dependency Injection: Validators can inject database contexts or external services to validate data against a live database.
2. Setup & Installation
Install the required NuGet packages. Note that you no longer need the bulky AutoValidation package; modern architecture prefers explicit manual validation or API Endpoint Filters.
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
3. REAL-TIME PRODUCTION EXAMPLES
Step 1: The Model (Clean and Attribute-Free)
public class UserRegistrationDto
{
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; }
public DateTime DateOfBirth { get; set; }
public AddressDto Address { get; set; } // Nested object
}
Step 2: The Validator (AbstractValidator)
// Validators/UserRegistrationValidator.cs
using FluentValidation;
public class UserRegistrationValidator : AbstractValidator<UserRegistrationDto>
{
private readonly IUserRepository _userRepository;
// We can use Dependency Injection inside validators!
public UserRegistrationValidator(IUserRepository userRepository)
{
_userRepository = userRepository;
// Base rules
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First Name is required")
.MaximumLength(50).WithMessage("Maximum 50 characters allowed");
// Chained complex rules
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8)
.Matches("[A-Z]").WithMessage("Password must contain 1 uppercase letter")
.Matches("[0-9]").WithMessage("Password must contain 1 number");
// Cross-property validation
RuleFor(x => x.ConfirmPassword)
.Equal(x => x.Password).WithMessage("Passwords do not match");
// Complex business rules
RuleFor(x => x.DateOfBirth)
.LessThan(DateTime.Now.AddYears(-18))
.WithMessage("You must be at least 18 years old to register.");
// Async Database Validation via DI
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.MustAsync(async (email, cancellation) => {
bool exists = await _userRepository.EmailExistsAsync(email);
return !exists; // Must return true to pass
})
.WithMessage("This email is already registered.");
// Validating nested objects
RuleFor(x => x.Address).SetValidator(new AddressValidator());
}
}
Step 3: Registration in Program.cs
// Using DependencyInjectionExtensions makes this a one-liner.
// It scans the assembly, finds all classes inheriting AbstractValidator,
// and registers them in the DI container natively.
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
Step 4: Explicit Execution in Controller
public class AccountController : Controller
{
private readonly IValidator<UserRegistrationDto> _validator;
public AccountController(IValidator<UserRegistrationDto> validator)
{
_validator = validator;
}
[HttpPost]
public async Task<IActionResult> Register(UserRegistrationDto model)
{
// 1. Execute Validation
var validationResult = await _validator.ValidateAsync(model);
// 2. Check Result
if (!validationResult.IsValid)
{
// 3. Map FluentValidation errors back to standard MVC ModelState
// This ensures standard Tag Helpers () still work!
foreach (var error in validationResult.Errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
return View(model);
}
// Proceed with registration...
return RedirectToAction("Success");
}
}
4. Elegant Extension: AddToModelState()
Writing that foreach loop in every controller violates DRY. Let's create an extension method to bridge FluentValidation and ASP.NET Core MVC instantly.
// Extensions/ValidationExtensions.cs
public static class ValidationExtensions
{
public static void AddToModelState(this FluentValidation.Results.ValidationResult result,
ModelStateDictionary modelState)
{
foreach (var error in result.Errors)
{
modelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
}
// Controller usage shrinks to:
if (!validationResult.IsValid)
{
validationResult.AddToModelState(ModelState);
return View(model);
}
5. Best Practices
✅ DO
- Use Explicit Validation (injecting
IValidator) rather than auto-validation packages, which obscure flow. - Use
CascadeMode = CascadeMode.Stopif you want validation to halt at the first failure on a specific property, saving database calls (e.g. don't check DB uniqueness if email is empty).
❌ DON'T
- Don't mix Data Annotations and FluentValidation in the same class. Commit to one architecture.
- Avoid heavy, long-running processes inside
MustAsync. Remember that validators block request execution.
6. Interview Mastery
Q: "Why has the community moved away from FluentValidation's AutoValidation package in modern .NET?"
Architect Answer: "AutoValidation hooks deep into the ASP.NET Core MVC input formatters to validate models before they even hit the controller. While this sounds convenient, it causes two critical problems in enterprise apps. First, it makes it impossible to use asynchronous validation rules (like database lookups via MustAsync) because the MVC binding pipeline is fundamentally synchronous. Second, it hides the control flow. Modern .NET architecture prefers Explicit Validation — injecting IValidator<T> into a controller, service, or API Endpoint Filter — allowing full asynchronous context, better testability, and clear execution flow."