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
- Use
WithMessage()para mensajes de error personalizados. - Use
WithSeverity(Severity.Error|Warning|Info)para especificar la prioridad. - Use
Cascade(CascadeMode.Stop)para detener la validación después del primer error.
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
- Usa
WithName("Nombre del Campo")para mostrar un nombre de campo más amigable. - Habilita la compatibilidad multilingüe con archivos de recursos (.resx).
- El LanguageManager puede personalizarse para mensajes globales.
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
- Inyecta servicios o repositorios en el constructor del validador para utilizarlos dentro de MustAsync.
- Comparte reglas repetidas usando
Include:
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
- Escribe validadores sin estado; inyecta solo las dependencias necesarias en el constructor.
- Reduce las llamadas MustAsync intensivas en I/O usando caché en entornos de alto tráfico.
- Usa
RuleSetpara validaciones basadas en escenarios en modelos grandes. - Mantén los mensajes de error cortos y amigables; personaliza los nombres de los campos con
WithName. - Además de la validación del lado del servidor, considera la validación del lado del cliente si es necesario.
TL;DR
- FluentValidation separa las reglas del modelo, ofreciendo validaciones legibles y comprobables.
RuleFor,RuleForEachySetValidatorson 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
Clases, Objetos, Propiedades y Métodos en C#
Aprende cómo las clases, objetos, propiedades y métodos en C# forman la base de la programación orientada a objetos.
Fundamentos de Inyección de Dependencias en C#
Aprende los fundamentos de Inyección de Dependencias en C#, gestionando dependencias y logrando bajo acoplamiento.