DEV Community

Romny Duarte
Romny Duarte

Posted on

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

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; }
}
Enter fullscreen mode Exit fullscreen mode

¿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; }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

✍️ Caso real: crear usuario con CQRS + MediatR

1. Command

using MediatR;

public record CreateUserCommand(string Name, string Email) : IRequest<Guid>;
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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<,>));
Enter fullscreen mode Exit fullscreen mode

🌐 Uso desde Minimal API

app.MapPost("/users", async (CreateUserCommand command, IMediator mediator) =>
{
    var result = await mediator.Send(command);
    return Results.Ok(result);
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

⚖️ 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");
}
Enter fullscreen mode Exit fullscreen mode

Validar en entidades

[Required]
public string Email { get; set; }
Enter fullscreen mode Exit fullscreen mode

Mezclar validación con lógica de negocio

if (request.Email.Contains("gmail"))
{
    // lógica de negocio
}
Enter fullscreen mode Exit fullscreen mode

✅ 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)