En esta página

Middleware y filtros en ASP.NET Core

12 min lectura TextoCap. 5 — Producción

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 ←─── ... ←─── Endpoint

Orden 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

  1. Middleware de logging: Implementa RequestLoggingMiddleware que registre método, ruta, código de respuesta y tiempo de ejecución usando ILogger.
  2. Filtro de correlación: Crea un ActionFilter que agregue un header X-Request-Id con un GUID único a cada respuesta.
  3. Manejo de excepciones: Implementa un ExceptionMiddleware que convierta NotFoundException en 404, ValidationException en 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
            });
        }
    }
}