AI News Hub Logo

AI News Hub

FluentValidation en .NET 10 sin ensuciar tus entidades (Clean Architecture + MediatR)

DEV Community
Romny Duarte

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. public class User { public string Name { get; set; } [Required] [EmailAddress] public string Email { get; set; } } 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 En Clean Architecture, el dominio debe ser: Independiente Puro Libre de frameworks public class User { public string Name { get; set; } public string Email { get; set; } } Sin validaciones, sin atributos y sin dependencias externas. En la Application Layer, no en el dominio. Ahí es donde FluentValidation realmente brilla. dotnet add package FluentValidation dotnet add package FluentValidation.DependencyInjectionExtensions dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection using MediatR; public record CreateUserCommand(string Name, string Email) : IRequest; using FluentValidation; public class CreateUserCommandValidator : AbstractValidator { 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. using MediatR; public class CreateUserCommandHandler : IRequestHandler { public async Task 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. 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 : IPipelineBehavior where TRequest : notnull { private readonly IEnumerable> _validators; public ValidationBehavior(IEnumerable> validators) { _validators = validators; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { if (_validators.Any()) { var context = new ValidationContext(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. 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)); 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. 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); } } 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 if (string.IsNullOrEmpty(request.Email)) { throw new Exception("Email inválido"); } [Required] public string Email { get; set; } if (request.Email.Contains("gmail")) { // lógica de negocio } 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 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. 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