Cargando...

Uso de FluentValidation en C#

Aprende FluentValidation en C# para definir reglas de validación claras y mantenibles.

FluentValidation es una biblioteca popular en C# que permite definir reglas de validación mediante una API fluida. Permite crear reglas reutilizables y comprobables que son independientes de las clases del modelo. Este artículo ofrece una guía práctica: desde la instalación hasta las funciones avanzadas, incluyendo la integración con ASP.NET Core y las pruebas unitarias.


Instalación

Para el paquete básico y la integración opcional con ASP.NET Core:


dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore   # Para proyectos web

Para proyectos de escritorio (WPF/WinForms) o servicios, solo se necesita FluentValidation.


Uso básico: Un validador simple

Una clase de validador hereda de AbstractValidator<T>, y las reglas se definen con RuleFor.


using FluentValidation;

public class UserDto
{
    public string? Name { get; set; }
    public string? Email { get; set; }
    public int Age { get; set; }
}

public class UserDtoValidator : AbstractValidator<UserDto>
{
    public UserDtoValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("El nombre no puede estar vacío")
            .Length(2, 50).WithMessage("El nombre debe tener entre 2 y 50 caracteres");

        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("El correo electrónico es obligatorio")
            .EmailAddress().WithMessage("Ingrese un correo electrónico válido");

        RuleFor(x => x.Age)
            .InclusiveBetween(18, 120).WithMessage("La edad debe estar entre 18 y 120 años");
    }
}

Para validar: var result = new UserDtoValidator().Validate(model); Los errores se pueden leer a través de result.Errors.


Mensajes, Severidad y Modo en Cascada


RuleFor(x => x.Name)
    .Cascade(CascadeMode.Stop)
    .NotEmpty().WithMessage("El nombre es obligatorio").WithSeverity(Severity.Error)
    .MinimumLength(2).WithMessage("El nombre debe tener al menos 2 caracteres");

Reglas condicionales: When / Unless

Se usa para ejecutar una regla solo cuando se cumple una condición específica.


RuleFor(x => x.Email)
  .NotEmpty().EmailAddress();

RuleFor(x => x.Age)
  .GreaterThan(0);

RuleFor(x => x)
  .Must(x => x.Email!.EndsWith("@company.com"))
  .When(x => x.Age >= 18)
  .WithMessage("Los usuarios mayores de 18 años deben usar un correo electrónico corporativo");

Objetos relacionados y colecciones

Use SetValidator para validar modelos anidados y RuleForEach para elementos de colecciones.


public class AddressDto { public string? City { get; set; } public string? Zip { get; set; } }

public class AddressValidator : AbstractValidator<AddressDto>
{
    public AddressValidator()
    {
        RuleFor(x => x.City).NotEmpty();
        RuleFor(x => x.Zip).NotEmpty().Length(5, 10);
    }
}

public class CustomerDto
{
    public string? Name { get; set; }
    public AddressDto? Address { get; set; }
    public List<string> Phones { get; set; } = new();
}

public class CustomerValidator : AbstractValidator<CustomerDto>
{
    public CustomerValidator()
    {
        RuleFor(x => x.Name).NotEmpty();

        RuleFor(x => x.Address)
            .NotNull().WithMessage("La dirección es obligatoria")
            .SetValidator(new AddressValidator());

        RuleForEach(x => x.Phones)
            .NotEmpty().Matches(@"^\+?\d{7,15}$")
            .WithMessage("Número de teléfono no válido");
    }
}

Reglas personalizadas y asíncronas

Use Must/MustAsync para validaciones complejas o dependientes de servicios externos.


RuleFor(x => x.Email)
  .MustAsync(async (email, ct) =>
  {
      // Ejemplo: verificación de unicidad (pseudo-repositorio)
      bool exists = await _userRepository.ExistsByEmailAsync(email, ct);
      return !exists;
  })
  .WithMessage("El correo electrónico ya está registrado");

Validación basada en escenarios con RuleSet

Puede agrupar reglas para diferentes escenarios (por ejemplo, Create, Update).


public class ProductDto { public string? Name { get; set; } public decimal Price { get; set; } }

public class ProductValidator : AbstractValidator<ProductDto>
{
    public ProductValidator()
    {
        RuleSet("Create", () =>
        {
            RuleFor(x => x.Name).NotEmpty();
            RuleFor(x => x.Price).GreaterThan(0);
        });

        RuleSet("Update", () =>
        {
            RuleFor(x => x.Price).GreaterThanOrEqualTo(0);
        });
    }
}

// Uso:
// validator.Validate(model, options: o => o.IncludeRuleSets("Create"));

Personalización y Localización de Mensajes de Error


RuleFor(x => x.Email)
  .WithName("Correo Electrónico")
  .NotEmpty().WithMessage("{PropertyName} no puede estar vacío")
  .EmailAddress().WithMessage("{PropertyName} no es válido");

Integración con ASP.NET Core

Registra los servicios para habilitar la validación automática del modelo e integración con ModelState.


// Program.cs (Minimal API / ASP.NET Core 7+)
using FluentValidation;
using FluentValidation.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddFluentValidationAutoValidation();  // Funciona automáticamente con el enlace de modelos MVC
builder.Services.AddValidatorsFromAssemblyContaining<UserDtoValidator>(); // Escanea todos los validadores

var app = builder.Build();
app.MapControllers();
app.Run();

En un controlador no es necesario invocar manualmente el validador; después del model binding, FluentValidation se ejecuta automáticamente y escribe los errores en ModelState.


Tópicos Avanzados: Dependencias, Inyección y Reglas Reutilizables


public class NameRules : AbstractValidator<UserDto>
{
    public NameRules()
        => RuleFor(x => x.Name).NotEmpty().MaximumLength(50);
}

public class UserDtoValidator2 : AbstractValidator<UserDto>
{
    public UserDtoValidator2()
    {
        Include(new NameRules());
        RuleFor(x => x.Email).EmailAddress();
    }
}

Testabilidad: FluentValidation.TestHelper

Usa extensiones auxiliares para probar rápidamente tus validadores.


dotnet add package FluentValidation.TestHelper

using FluentValidation.TestHelper;
using Xunit;

public class UserDtoValidatorTests
{
    [Fact]
    public void Nombre_No_Debe_Estar_Vacio()
    {
        var v = new UserDtoValidator();
        var result = v.TestValidate(new UserDto { Name = "" });
        result.ShouldHaveValidationErrorFor(x => x.Name);
    }

    [Fact]
    public void Email_Debe_Ser_Valido()
    {
        var v = new UserDtoValidator();
        var result = v.TestValidate(new UserDto { Email = "x" });
        result.ShouldHaveValidationErrorFor(x => x.Email);
    }
}

Ejemplo Real: Formulario de Registro + Dirección & Teléfonos

El siguiente ejemplo muestra la validación de un registro de usuario, una dirección anidada y una lista de teléfonos.


public class RegisterRequest
{
    public string? FullName { get; set; }
    public string? Email { get; set; }
    public AddressDto? Address { get; set; }
    public List<string> Phones { get; set; } = new();
}

public class RegisterValidator : AbstractValidator<RegisterRequest>
{
    public RegisterValidator(IEmailBlacklistService blacklist)
    {
        RuleFor(x => x.FullName)
            .NotEmpty().WithMessage("El nombre completo es obligatorio")
            .MaximumLength(80);

        RuleFor(x => x.Email)
            .NotEmpty().EmailAddress()
            .MustAsync(async (mail, ct) => !await blacklist.IsBlacklistedAsync(mail, ct))
            .WithMessage("El correo electrónico no está permitido");

        RuleFor(x => x.Address)
            .NotNull().SetValidator(new AddressValidator());

        RuleForEach(x => x.Phones)
            .NotEmpty()
            .Matches(@"^\+?\d{7,15}$").WithMessage("Formato de teléfono inválido");

        // Condicional: Si el país de la dirección es 'ES', el código postal debe tener 5 dígitos
        When(x => x.Address?.Country == "ES", () =>
        {
            RuleFor(x => x.Address!.Zip)
                .Matches(@"^\d{5}$").WithMessage("En España, el código postal debe tener 5 dígitos");
        });
    }
}

Rendimiento y Mejores Prácticas


TL;DR

  • FluentValidation separa las reglas del modelo, ofreciendo validaciones legibles y comprobables.
  • RuleFor, RuleForEach y SetValidator son los bloques fundamentales.
  • Usa When/Unless para reglas condicionales; RuleSet para validaciones por escenario.
  • Must/MustAsync es ideal para comprobaciones personalizadas o de servicios externos.
  • En ASP.NET Core, habilita la integración automática con AddFluentValidationAutoValidation().
  • Asegura tus validadores con pruebas unitarias utilizando TestHelper.

Artículos relacionados