Advanced Model Validation (FluentValidation)
Why Beyond Data Annotations?
While [Required] and [StringLength] are convenient, they fall short in professional enterprise applications due to:
- Logic Leakage: Business rules (e.g., “Field A depends on Field B”) are hardcoded into your data transfer objects (DTOs).
- Lack of Dependency Injection: You can’t easily perform a database check (e.g., “Is this email unique?”) inside a Data Annotation.
- No Async Support: Data Annotations only support synchronous validation.
The Solution: FluentValidation
FluentValidation is a library for .NET that uses a Fluent Interface and lambda expressions for building strongly-typed validation rules.
Core Concepts
1. Decoupled Logic
Validation rules live in a separate AbstractValidator<T> class, keeping your DTOs (POCOs) clean.
2. Complex & Conditional Rules
You can easily chain rules or apply them conditionally using .When() or .Unless().
3. Asynchronous Validation
Use .MustAsync() to perform I/O-bound checks (like database lookups) during the validation pipeline.
Technical Implementation
1. The Validator Class
public class RegisterUserRequestValidator : AbstractValidator<RegisterUserRequest>
{
private readonly IUserRepository _repo;
public RegisterUserRequestValidator(IUserRepository repo)
{
_repo = repo;
RuleFor(x => x.Email)
.NotEmpty().EmailAddress()
.MustAsync(BeUniqueEmail).WithMessage("Email already in use.");
RuleFor(x => x.Password)
.MinimumLength(8)
.Matches("[A-Z]").WithMessage("Must contain an uppercase letter.");
// Conditional Validation
RuleFor(x => x.CompanyName)
.NotEmpty()
.When(x => x.IsBusinessAccount);
}
private async Task<bool> BeUniqueEmail(string email, CancellationToken token)
{
return !await _repo.ExistsAsync(email);
}
}
Practice Exercise
Implement a validator where the EndDate must be after the StartDate. Then, register it in a modern .NET 8 Program.cs.
Answer
1. The Validator Logic
public class PeriodValidator : AbstractValidator<PeriodModel>
{
public PeriodValidator()
{
RuleFor(x => x.StartDate).NotEmpty();
RuleFor(x => x.EndDate)
.NotEmpty()
.GreaterThan(x => x.StartDate)
.WithMessage("End date must be after the start date.");
}
}
2. Registration in Program.cs
using FluentValidation;
using FluentValidation.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Register all validators in the assembly automatically
builder.Services.AddValidatorsFromAssemblyContaining<PeriodValidator>();
// Integration with ASP.NET Core MVC/API
builder.Services.AddFluentValidationAutoValidation();
Why This Works
- Single Responsibility Principle: The model defines the Data; the validator defines the Rules.
- Testability: You can write unit tests for your validator logic without needing to start a web server or mock the
ModelState. - Clarity: The “Fluent” syntax reads like a sentence, making the business requirements obvious to anyone reading the code.
Summary
FluentValidation is the gold standard for .NET applications. By separating validation from data, you create a system that is easier to maintain, faster to test, and capable of handling complex business rules that Data Annotations simply cannot represent.