Middleware vs Filters vs Handlers en ASP.NET Core
Tres formas de interceptar una petición en ASP.NET Core que se confunden a menudo: qué ve cada una, en qué orden se ejecutan y cuál elegir en cada caso.
Hay que agregar algo que se ejecute en todas las peticiones: medir el tiempo de respuesta, validar un encabezado, registrar quién llamó. En ASP.NET Core hay al menos tres lugares donde ponerlo, y elegir el equivocado no falla: simplemente hace que el código se ejecute cuando no debería, o que no vea la información que necesita.
La diferencia entre middleware, filtros y handlers no está en qué pueden hacer, sino en qué ven y cuándo se ejecutan.
El recorrido de una petición
Petición
→ Middleware 1 (p. ej. manejo de excepciones)
→ Middleware 2 (autenticación)
→ Routing → Endpoint
→ Filtros (MVC o endpoint filters)
→ Acción / handler del endpoint
← Filtros
← Middleware 2
← Middleware 1
Respuesta
El middleware envuelve todo. Los filtros solo existen dentro de un endpoint que ya fue elegido.
Middleware: todo el tráfico, sin contexto del endpoint
El middleware ve cada petición que llega a la aplicación, incluidas las que terminan en archivos estáticos, health checks o rutas que no existen.
public class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var start = Stopwatch.GetTimestamp();
await next(context);
var elapsed = Stopwatch.GetElapsedTime(start);
logger.LogInformation("{Method} {Path} → {StatusCode} en {Elapsed} ms",
context.Request.Method, context.Request.Path, context.Response.StatusCode, elapsed.TotalMilliseconds);
}
}
app.UseMiddleware<RequestTimingMiddleware>();
Lo usamos para lo transversal a toda la aplicación: manejo global de errores, encabezados de seguridad, compresión, CORS, correlation IDs y métricas por petición.
Lo que no ve: el modelo ya enlazado, el resultado de la validación ni (antes del routing) qué endpoint se va a ejecutar. Si lo registras después de UseRouting, puedes leer el endpoint con context.GetEndpoint() y sus metadatos, pero sigue sin ver los argumentos de la acción.
El orden importa. El middleware se ejecuta en el orden en que se registra. Un manejador de excepciones registrado después de otro middleware no captura los errores de ese middleware.
Filtros: dentro del endpoint, con el modelo enlazado
Filtros de MVC (controladores)
Tienen acceso a los argumentos de la acción, al ModelState y al resultado:
public class RequireTenantFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.HttpContext.Request.Headers.ContainsKey("X-Tenant-Id"))
{
context.Result = new BadRequestObjectResult("Falta el encabezado X-Tenant-Id");
return; // corta la ejecución: la acción no se llama
}
await next();
}
}
Hay varios tipos, que se ejecutan en este orden: autorización, recursos, acción, excepción y resultado. Cada uno tiene su momento.
Endpoint filters (Minimal APIs)
Desde .NET 7, las Minimal APIs tienen su propio mecanismo, más simple:
app.MapPost("/api/orders", CreateOrder)
.AddEndpointFilter(async (context, next) =>
{
var request = context.GetArgument<CreateOrderRequest>(0);
if (request.Lines.Count == 0)
return Results.Problem("El pedido debe tener al menos una línea", statusCode: 400);
return await next(context);
});
Se pueden aplicar a un grupo de rutas con MapGroup(...).AddEndpointFilter(...).
Los usamos para lógica que depende del endpoint o de sus argumentos: validación de modelos, auditoría de una acción concreta o comprobaciones que solo aplican a cierto grupo de rutas.
"Handlers": tres cosas distintas con el mismo nombre
La palabra handler aparece en ASP.NET Core con significados diferentes, y conviene no mezclarlos:
| Handler | Qué hace |
|---|---|
IExceptionHandler (.NET 8+) |
Convierte excepciones no controladas en respuestas, dentro del middleware de excepciones |
AuthorizationHandler<T> |
Evalúa un requisito de una política de autorización |
DelegatingHandler |
Intercepta las peticiones salientes de un HttpClient |
El último se confunde con frecuencia con el middleware, pero trabaja en la dirección contraria: no ve las peticiones que llegan a tu API, sino las que tu API hace a otros servicios.
public class CorrelationIdHandler(IHttpContextAccessor accessor) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
// Propaga el ID de correlación a los servicios que llamamos
var id = accessor.HttpContext?.TraceIdentifier;
if (id is not null) request.Headers.TryAddWithoutValidation("X-Correlation-Id", id);
return base.SendAsync(request, ct);
}
}
builder.Services.AddTransient<CorrelationIdHandler>();
builder.Services.AddHttpClient<InventoryClient>().AddHttpMessageHandler<CorrelationIdHandler>();
Cómo elegir
| Necesitas... | Usa |
|---|---|
| Algo en todas las peticiones, incluidas las que no llegan a un endpoint | Middleware |
| Los argumentos o el modelo ya validado | Filtro |
| Aplicarlo solo a ciertos endpoints o grupos | Filtro (o middleware condicionado por metadatos) |
Convertir excepciones en respuestas ProblemDetails |
IExceptionHandler |
| Decidir si un usuario puede acceder a un recurso | Política + AuthorizationHandler |
| Modificar llamadas a otros servicios | DelegatingHandler |
El trade-off
El middleware es la opción más general y la más fácil de entender, pero cuesta un poco en cada petición y no tiene contexto del endpoint. Los filtros son precisos, pero quedan atados a MVC o a Minimal APIs y es fácil olvidar registrarlos en un endpoint nuevo. Un filtro global evita ese olvido, a cambio de ejecutarse donde quizás no hace falta.
Cuándo no usar ninguno
Si una regla solo aplica a un endpoint, a veces lo más claro es ponerla en el propio endpoint. Un filtro con una sola aplicación es una indirección más que el próximo desarrollador tendrá que buscar para entender qué pasa.
Conclusión
Middleware para lo que afecta a toda la aplicación; filtros para lo que depende del endpoint; handlers para tres problemas concretos que no son ninguno de los anteriores. Cuando el código está en el nivel correcto, nadie tiene que preguntarse por qué se ejecuta, o por qué no.
¿Necesitas un equipo .NET para tu proyecto?
Construimos y modernizamos backends en .NET y SQL Server: APIs, integraciones y sistemas empresariales a medida.
Ver desarrollo a medida