Fetching latest headlines…
FluentValidation en .NET 10 sin ensuciar tus entidades (Clean Architecture + MediatR)
NORTH AMERICA
🇺🇸 United StatesApril 19, 2026

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

0 views0 likes0 comments
Originally published byDev.to

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

Comments (0)

Sign in to join the discussion

Be the first to comment!