The model validation is the process of checking that the received data meets the expected rules.
A basic rule of web development is not to trust client data. It doesn’t matter if it comes from your React application, a mobile app, or an external integration: everything that comes over the network can be incorrect or malicious.
If you expect a price, they will send you a negative number. If you expect an email, they will send you “hello”. If you expect a date, they will send you the text of Don Quixote.
To prevent this junk data from corrupting our database or causing exceptions, we need a validation layer.
In .NET we have several ways to do this. Let’s look at native validation with Data Annotations and an alternative based on FluentValidation.
Data Annotations
This is the method that comes integrated in .NET. It consists of decorating the properties of our DTOs with attributes that define the rules.
It is very convenient because the validation logic lives alongside the data definition.
Practical Example
using System.ComponentModel.DataAnnotations;
public class CrearUsuarioDto
{
[Required(ErrorMessage = "El nombre es obligatorio")]
[StringLength(50, MinimumLength = 3)]
public string Nombre { get; set; }
[Required]
[EmailAddress] // Validates format [email protected]
public string Email { get; set; }
[Range(18, 99)]
public int Edad { get; set; }
[RegularExpression(@"^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$",
ErrorMessage = "La contraseña debe tener letras y números")]
public string Password { get; set; }
}How does it execute?
If you use [ApiController] in your controllers, you don’t need to do anything.
The framework validates the object automatically before entering the method. If any rule is not met, it instantly returns a 400 Bad Request with the list of errors.
If you don’t use [ApiController], you must check the model state manually:
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}FluentValidation
Data Annotations is fine for simple things, but it has problems:
- ❌ Clutters your DTOs: You mix data definitions with business rules.
- ❌ Limited logic: It’s difficult to validate things like “Field X is required only if field Y is true”.
- ❌ Hard to test: Testing attributes is cumbersome.
To solve this, many .NET projects use the FluentValidation library.
Installation
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensionsSeparating the Logic
With FluentValidation, we create a separate class (a Validator) for each DTO. The DTO remains clean and pure.
using FluentValidation;
public class CrearUsuarioValidator : AbstractValidator<CrearUsuarioDto>
{
public CrearUsuarioValidator()
{
RuleFor(x => x.Nombre)
.NotEmpty().WithMessage("El nombre no puede estar vacío")
.Length(3, 50);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
// Complex conditional rules
RuleFor(x => x.TarjetaCredito)
.NotEmpty()
.When(x => x.MetodoPago == "Tarjeta")
.WithMessage("Si pagas con tarjeta, necesitamos el número");
// Custom validations
RuleFor(x => x.Edad)
.Must(SerMayorDeEdad).WithMessage("Debes ser mayor de edad");
}
private bool SerMayorDeEdad(int edad)
{
return edad >= 18;
}
}Registration in Program.cs
We register the validators in the dependency injection container:
builder.Services.AddValidatorsFromAssemblyContaining<Program>();Then we can inject IValidator<CrearUsuarioDto> and execute the validation where appropriate.
The old automatic integration of FluentValidation.AspNetCore with the MVC pipeline is no longer the primary recommendation for new projects. Additionally, it does not work with Minimal APIs. For modern code, it is usually clearer to use manual validation, endpoint filters, or a specific filter-based integration.
Data Annotations vs FluentValidation: Which to Choose?
| Feature | Data Annotations | FluentValidation |
|---|---|---|
| Complexity | Very low | Medium |
| Location | In the DTO itself (Attributes) | In a separate class (SRP) |
| Power | Basic (formats, ranges) | High (conditional logic, queries, composed rules) |
| Dependency Injection | Limited | Allows injecting services into the validator |
| Organization | Rules alongside the DTO | Rules in a separate class |
- For quick prototypes or trivial validations (
Required), use Data Annotations. - For conditional rules or validators requiring dependencies, FluentValidation can keep the code more organized and easier to test.
Validating in Minimal APIs
In Minimal APIs we don’t have the automatic validation from [ApiController]. If we use FluentValidation, we can inject the validator and execute it explicitly:
app.MapPost("/usuarios", async (CrearUsuarioDto dto, IValidator<CrearUsuarioDto> validator) =>
{
var resultado = await validator.ValidateAsync(dto);
if (!resultado.IsValid)
{
return Results.ValidationProblem(resultado.ToDictionary());
}
// Save logic...
return Results.Ok();
});Since .NET 10, Minimal APIs can also validate Data Annotations natively by registering builder.Services.AddValidation(). If the model is not valid, the endpoint returns a 400 Bad Request before executing the handler.