Hola a Todos. Uno de los errores más comunes al construir aplicaciones en .NET es mezclar validaciones directamente en las entidades usando atributos como:
- [Required]
- [MaxLength]
- [EmailAddress]
Aunque esto funciona, introduce varios problemas:
- ❌ Acopla el dominio a frameworks como ASP.NET
- ❌ Dificulta las pruebas unitarias
- ❌ Mezcla responsabilidades
- ❌ Reduce reutilización
En este artículo veremos cómo usar FluentValidation de forma correcta en .NET 10, siguiendo Clean Architecture y usando MediatR, manteniendo el dominio completamente limpio.
🧨 El problema: entidades contaminadas
public class User
{
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
}
¿Qué está mal aquí?
- La entidad depende de
System.ComponentModel.DataAnnotations - No puedes reutilizarla fácilmente fuera de ASP.NET
- Las validaciones no son fácilmente testeables de forma aislada
✅ Principio clave: dominio limpio
En Clean Architecture, el dominio debe ser:
- Independiente
- Puro
- Libre de frameworks
Entidad correcta
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
Sin validaciones, sin atributos y sin dependencias externas.
🧠 ¿Dónde deben ir las validaciones?
En la Application Layer, no en el dominio.
Ahí es donde FluentValidation realmente brilla.
🔥 Instalación
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
✍️ Caso real: crear usuario con CQRS + MediatR
1. Command
using MediatR;
public record CreateUserCommand(string Name, string Email) : IRequest<Guid>;
2. Validator
using FluentValidation;
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("El nombre es obligatorio")
.MaximumLength(100);
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage("El email es obligatorio")
.EmailAddress()
.WithMessage("Email inválido");
}
}
Aquí está la gran ventaja: las reglas viven fuera del dominio.
⚙️ Handler de MediatR
using MediatR;
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User
{
Name = request.Name,
Email = request.Email
};
// Simulación de persistencia
return Guid.NewGuid();
}
}
En este punto el request ya llega validado gracias al pipeline.
🧩 Validación automática con MediatR Pipeline Behavior
Aquí es donde el enfoque se vuelve mucho más potente.
En vez de validar manualmente en cada handler, puedes centralizar toda la validación usando un PipelineBehavior.
using FluentValidation;
using MediatR;
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken))
);
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
{
throw new ValidationException(failures);
}
}
return await next();
}
}
Con esto, cualquier command que tenga un validator se valida automáticamente.
🧱 Registro en Program.cs
using FluentValidation;
using MediatR;
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateUserCommand).Assembly);
});
builder.Services.AddValidatorsFromAssembly(typeof(CreateUserCommandValidator).Assembly);
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
🌐 Uso desde Minimal API
app.MapPost("/users", async (CreateUserCommand command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return Results.Ok(result);
});
No necesitas escribir validaciones manuales en controllers ni endpoints.
🧪 Testing del Validator
Una de las grandes ventajas de FluentValidation es que puedes probar las reglas fácilmente.
using FluentValidation.TestHelper;
using Xunit;
public class CreateUserCommandValidatorTests
{
private readonly CreateUserCommandValidator _validator = new();
[Fact]
public void Should_Have_Error_When_Email_Is_Invalid()
{
var command = new CreateUserCommand("Romny", "correo-invalido");
var result = _validator.TestValidate(command);
result.ShouldHaveValidationErrorFor(x => x.Email);
}
}
⚖️ Validar DTO o validar entidad
Una de las preguntas más comunes es: ¿debo validar la entidad o el DTO?
La recomendación es validar Commands o DTOs.
¿Por qué?
- Representan la entrada del sistema
- No contaminan el dominio
- Mantienen mejor separación de responsabilidades
- Encajan mejor con CQRS
🚫 Anti-patrones comunes
Validar en Controllers
if (string.IsNullOrEmpty(request.Email))
{
throw new Exception("Email inválido");
}
Validar en entidades
[Required]
public string Email { get; set; }
Mezclar validación con lógica de negocio
if (request.Email.Contains("gmail"))
{
// lógica de negocio
}
✅ Buenas prácticas
- Validar en Application Layer
- Usar FluentValidation
- Validar Commands y DTOs
- Integrar validación automática con MediatR
- Mantener el dominio limpio
- Escribir pruebas unitarias para validadores
🧠 Conclusión
Usar FluentValidation con Clean Architecture y MediatR en .NET 10 te permite:
- Separar responsabilidades correctamente
- Tener validaciones testeables
- Centralizar validaciones
- Mantener el dominio limpio
- Escalar la arquitectura sin deuda técnica
Si estás construyendo sistemas modernos en .NET, este enfoque ya no es un extra: es prácticamente el estándar.
🎯 TL;DR
- No pongas validaciones en entidades
- Usa FluentValidation en Application Layer
- Integra con MediatR usando Pipeline Behavior
- Valida Commands y DTOs, no Domain Models
- Mantén el dominio limpio
Espero con esto poder ayudarlos.
Sl2
Romny
Top comments (0)