En esta página
Middleware y filtros en ASP.NET Core
El pipeline de middleware
En ASP.NET Core, cada solicitud HTTP pasa por una cadena de middleware. Cada middleware decide si procesa la solicitud y si la pasa al siguiente:
Request ──→ Middleware 1 ──→ Middleware 2 ──→ ... ──→ Endpoint
↑ ↑ ↑
└── next() ─────────┘── next() ─────────────┘
Response ←─── Middleware 1 ←─── Middleware 2 ←─── ... ←─── EndpointOrden recomendado en ASP.NET Core:
var app = builder.Build();
// 1. Manejo de excepciones — PRIMERO
app.UseExceptionHandler("/error");
app.UseHsts();
// 2. HTTPS redirect
app.UseHttpsRedirection();
// 3. Archivos estáticos
app.UseStaticFiles();
// 4. Routing
app.UseRouting();
// 5. CORS
app.UseCors("PolicyName");
// 6. Autenticación → Autorización
app.UseAuthentication();
app.UseAuthorization();
// 7. Endpoints — ÚLTIMO
app.MapControllers();
app.Run();Tipos de middleware
Use — encadenado (más común)
app.Use(async (context, next) =>
{
// Antes de la siguiente capa
Console.WriteLine($"Antes: {context.Request.Path}");
await next.Invoke(context); // pasar al siguiente
// Después de la siguiente capa
Console.WriteLine($"Después: {context.Response.StatusCode}");
});Run — terminal (no encadena)
// Intercepta TODAS las requests que lleguen a este punto
app.Run(async context =>
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Ruta no encontrada");
});Map — bifurcación por ruta
// Solo procesa requests a /health
app.Map("/health", branch =>
{
branch.Run(async ctx =>
{
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsJsonAsync(new { status = "healthy" });
});
});Middleware personalizado con clase
La forma recomendada para middleware que necesita inyección de dependencias:
// Opción 1: IMiddleware (más limpio, DI por constructor)
public class RateLimitMiddleware : IMiddleware
{
private static readonly Dictionary<string, (int Count, DateTime Reset)> _hits = new();
private readonly ILogger<RateLimitMiddleware> _logger;
public RateLimitMiddleware(ILogger<RateLimitMiddleware> logger)
=> _logger = logger;
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
var ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var ahora = DateTime.UtcNow;
var ventana = TimeSpan.FromMinutes(1);
if (_hits.TryGetValue(ip, out var estado))
{
if (ahora < estado.Reset && estado.Count >= 60) // 60 req/min
{
_logger.LogWarning("Rate limit superado para IP {Ip}", ip);
ctx.Response.StatusCode = 429; // Too Many Requests
ctx.Response.Headers["Retry-After"] = "60";
await ctx.Response.WriteAsJsonAsync(new { error = "Demasiadas solicitudes" });
return;
}
if (ahora >= estado.Reset)
_hits[ip] = (1, ahora.Add(ventana));
else
_hits[ip] = (estado.Count + 1, estado.Reset);
}
else
{
_hits[ip] = (1, ahora.Add(ventana));
}
await next(ctx);
}
}
// Opción 2: Convención (patrón InvokeAsync con DI por parámetro)
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext ctx)
{
// DI por parámetro del método InvokeAsync
var correlationId = ctx.Request.Headers["X-Correlation-Id"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
ctx.Items["CorrelationId"] = correlationId;
ctx.Response.Headers["X-Correlation-Id"] = correlationId;
await _next(ctx);
}
}Registrar y usar middleware
// Registrar en el contenedor DI
builder.Services.AddTransient<RateLimitMiddleware>();
builder.Services.AddTransient<LoggingMiddleware>();
builder.Services.AddTransient<ExceptionMiddleware>();
// Método de extensión para uso limpio
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRateLimit(this IApplicationBuilder app)
=> app.UseMiddleware<RateLimitMiddleware>();
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
=> app.UseMiddleware<LoggingMiddleware>();
}
// En Program.cs
app.UseExceptionMiddleware(); // primero
app.UseRequestLogging();
app.UseRateLimit();Filtros de acción
Los filtros son middleware específico para el pipeline de MVC/controladores:
| Tipo | Cuándo se ejecuta |
|---|---|
IAuthorizationFilter |
Antes de la acción — decide si continuar |
IResourceFilter |
Después de routing, antes de model binding |
IActionFilter |
Justo antes/después de la acción |
IResultFilter |
Antes/después de que el resultado se escriba |
IExceptionFilter |
Cuando hay una excepción no manejada |
// Filtro de validación de API Key como atributo
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.Request.Headers.TryGetValue("X-API-Key", out var key))
{
context.Result = new UnauthorizedObjectResult(
new { error = "API Key requerida" });
return;
}
var expectedKey = context.HttpContext.RequestServices
.GetRequiredService<IConfiguration>()["ApiKey"];
if (key != expectedKey)
context.Result = new ForbidResult();
}
}
// Uso
[ApiKey]
[HttpGet("datos-privados")]
public IActionResult ObtenerDatosPrivados() => Ok("Solo con API Key");Manejo global de excepciones con ProblemDetails
// ASP.NET Core 7+ — UseExceptionHandler con ProblemDetails
builder.Services.AddProblemDetails();
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async ctx =>
{
var exceptionHandlerFeature = ctx.Features
.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
var exception = exceptionHandlerFeature?.Error;
ctx.Response.StatusCode = exception switch
{
ArgumentException => 400,
UnauthorizedAccessException => 401,
KeyNotFoundException => 404,
_ => 500
};
await ctx.Response.WriteAsJsonAsync(new ProblemDetails
{
Title = "Ocurrió un error",
Detail = exception?.Message,
Status = ctx.Response.StatusCode,
Instance = ctx.Request.Path
});
});
});Práctica
- Middleware de logging: Implementa
RequestLoggingMiddlewareque registre método, ruta, código de respuesta y tiempo de ejecución usandoILogger. - Filtro de correlación: Crea un
ActionFilterque agregue un headerX-Request-Idcon un GUID único a cada respuesta. - Manejo de excepciones: Implementa un
ExceptionMiddlewareque conviertaNotFoundExceptionen 404,ValidationExceptionen 400, y cualquier otra en 500 con un body JSON consistente.
En la siguiente lección aprenderemos a escribir tests en .NET con xUnit, Moq y WebApplicationFactory para APIs.
El orden del pipeline importa
El middleware se ejecuta en el orden en que se registra con app.Use*. El manejo de excepciones DEBE ser el primero (para capturar errores de cualquier middleware siguiente). La autenticación DEBE ir antes de la autorización. UseRouting DEBE ir antes de UseAuthorization y UseEndpoints.
IMiddleware vs middleware inline
IMiddleware permite inyección de dependencias por constructor y es más fácil de probar. El middleware inline (app.Use((ctx, next) => ...)) es útil para lógica simple de una o dos líneas. Para middleware que necesita servicios (logging, DB), usa siempre IMiddleware.
No olvides registrar IMiddleware en el DI
A diferencia del middleware inline, IMiddleware requiere estar registrado en el contenedor de DI. Agrega builder.Services.AddTransient<LoggingMiddleware>() antes de builder.Build(). Si olvidas el registro obtendrás una InvalidOperationException al iniciar la app.
// ── Middleware con IMiddleware (DI-friendly) ─────────
public class LoggingMiddleware : IMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
=> _logger = logger;
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var metodo = context.Request.Method;
var ruta = context.Request.Path;
_logger.LogInformation("→ {Metodo} {Ruta}", metodo, ruta);
try
{
await next(context); // llama al siguiente middleware
}
finally
{
sw.Stop();
_logger.LogInformation(
"← {Metodo} {Ruta} {Status} ({Ms}ms)",
metodo, ruta,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}
}
// ── Middleware de manejo global de excepciones ───────
public class ExceptionMiddleware : IMiddleware
{
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(ILogger<ExceptionMiddleware> logger)
=> _logger = logger;
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
try
{
await next(ctx);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error no manejado");
ctx.Response.StatusCode = 500;
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsJsonAsync(new
{
error = "Error interno del servidor",
traceId = ctx.TraceIdentifier
});
}
}
}
Inicia sesión para guardar tu progreso