Start Debugging

Validación de minimal API vs FluentValidation en ASP.NET Core 11: ¿cuál deberías elegir?

Usa la validación integrada generada por código fuente para reglas síncronas expresables con atributos en ASP.NET Core 11; recurre a FluentValidation cuando necesites reglas asíncronas, lógica compleja entre campos o mantener la validación fuera de tus modelos de dominio.

Si vas a empezar una minimal API nueva en ASP.NET Core 11, usa la validación integrada: llama a AddValidation(), anota tu record de solicitud con DataAnnotations y un generador de código fuente devuelve un 400 ProblemDetails antes de que se ejecute tu handler, sin dependencias y con soporte para Native AOT. Recurre a FluentValidation (actualmente 12.1.1, Apache 2.0) solo cuando necesites algo que la funcionalidad integrada realmente no puede hacer: reglas asíncronas que consultan una base de datos, lógica rica entre campos o validación mantenida por completo fuera de tus modelos de dominio. El eje decisivo son las reglas asíncronas y condicionales complejas. Si las necesitas, gana FluentValidation; si no, la funcionalidad integrada es la opción de menor fricción. Todo lo que sigue apunta a .NET 11 con Microsoft.NET.Sdk.Web y C# 14; la funcionalidad integrada se publicó por primera vez en .NET 10 y no cambia en 11.

La matriz de características

Esta es la tabla por la que viniste. Cada fila refleja .NET 11 y FluentValidation 12.1.1.

CaracterísticaIntegrada (DataAnnotations)FluentValidation 12
Dependencianinguna (incluida, .NET 10+)paquete NuGet, Apache 2.0
Mecanismogenerador de código fuente en compilaciónreflexión en runtime + árboles de expresión
Native AOT / trimmingcompatible (sin reflexión en runtime)requiere cuidado, soporte parcial
Reglas asíncronas (consultas BD / HTTP)nosí (MustAsync, ValidateAsync)
Reglas entre camposIValidatableObject (verboso)de primera clase (RuleFor(...).When(...))
Dónde viven las reglasen el modelo, como atributosen una clase de validador aparte
Condicional / conjuntos de reglaslimitadoWhen/Unless/WhenAsync, conjuntos de reglas
400 automático en una minimal APIsí, endpoint filter integradomanual: tú escribes el endpoint filter
Forma de la respuesta de errorProblemDetails (RFC 9457) gratistú mapeas los fallos a una respuesta
Reglas compuestas reutilizablesValidationAttribute personalizadoSetValidator, encadenamiento de reglas, herencia

La lectura corta: la funcionalidad integrada optimiza para “actívala y olvídate” en modelos simples, y FluentValidation optimiza para el poder expresivo en los complejos. Ninguna es estrictamente mejor. Las filas que deciden la mayoría de los casos son las reglas asíncronas y dónde viven las reglas.

Cuándo elegir la validación integrada

La validación integrada, explicada paso a paso en cómo validar cuerpos de solicitud en minimal APIs sin controladores, es el valor por defecto correcto en estos casos:

Cuándo elegir FluentValidation

FluentValidation 12 se gana su dependencia cuando la funcionalidad integrada choca con un muro:

Cómo se ve cada una en código

La versión integrada son las anotaciones que ya conoces, más un registro de servicio:

// .NET 11, C# 14 -- Program.cs
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation();
var app = builder.Build();

app.MapPost("/products", (CreateProduct product) =>
    TypedResults.Created($"/products/{product.Sku}", product));

app.Run();

public record CreateProduct(
    [Required, Length(3, 20)] string Sku,
    [Required, MinLength(2)] string Name,
    [Range(1, 10_000)] int Quantity);

Un cuerpo inválido nunca llega al handler. Vuelve como un 400 con un payload ProblemDetails indexado por Sku, Name y Quantity automáticamente.

El equivalente de FluentValidation mueve las reglas a una clase de validador y, como la pipeline de validación automática integrada está obsoleta (más sobre esto abajo), lo conecta a través de un endpoint filter que llamas explícitamente:

// .NET 11, C# 14 -- FluentValidation 12.1.1
using FluentValidation;
using FluentValidation.Results;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IValidator<CreateProduct>, CreateProductValidator>();
var app = builder.Build();

app.MapPost("/products", (CreateProduct product) =>
        TypedResults.Created($"/products/{product.Sku}", product))
   .AddEndpointFilter(async (ctx, next) =>
   {
       var product = ctx.GetArgument<CreateProduct>(0);
       var validator = ctx.HttpContext.RequestServices
           .GetRequiredService<IValidator<CreateProduct>>();

       ValidationResult result = await validator.ValidateAsync(product);
       if (!result.IsValid)
           return Results.ValidationProblem(result.ToDictionary());

       return await next(ctx);
   });

public record CreateProduct(string Sku, string Name, int Quantity);

public sealed class CreateProductValidator : AbstractValidator<CreateProduct>
{
    public CreateProductValidator()
    {
        RuleFor(x => x.Sku).NotEmpty().Length(3, 20);
        RuleFor(x => x.Name).NotEmpty().MinimumLength(2);
        RuleFor(x => x.Quantity).InclusiveBetween(1, 10_000);
    }
}

Es más código, pero también es donde vive el poder. Agrega una comprobación de unicidad asíncrona y la funcionalidad integrada simplemente no puede seguir:

// .NET 11, C# 14 -- FluentValidation 12.1.1
public sealed class CreateProductValidator : AbstractValidator<CreateProduct>
{
    public CreateProductValidator(IProductRepository repo)
    {
        RuleFor(x => x.Sku)
            .NotEmpty()
            .Length(3, 20)
            .MustAsync(async (sku, ct) => !await repo.SkuExistsAsync(sku, ct))
            .WithMessage("A product with this SKU already exists.");
    }
}

MustAsync consulta la base de datos durante la validación. No hay ningún atributo de DataAnnotations que haga esto sin que abras un DbContext dentro de la regla, e IValidatableObject.Validate es síncrono, así que no puede esperar nada. Este es el muro que envía a los equipos a FluentValidation.

Puedes factorizar el código repetitivo del endpoint filter en una pequeña extensión genérica para que cada endpoint solo encadene .WithValidation<CreateProduct>(). La mecánica de componer filtros en un grupo de rutas es la misma que se describe en organizar endpoints de minimal API con MapGroup, así que un único MapGroup(...).AddEndpointFilter(...) puede validar un grupo entero.

El benchmark: arranque, no estado estacionario

La historia honesta de rendimiento aquí no es el throughput de solicitudes. Para un puñado de propiedades, ambos enfoques validan en microsegundos y el costo queda eclipsado por la deserialización JSON y lo que haga tu handler. La diferencia medible está en los bordes: el arranque en frío y AOT.

Como la funcionalidad integrada se genera en tiempo de compilación, no agrega reflexión en el arranque. FluentValidation construye sus cadenas de reglas con árboles de expresión que se compilan en el primer uso, así que la primera validación de cada tipo paga un costo único de JIT y compilación de expresión. En un servidor caliente ese costo se amortiza a nada. En una función serverless con arranque en frío que atiende una solicitud y se congela, lo pagas en una fracción significativa de invocaciones.

La distinción más marcada es la publicación. Una minimal API con Native AOT y el validador integrado se publica y se ejecuta sin advertencias de trimming, porque no hay reflexión que preservar. La misma aplicación usando FluentValidation hará surgir advertencias de trimming a menos que mantengas anclados los tipos validados y sus miembros, ya que la biblioteca usa reflexión sobre tu modelo y sobre expresiones de acceso a miembros. Eso no es “FluentValidation es lento”, es “FluentValidation te cuesta trabajo de seguridad de trimming que la funcionalidad integrada no”. Si tu objetivo de despliegue es AOT, trátalo como el factor decisivo en lugar de cualquier conteo de nanosegundos. Las cifras más viejas que este ciclo de .NET 11 deberían volver a medirse antes de confiar en ellas; las internas de validación cambiaron entre .NET 8 y .NET 10.

El detalle que decide por ti: el paquete AspNetCore obsoleto

Si buscas la integración de FluentValidation con ASP.NET Core, encontrarás el paquete FluentValidation.AspNetCore y su antigua pipeline de validación automática. No empieces ahí. Los mantenedores la marcaron como obsoleta, y la documentación es explícita: “We no longer recommend using this approach for new projects but it is still available for legacy implementations.” No ejecuta validadores asíncronos (lanza excepción), nunca soportó minimal APIs ni Blazor, y su comportamiento implícito es difícil de depurar. El camino recomendado en 2026 es el manual mostrado arriba: registra IValidator<T> en DI y llama tú mismo a ValidateAsync, desde un endpoint filter para minimal APIs o desde el handler. Si un tutorial te dice que llames a AddFluentValidationAutoValidation(), es anterior a esta recomendación.

Un segundo detalle apunta en sentido contrario, a favor de FluentValidation: el miedo a la licencia está mal ubicado aquí. FluentValidation sigue siendo Apache 2.0 y gratuita, incluido para uso comercial. La biblioteca que cambió a una licencia restrictiva de pago en enero de 2025 fue Fluent Assertions, un proyecto distinto con un nombre confusamente similar. No tienen relación, y elegir FluentValidation no te expone a una tarifa por puesto. Si un revisor de políticas marca “Fluent*” en tu lista de dependencias, esa es la distinción que hay que hacer.

El último es una trampa de corrección exclusiva de FluentValidation: si alguna regla de un validador es asíncrona, cada sitio de llamada debe usar ValidateAsync. Un Validate() síncrono perdido en un validador que contiene una regla MustAsync lanza una excepción en runtime, no en compilación, así que puede pasar pruebas que nunca ejercitan esa ruta y fallar en producción. Estandariza ValidateAsync en todas partes para evitar la trampa por completo.

La recomendación, repetida

Usa por defecto la validación integrada en ASP.NET Core 11. Es gratis, es compatible con AOT, te entrega un contrato ProblemDetails estándar y, para las reglas síncronas y con forma de atributo que constituyen la mayoría de la validación de solicitudes, es estrictamente menos trabajo que agregar una dependencia. Elige FluentValidation de forma deliberada, cuando hayas chocado con un límite específico: reglas asíncronas que necesitan E/S, lógica condicional entre campos que IValidatableObject vuelve fea, o una regla arquitectónica de que la validación no debe tocar tus modelos de dominio. Muchos sistemas reales terminan con ambas, y eso está bien: deja que la funcionalidad integrada proteja los DTO simples y que FluentValidation se encargue del puñado de tipos de solicitud con reglas genuinamente complejas. El error es recurrir a una biblioteca de validación en un servicio nuevo de .NET 11 por costumbre, antes de tener una regla que el framework no pueda ya expresar gratis.

Relacionados

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver