.NET Development
FluentValidation en .NET 10: Validación Limpia para Aplicaciones Modernas en C#
Toda aplicación llega a un punto donde la lógica de validación dispersa se convierte en un problema serio. Una verificación en el controlador, otra en un método del servicio, una tercera enterrada dentro del constructor de una entidad de dominio. Cuando las reglas de validación viven en todas partes, no pertenecen a ningún lugar concreto. Los cambios se vuelven arriesgados porque nadie puede responder con confianza dónde vive realmente toda la validación de una operación determinada. FluentValidation aborda este problema de forma directa. Es una biblioteca open-source para .NET que permite definir reglas de validación como clases dedicadas, separadas tanto de los modelos de dominio como de la capa API. Cada validador es una unidad autocontenida que puede probarse de forma independiente, componerse con otros validadores e inyectarse mediante inyección de dependencias como cualquier otro servicio. En este artículo aprenderás qué es FluentValidation, qué problemas resuelve que las Data Annotations y la validación manual no pueden, y cómo implementarlo desde cero en .NET 10 con C# 14. También verás cómo se integra con pipeline behaviors de MediatR en un contexto de Clean Architecture, convirtiendo la validación en una preocupación transversal que nunca se filtra en tu lógica de negocio.
Qué es FluentValidation y por qué existe
FluentValidation es una biblioteca de validación open-source para .NET creada por Jeremy Skinner. Proporciona una API fluida construida alrededor de la clase base AbstractValidator<T>, donde defines reglas de validación para un tipo específico usando cadenas de métodos fuertemente tipadas. En lugar de decorar propiedades con atributos, escribes expresiones de reglas explícitas que se leen casi como lenguaje natural: RuleFor(x => x.Email).NotEmpty().EmailAddress().
La biblioteca ha acumulado más de 350 millones de descargas en NuGet, convirtiéndola en una de las soluciones de validación más ampliamente adoptadas en el ecosistema .NET. Su popularidad no es accidental. FluentValidation trata la validación como lo que realmente es: lógica de negocio que merece su propia capa, sus propias pruebas y su propio ciclo de vida evolutivo, independiente de los modelos que valida.
Considera un sistema de catálogo de productos donde el mismo CreateProductCommand necesita reglas de validación diferentes según la categoría del producto. Con Data Annotations, necesitarías atributos condicionales o atributos de validación personalizados que rápidamente se vuelven inmanejables. Con FluentValidation, escribes una clase validadora que usa condiciones When y Unless naturalmente dentro de la cadena fluida, manteniendo cada regla visible en una única ubicación.
Perspectiva técnica: FluentValidation evita intencionalmente el acoplamiento con cualquier framework específico. Aunque proporciona paquetes de integración para ASP.NET Core, la biblioteca central tiene cero dependencias más allá de .NET. Esta decisión de diseño significa que tus validadores funcionan de forma idéntica en controladores de API, workers en segundo plano, aplicaciones de consola y pruebas de integración sin necesidad de modificaciones.
Problemas que resuelve FluentValidation
Separación de responsabilidades
Las Data Annotations mezclan metadatos de validación con definiciones de modelos. Un atributo [Required] o [MaxLength(100)] en una propiedad le dice a la clase del modelo sobre preocupaciones de infraestructura que no debería conocer. En Clean Architecture, tus entidades de dominio y objetos de comando deben expresar qué son, no cómo deben validarse. FluentValidation mueve toda la lógica de validación a clases validadoras dedicadas que viven en la capa Application, dejando los modelos limpios y enfocados en su responsabilidad principal: transportar datos.
Composición de reglas complejas
La validación del mundo real raramente se reduce a "requerido" y "longitud máxima". Necesitas reglas que dependan de otras propiedades: un porcentaje de descuento que debe ser cero cuando un producto no está en oferta. Necesitas reglas que llamen a servicios externos de forma asíncrona: verificar que un email no esté ya registrado. Necesitas reglas que se apliquen condicionalmente según el contexto de la operación: un comando de actualización que permite nulo para campos sin cambios mientras que un comando de creación requiere todo. FluentValidation maneja todos estos escenarios a través de When, Unless, Must, MustAsync y validadores Custom, sin obligarte a hacer acrobacias con atributos.
El resultado neto es que las reglas de validación son legibles, testeables y mantenibles. Cuando una regla de negocio cambia, modificas exactamente una clase validadora. Cuando aparece un bug en la validación, escribes una prueba unitaria enfocada para esa regla específica.
Instalación y configuración en .NET 10
Poner en marcha FluentValidation en un proyecto .NET 10 requiere dos paquetes NuGet. La biblioteca central proporciona las clases base de validadores y las definiciones de reglas. El paquete de extensiones de inyección de dependencias agrega escaneo de ensamblados para registrar todos los validadores automáticamente.
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
El registro en tu Program.cs usa una sola línea que escanea el ensamblado en busca de todas las clases que heredan de AbstractValidator<T>:
var builder = WebApplication.CreateBuilder(args);
// Registrar todos los validadores del ensamblado que contiene CreateProductValidator
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductValidator>();
var app = builder.Build();
app.Run();
Esto registra cada validador en el mismo ensamblado que CreateProductValidator con un tiempo de vida scoped por defecto. En Clean Architecture, apuntarías esto al ensamblado de tu capa Application, asegurando que todos los validadores sean descubiertos sin importar cuántos agregues. Sin registro manual por validador, sin olvidar conectar uno nuevo.
Escenario práctico: Un equipo con 45 validadores distribuidos en diferentes módulos de funcionalidades añadió un nuevo endpoint con su validador. Porque el escaneo de ensamblados maneja el registro, el nuevo validador funcionó inmediatamente después de crearlo sin tocar Program.cs. Compara esto con el registro manual de DI donde omitir una sola línea causa un bypass silencioso de validación en producción.
Conceptos fundamentales: tu primer validador
AbstractValidator y RuleFor
Cada validador de FluentValidation hereda de AbstractValidator<T>, donde T es el tipo que se está validando. Las reglas se definen en el constructor usando el método RuleFor, que toma una expresión lambda que apunta a la propiedad que deseas validar. Encadenas validadores incorporados después de RuleFor para expresar tus restricciones.
Validadores incorporados
FluentValidation viene con un conjunto completo de validadores incorporados que cubren escenarios comunes:
- NotEmpty / NotNull — asegura que el valor está presente y no es el valor por defecto
- MaximumLength / MinimumLength — restringe la longitud de cadenas
- EmailAddress — valida formato de email
- GreaterThan / LessThan / InclusiveBetween — validación de rangos numéricos
- Matches — validación con patrones regex
- Must — predicado personalizado para reglas de negocio
- SetValidator — delega a un validador hijo para objetos anidados
- ForEach — valida cada elemento en una colección
Cada validador soporta .WithMessage() para mensajes de error personalizados y .WithErrorCode() para identificadores de error legibles por máquina que las aplicaciones front-end pueden usar para localización.
public class CreateProductValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("El nombre del producto es obligatorio.")
.MaximumLength(200).WithMessage("El nombre no puede superar 200 caracteres.");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("El precio debe ser mayor que cero.");
RuleFor(x => x.Sku)
.NotEmpty()
.Matches(@"^[A-Z]{2,4}-\d{4,8}$")
.WithMessage("El SKU debe seguir el formato XX-0000 (2-4 letras, guion, 4-8 dígitos).");
RuleFor(x => x.CategoryId)
.NotEmpty().WithMessage("Todo producto debe pertenecer a una categoría.");
RuleFor(x => x.Description)
.MaximumLength(2000)
.When(x => !string.IsNullOrEmpty(x.Description));
}
}
Escenario práctico: Este validador maneja un flujo real de creación de productos. La regla del SKU usa una expresión regular para imponer un formato específico de la empresa. La validación de la descripción solo se aplica cuando realmente se proporciona una descripción, evitando falsos positivos en campos opcionales. Observa cómo cada regla se lee como un requisito de negocio claro, no como un atributo críptico.
Reglas de validación avanzadas
Validadores personalizados con Must y Custom
El validador Must acepta cualquier predicado que retorne un booleano. Para escenarios más complejos donde necesitas agregar múltiples mensajes de error o inspeccionar el contexto de validación, Custom te da control total sobre el proceso de validación. Estos dos validadores cubren el 90% de los casos donde los validadores incorporados resultan insuficientes.
Reglas condicionales: When y Unless
La validación de negocio es inherentemente condicional. Una dirección de envío es obligatoria solo cuando el método de entrega no es digital. Un número de identificación fiscal es obligatorio solo para cuentas empresariales. When aplica una regla solo si una condición es verdadera. Unless aplica una regla solo si una condición es falsa. Ambos soportan ApplyConditionTo.AllValidators para limitar el alcance de la condición a múltiples reglas encadenadas a la vez.
Validación asíncrona
Algunas reglas requieren consultas a base de datos o llamadas a servicios externos. MustAsync acepta un predicado asíncrono, y ValidateAsync ejecuta todas las reglas incluyendo las asíncronas. Esto es crítico para reglas como verificar que un email no esté ya registrado en la base de datos.
public class UpdateOrderValidator : AbstractValidator<UpdateOrderCommand>
{
public UpdateOrderValidator(IOrderRepository orderRepository)
{
RuleFor(x => x.OrderId)
.NotEmpty()
.MustAsync(async (id, cancellation) =>
await orderRepository.ExistsAsync(id, cancellation))
.WithMessage("La orden especificada no existe.");
RuleFor(x => x.Discount)
.InclusiveBetween(0, 50)
.When(x => x.ApplyDiscount)
.WithMessage("El descuento debe estar entre 0% y 50%.");
RuleFor(x => x.ShippingAddress)
.NotEmpty()
.Unless(x => x.DeliveryMethod == DeliveryMethod.Digital)
.WithMessage("La dirección de envío es obligatoria para entregas físicas.");
RuleFor(x => x.Notes)
.Must(notes => !notes.Contains("<script>"))
.When(x => !string.IsNullOrEmpty(x.Notes))
.WithMessage("Las notas contienen contenido no permitido.");
}
}
Perspectiva técnica: Cuando uses MustAsync, llama siempre a ValidateAsync en lugar de Validate. Llamar al método síncrono Validate en un validador que contiene reglas asíncronas lanza una excepción AsyncValidatorInvokedSynchronouslyException. Esta es una decisión de diseño deliberada de FluentValidation para prevenir deadlocks y asegurar que los validadores asíncronos se ejecuten en el contexto de sincronización correcto. En pipeline behaviors de MediatR, esto ocurre naturalmente ya que el behavior en sí es asíncrono.
Integración con ASP.NET Core en .NET 10
Validación automática con Endpoint Filters (Minimal APIs)
Las Minimal APIs de .NET 10 soportan endpoint filters, que proporcionan un punto de intercepción limpio para validación antes de que el handler del endpoint se ejecute. En lugar de llamar manualmente a los validadores dentro de cada endpoint, creas un filtro reutilizable que resuelve el validador apropiado desde DI y valida la solicitud automáticamente.
public class ValidationEndpointFilter<TRequest>(
IValidator<TRequest> validator) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
if (context.Arguments.FirstOrDefault(a => a is TRequest) is not TRequest request)
return await next(context);
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
{
var errors = result.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
return Results.ValidationProblem(errors);
}
return await next(context);
}
}
Validación manual en Controllers
Para APIs basadas en controladores, inyecta IValidator<T> directamente en el controlador o método de acción. Llama a ValidateAsync y retorna una respuesta ValidationProblem cuando el resultado es inválido. El patrón es directo pero repetitivo, razón por la cual el enfoque de pipeline mostrado más adelante es preferible para aplicaciones más grandes.
Aplicar el filtro a un endpoint de Minimal API es una sola llamada de método:
app.MapPost("/api/products", async (CreateProductCommand command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return Results.Created($"/api/products/{result.Id}", result);
})
.AddEndpointFilter<ValidationEndpointFilter<CreateProductCommand>>()
.WithName("CreateProduct")
.WithOpenApi();
Escenario práctico: Un equipo migrando de controladores a Minimal APIs reemplazó su action filter de validación con este patrón de endpoint filter. La migración tomó menos de un día porque los validadores en sí no cambiaron en absoluto. Solo la capa de integración cambió, demostrando el valor de separar la lógica de validación de la plomería específica del framework.
FluentValidation en Clean Architecture con CQRS
Dónde viven los validadores en la arquitectura
En Clean Architecture, los validadores pertenecen a la capa Application. Validan commands y queries, que son preocupaciones de la capa Application. Los validadores nunca deberían vivir en la capa Domain (las entidades de dominio imponen sus propios invariantes a través de constructores y métodos) ni en la capa API (que debería permanecer delgada). Coloca los validadores junto a su command o query correspondiente, típicamente en la misma carpeta de funcionalidad.
Pipeline Behavior de MediatR para validación
El patrón de integración más poderoso es un pipeline behavior de MediatR que intercepta cada request, ejecuta todos los validadores registrados y cortocircuita con un error de validación antes de que el handler se ejecute. Esta es la mejor inversión que puedes hacer en una arquitectura CQRS porque garantiza que ningún command o query llegue a un handler sin pasar validación primero.
public sealed class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!validators.Any())
return await next(cancellationToken);
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var errors = validationResults
.SelectMany(r => r.Errors)
.Where(e => e is not null)
.ToList();
if (errors.Count is not 0)
throw new ValidationException(errors);
return await next(cancellationToken);
}
}
Este behavior usa primary constructors de C# 14 y ejecuta todos los validadores en paralelo usando Task.WhenAll. Cuando cualquier validación falla, lanza una ValidationException que tu middleware de manejo de excepciones convierte en una respuesta de error estandarizada.
Registra todo en Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Registrar MediatR con pipeline behaviors
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<CreateProductHandler>();
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
// Registrar todos los validadores de FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductValidator>();
var app = builder.Build();
app.Run();
Perspectiva técnica: El orden de los pipeline behaviors importa. Registra ValidationBehavior antes de los behaviors de logging o rendimiento para que las solicitudes inválidas se rechacen antes de consumir recursos. Si tienes tanto un LoggingBehavior como un ValidationBehavior, registra validación primero. MediatR ejecuta los behaviors en el orden en que se registran, creando un patrón de anidamiento tipo muñeca rusa donde el primer behavior registrado es el envoltorio más externo.
Probando tus validadores
FluentValidation proporciona una API de testing dedicada a través del método de extensión TestValidate. Este método retorna un TestValidationResult que expone ShouldHaveValidationErrorFor y ShouldNotHaveValidationErrorFor para aserciones precisas.
Mejores prácticas para testing de validadores:
- Prueba una regla por método de test — aísla cada regla para identificar fallos rápidamente.
- Prueba escenarios válidos e inválidos — verifica que las reglas rechacen datos incorrectos y acepten datos correctos.
- Prueba valores límite — para rangos numéricos y longitudes de cadena, prueba el límite exacto.
- Prueba reglas condicionales explícitamente — crea casos de prueba donde la condición es verdadera y falsa.
- Simula dependencias asíncronas — usa Moq o NSubstitute para validadores que llaman a repositorios.
public class CreateProductValidatorTests
{
private readonly CreateProductValidator _validator = new();
[Fact]
public void Deberia_tener_error_cuando_Name_esta_vacio()
{
var command = new CreateProductCommand { Name = "", Price = 10, Sku = "AB-1234" };
var result = _validator.TestValidate(command);
result.ShouldHaveValidationErrorFor(x => x.Name);
}
[Fact]
public void No_deberia_tener_error_cuando_command_es_valido()
{
var command = new CreateProductCommand
{
Name = "Widget Pro",
Price = 29.99m,
Sku = "WP-12345",
CategoryId = Guid.NewGuid()
};
var result = _validator.TestValidate(command);
result.ShouldNotHaveAnyValidationErrors();
}
[Theory]
[InlineData("ab-1234")]
[InlineData("ABCDE-123")]
[InlineData("AB1234")]
public void Deberia_tener_error_cuando_Sku_formato_invalido(string sku)
{
var command = new CreateProductCommand { Name = "Test", Price = 10, Sku = sku };
var result = _validator.TestValidate(command);
result.ShouldHaveValidationErrorFor(x => x.Sku);
}
}
Escenario práctico: Un equipo de desarrollo descubrió un bug en producción donde se estaban creando productos con precio cero. Rastrearon el problema hasta una regla GreaterThan(0) faltante. Después de agregar la regla, escribieron un test parametrizado cubriendo cero, negativos y precios válidos. La corrección y el test tomaron doce minutos. Encontrar el mismo bug a través de pruebas de integración o reportes de clientes habría costado significativamente más.
FluentValidation vs Data Annotations vs validación manual
Elegir una estrategia de validación afecta tu arquitectura durante años. Esta comparación cubre las dimensiones que más importan para aplicaciones mantenibles:
| Criterio | FluentValidation | Data Annotations | Validación manual |
|---|---|---|---|
| Separación de responsabilidades | Separación completa en clases dedicadas | Mezclada en definiciones del modelo | Dispersa en servicios |
| Reglas condicionales complejas | When/Unless con soporte completo de lambdas | Limitado, requiere atributos personalizados | Posible pero verboso |
| Validación asíncrona | MustAsync y ValidateAsync integrados | No soportado nativamente | Implementación manual async |
| Testeabilidad | API TestValidate, completamente unitaria | Requiere simulación de ModelState | Testeable pero sin API dedicada |
| Integración CQRS | Nativa via pipeline behaviors de MediatR | Sin concepto de pipeline | Middleware personalizado requerido |
| Validación entre propiedades | RuleFor con acceso lambda al objeto completo | IValidatableObject, limitado | Acceso completo, sin estructura |
| Reutilización | Include() para componer validadores | Atributos compartidos | Métodos utilitarios |
| Personalización de mensajes | WithMessage, WithErrorCode, localización | Propiedad ErrorMessage | Completamente personalizable |
| Validación de colecciones | RuleForEach con validadores hijos | Limitado | Iteración manual |
| Acoplamiento al framework | Ninguno (biblioteca central) | Atado a System.ComponentModel | Ninguno |
| Curva de aprendizaje | Moderada, API fluida es intuitiva | Baja, basada en atributos | Baja pero escala mal |
Para aplicaciones con menos de diez reglas de validación y sin lógica de negocio compleja, Data Annotations son suficientes. Para todo lo demás, FluentValidation proporciona una experiencia de desarrollo y un resultado arquitectónico significativamente mejores.
Errores comunes que cometen los desarrolladores
Después de revisar cientos de implementaciones de FluentValidation en codebases de producción, estos son los problemas más frecuentes:
Colocar validadores en la capa Domain. Las entidades de dominio imponen invariantes a través de sus constructores y métodos. Los validadores validan entrada externa (commands, queries, DTOs) y pertenecen a la capa Application. Mezclar estas responsabilidades viola el Principio de Responsabilidad Única.
Llamar a Validate en vez de ValidateAsync. Si cualquier regla en tu validador usa MustAsync, WhenAsync, o cualquier otro método asíncrono, debes llamar a ValidateAsync. El método síncrono Validate lanzará una excepción en tiempo de ejecución, y este bug solo aparece cuando se ejercita la ruta de la regla asíncrona.
Crear validadores monolíticos. Un validador con más de 50 reglas es un code smell. Usa Include(new AddressValidator()) para componer validadores más pequeños y enfocados. Esto también habilita la reutilización cuando múltiples commands comparten propiedades validadas comunes.
Olvidar el registro en DI. Agregar una nueva clase validadora pero olvidar que tu configuración de DI usa registro manual en lugar de escaneo de ensamblados. El validador simplemente nunca se ejecuta y datos inválidos entran al sistema silenciosamente.
Ignorar CascadeMode. Por defecto, FluentValidation continúa evaluando todas las reglas incluso después del primer fallo. Para validaciones costosas, establece RuleLevelCascadeMode = CascadeMode.Stop en el constructor del validador para cortocircuitar después del primer fallo por propiedad.
Mezclar FluentValidation con Data Annotations en el mismo modelo. Esto crea dos sistemas de validación ejecutándose independientemente, frecuentemente con reglas contradictorias. Elige un enfoque por bounded context y aplícalo consistentemente.
Escenario práctico: Un equipo desplegó un endpoint de registro donde el validador usaba MustAsync para verificar unicidad de email. Durante el desarrollo, solo probaron con emails válidos, así que la ruta asíncrona nunca se ejercitó. En producción, el primer registro con email duplicado provocó que el endpoint fallara con una excepción AsyncValidatorInvokedSynchronouslyException porque el controlador llamaba a Validate en lugar de ValidateAsync. La corrección fue un cambio de una palabra; la caída duró 45 minutos.
Rendimiento y escalabilidad
FluentValidation agrega una sobrecarga insignificante para reglas de validación síncronas. La biblioteca usa árboles de expresiones compilados para el acceso a propiedades, haciendo la resolución de propiedades casi tan rápida como el acceso directo. Para la gran mayoría de aplicaciones, el tiempo de validación se mide en microsegundos y es invisible comparado con consultas a base de datos o llamadas de red.
Para escenarios de alto rendimiento, considera estas optimizaciones:
CascadeMode.Stop previene la ejecución de reglas subsiguientes después del primer fallo en una propiedad. Si tu primera regla verifica NotEmpty y la segunda llama a un servicio externo, detenerse en el primer fallo evita llamadas de red innecesarias.
El tiempo de vida del validador importa. Registra validadores como singletons cuando no tienen dependencias inyectadas. Los tiempos de vida scoped o transient crean presión de recolección de basura bajo volúmenes altos de solicitudes. Cuando los validadores requieren servicios scoped (como un DbContext), mantenlos scoped pero sé consciente del costo de asignación.
RuleForEach con colecciones grandes puede volverse costoso. Si estás validando una colección con miles de elementos, considera validar solo un subconjunto o mover la validación masiva a un proceso en segundo plano. Un validador que toma 200ms por elemento en una colección de 5,000 elementos bloqueará la solicitud por más de 16 minutos.
Preguntas frecuentes
¿Se puede usar FluentValidation y Data Annotations juntos?
Técnicamente sí, pero no es recomendable. Ejecutar dos sistemas de validación simultáneamente crea confusión sobre cuál sistema captura cuál error. Si estás migrando desde Data Annotations, convierte validadores uno a la vez y elimina las anotaciones correspondientes conforme avanzas. Una migración gradual es preferible a mantener dos sistemas paralelos.
¿Funciona FluentValidation con Minimal APIs en .NET 10?
Sí. FluentValidation no tiene dependencia de MVC ni de controladores. Lo integras a través de endpoint filters como se muestra en este artículo, o a través de pipeline behaviors de MediatR. La lógica central de validación es agnóstica al framework, así que funciona de forma idéntica ya sea que tu solicitud provenga de un endpoint Minimal API, un controlador, un servicio gRPC o un worker en segundo plano.
¿Cómo valido objetos anidados y colecciones?
Usa SetValidator para objetos anidados y RuleForEach combinado con SetValidator para colecciones. Por ejemplo: RuleFor(x => x.Address).SetValidator(new AddressValidator()) delega a un validador separado. Para colecciones: RuleForEach(x => x.LineItems).SetValidator(new LineItemValidator()) valida cada elemento independientemente.
¿Cuál es el impacto en rendimiento comparado con Data Annotations?
Para reglas síncronas, la diferencia es despreciable: microsegundos por llamada de validación. FluentValidation usa árboles de expresiones compilados para el acceso a propiedades. La sobrecarga solo se vuelve notable con validadores asíncronos que realizan operaciones de I/O, y ese costo proviene del I/O en sí, no de FluentValidation.
¿Excepciones o resultados de error para la validación?
Ambos enfoques son válidos pero sirven contextos diferentes. En pipeline behaviors de MediatR, lanzar una ValidationException es el patrón estándar porque el behavior no tiene control sobre el tipo de retorno. En uso directo, retornar un ValidationResult y verificar IsValid evita el costo de rendimiento del manejo de excepciones. Usa excepciones para validación transversal en pipelines y resultados para validación dentro del handler.
Comienza a validar de la forma correcta
FluentValidation transforma la validación de una obligación dispersa en una parte estructurada, testeable y mantenible de tu arquitectura. Separa la lógica de validación de los modelos, soporta reglas complejas condicionales y asíncronas, se integra naturalmente con pipelines CQRS de MediatR, y produce validadores tan fáciles de testear como cualquier otra unidad en tu codebase.
El camino hacia la adopción no requiere una reescritura completa. Escoge un endpoint, idealmente uno con lógica de validación compleja dispersa entre controladores y servicios. Extrae esas reglas en una sola clase AbstractValidator<T>. Regístrala con inyección de dependencias. Escribe tres tests: uno para entrada válida, uno para el caso inválido más crítico, y uno para una condición límite. Ejecuta esos tests. Siente la diferencia entre testear un validador de forma aislada versus simular solicitudes HTTP a través de un controlador. Esa única migración demostrará el valor de forma más efectiva que cualquier artículo.
Compartir este artículo
Suscríbete
Recibe los últimos artículos directamente en tu bandeja de entrada.
Deja un comentario