Back to "Comprensión del conducto de solicitud y respuesta de ASP.NET - Parte 3: Gasoducto de Middleware"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI-Article ASP.NET ASP.NET Lifecycle

Comprensión del conducto de solicitud y respuesta de ASP.NET - Parte 3: Gasoducto de Middleware

Sunday, 09 November 2025

Introducción

Hemos explorado la capa de alojamiento y el servidor Kestrel en la Parte 2. Ahora llegamos al corazón de ASP.NET Core: la tubería middleware. Aquí es donde vive la lógica de su aplicación, donde se procesan las solicitudes y donde se construyen las respuestas. Entender middleware es fundamental para dominar el núcleo ASP.NET.

Middleware es elegantemente simple en concepto —cada componente es una función que procesa una solicitud HTTP— pero lo suficientemente potente como para manejar todo, desde la autenticación hasta el manejo de errores hasta el servicio de archivos. En esta parte, exploraremos cómo funciona middleware, examinaremos los componentes incorporados y aprenderemos a crear los nuestros.

NOTA: Esto es parte de una serie de artículos experimentales (por lo tanto datados en el pasado). Es una combinación de IA y mi propia edición y retoque.

¿Qué es Middleware?

Middleware es un software que se ensambla en una tubería de aplicación para manejar peticiones y respuestas.

  1. Elige si pasar la solicitud al siguiente componente en la tubería
  2. Puede realizar el trabajo antes y después del siguiente componente en la tubería

Piense en ello como una serie de llamadas de función anidada, donde cada middleware envuelve la siguiente:

graph LR
    A[Request] --> B[Middleware 1]
    B --> C[Middleware 2]
    C --> D[Middleware 3]
    D --> E[Endpoint]
    E --> D
    D --> C
    C --> B
    B --> F[Response]

Cada middleware puede:

  • Procesar la solicitud antes de pasarla al siguiente middleware
  • Circuito corto y devolver una respuesta inmediatamente
  • Procesar la respuesta a medida que regresa a través de la tubería

La firma delegada de Middleware

En su núcleo, middleware es sólo una función con esta firma:

public delegate Task RequestDelegate(HttpContext context);

Cada middleware recibe:

  • HttpContext context - Contiene toda la información de solicitud y respuesta
  • RequestDelegate next - El próximo middleware en la tubería

Flujo de ejecución de Middleware

Vamos a visualizar cómo fluye una petición a través de middleware:

sequenceDiagram
    participant Client
    participant MW1 as Middleware 1
    participant MW2 as Middleware 2
    participant MW3 as Middleware 3
    participant Endpoint

    Client->>MW1: HTTP Request
    Note over MW1: Before logic
    MW1->>MW2: next()
    Note over MW2: Before logic
    MW2->>MW3: next()
    Note over MW3: Before logic
    MW3->>Endpoint: next()
    Note over Endpoint: Execute endpoint
    Endpoint-->>MW3: Return
    Note over MW3: After logic
    MW3-->>MW2: Return
    Note over MW2: After logic
    MW2-->>MW1: Return
    Note over MW1: After logic
    MW1-->>Client: HTTP Response

Puntos clave:

  • Solicitud de flujos hacia adelante a través de la tubería (de arriba a abajo)
  • La respuesta fluye hacia atrás a través de la tubería (de abajo hacia arriba)
  • Cada middleware puede ejecutar código antes y después de llamar next()
  • Middleware puede cortocircuito al no llamar next()

Creando Middleware: Tres Enfoques

1. Middleware en línea con Use()

El enfoque más simple utiliza expresiones lambda:

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

app.Use(async (context, next) =>
{
    // Before the next middleware
    Console.WriteLine($"Request: {context.Request.Path}");
    var startTime = DateTime.UtcNow;

    // Call the next middleware
    await next(context);

    // After the next middleware
    var elapsed = DateTime.UtcNow - startTime;
    Console.WriteLine($"Response: {context.Response.StatusCode} ({elapsed.TotalMilliseconds}ms)");
});

app.MapGet("/", () => "Hello World!");

app.Run();

Salida al acceder /:

Request: /
Response: 200 (15.3ms)

2. Terminal de Middleware con Run()

Run() crea middleware terminal que termina la tubería:

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

app.Use(async (context, next) =>
{
    Console.WriteLine("This executes");
    await next(context);
});

app.Run(async context =>
{
    Console.WriteLine("This is terminal - no next() to call");
    await context.Response.WriteAsync("End of pipeline");
});

// This never executes because Run() terminates the pipeline
app.Use(async (context, next) =>
{
    Console.WriteLine("This never executes");
    await next(context);
});

app.Run();

3. Medioware basado en clases

Para middleware complejo, use una clase:

// Middleware class
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();

        // Store start time in HttpContext.Items for other middleware to access
        context.Items["RequestStartTime"] = DateTime.UtcNow;

        try
        {
            await _next(context);
        }
        finally
        {
            sw.Stop();
            _logger.LogInformation(
                "Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
                context.Request.Method,
                context.Request.Path,
                sw.ElapsedMilliseconds,
                context.Response.StatusCode
            );
        }
    }
}

// Extension method for convenience
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestTimingMiddleware>();
    }
}

// Usage
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseRequestTiming();

app.MapGet("/", () => "Hello World!");

app.Run();

Producto:

info: RequestTimingMiddleware[0]
      Request GET / completed in 12ms with status 200

El pedido de Middleware importa

El orden en el que se añade middleware a la tubería es crítico. Aquí está el orden recomendado:

graph TD
    A[Exception Handler] --> B[HSTS]
    B --> C[HTTPS Redirection]
    C --> D[Static Files]
    D --> E[Routing]
    E --> F[CORS]
    F --> G[Authentication]
    G --> H[Authorization]
    H --> I[Custom Middleware]
    I --> J[Session]
    J --> K[Response Caching]
    K --> L[Response Compression]
    L --> M[Endpoints]

¿Por qué esta orden?

  1. Manipulador de excepción - Debe ser primero para atrapar todas las excepciones
  2. HSTS - Cabecera de seguridad que debe ser fijada temprano
  3. Redirección HTTPS - Redirigir a HTTPS antes de procesar
  4. Archivos estáticos - Puede cortocircuito temprano para el contenido estático
  5. Enrutamiento - Coincide con la petición a un punto final
  6. CORS - Debe ser después de Enrutamiento y antes de Autenticación
  7. Autenticación - Identifica quién es el usuario
  8. Autorización - Determina lo que el usuario puede hacer
  9. Middleware personalizado - Su lógica de aplicación
  10. Sesión - Gestión del estado de sesión
  11. Caché de respuesta - Respuestas caché
  12. Compresión de la respuesta - Comprimir las respuestas
  13. Puntos finales - Ejecutar el punto final emparejado

Veamos esto en código:

var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
builder.Services.AddResponseCaching();
builder.Services.AddResponseCompression();
builder.Services.AddSession();
builder.Services.AddCors();

var app = builder.Build();

// 1. Exception handling (must be first)
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
    app.UseHsts(); // HTTP Strict Transport Security
}

// 2. HTTPS redirection
app.UseHttpsRedirection();

// 3. Static files (can short-circuit)
app.UseStaticFiles();

// 4. Routing (matches endpoints)
app.UseRouting();

// 5. CORS (after routing, before auth)
app.UseCors();

// 6. Authentication (who are you?)
app.UseAuthentication();

// 7. Authorization (what can you do?)
app.UseAuthorization();

// 8. Custom middleware
app.UseRequestTiming();

// 9. Session
app.UseSession();

// 10. Response caching
app.UseResponseCaching();

// 11. Response compression
app.UseResponseCompression();

// 12. Endpoints
app.MapGet("/", () => "Hello World!");

app.Run();

Buceo profundo de Middleware incorporado

Excepción Manipulador Middleware

Captura excepciones de middleware posterior y genera respuestas de error:

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

// Global exception handler
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.StatusCode = 500;
        context.Response.ContentType = "application/json";

        var error = context.Features.Get<IExceptionHandlerFeature>();
        if (error != null)
        {
            var ex = error.Error;

            await context.Response.WriteAsJsonAsync(new
            {
                error = new
                {
                    message = ex.Message,
                    type = ex.GetType().Name,
                    stackTrace = app.Environment.IsDevelopment() ? ex.StackTrace : null
                }
            });
        }
    });
});

// This will be caught by the exception handler
app.MapGet("/error", () =>
{
    throw new InvalidOperationException("Something went wrong!");
});

app.MapGet("/", () => "Hello World!");

app.Run();

Ensayo:

$ curl http://localhost:5000/error
{
  "error": {
    "message": "Something went wrong!",
    "type": "InvalidOperationException",
    "stackTrace": "..."
  }
}

Archivos estáticos Middleware

Sirve archivos estáticos y cortocircuita la tubería:

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

// Default: serves files from wwwroot/
app.UseStaticFiles();

// Serve files from additional directory
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/StaticFiles"
});

// Custom file type (MIME mapping)
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".myapp"] = "application/x-myapp";

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
});

// Enable directory browsing (development only!)
if (app.Environment.IsDevelopment())
{
    app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
        RequestPath = "/browse"
    });
}

app.Run();

Diagrama de flujo:

graph TD
    A[Request: /css/site.css] --> B{Static Files Middleware}
    B -->|File exists| C[Serve file]
    C --> D[Return 200]
    B -->|File not found| E[Call next middleware]
    E --> F[Routing/Endpoints]
    F --> G[Return 404]

    style C stroke:#10b981,stroke-width:3px
    style G stroke:#ef4444,stroke-width:3px

Autenticación Middleware

Autentica el usuario en función de la solicitud:

var builder = WebApplication.CreateBuilder(args);

// Add authentication services
builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies", options =>
    {
        options.LoginPath = "/login";
        options.LogoutPath = "/logout";
        options.ExpireTimeSpan = TimeSpan.FromHours(1);
    })
    .AddJwtBearer("Bearer", options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "https://myapp.com",
            ValidAudience = "https://myapp.com",
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("your-secret-key-here"))
        };
    });

var app = builder.Build();

app.UseAuthentication(); // Must be before UseAuthorization()

app.MapGet("/public", () => "Anyone can access this");

app.MapGet("/protected", () => "Only authenticated users can access this")
    .RequireAuthorization();

app.MapGet("/login", async (HttpContext context) =>
{
    var claims = new[]
    {
        new Claim(ClaimTypes.Name, "testuser"),
        new Claim(ClaimTypes.Email, "[email protected]")
    };

    var identity = new ClaimsIdentity(claims, "Cookies");
    var principal = new ClaimsPrincipal(identity);

    await context.SignInAsync("Cookies", principal);

    return Results.Ok("Logged in");
});

app.Run();

Autorización Middleware

Determina si un usuario autenticado tiene permiso para acceder a un recurso:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies");

// Configure authorization policies
builder.Services.AddAuthorization(options =>
{
    // Policy: Requires specific claim
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireClaim("role", "admin"));

    // Policy: Requires age over 18
    options.AddPolicy("Adults", policy =>
        policy.RequireAssertion(context =>
        {
            var ageClaim = context.User.FindFirst("age");
            if (ageClaim != null && int.TryParse(ageClaim.Value, out var age))
            {
                return age >= 18;
            }
            return false;
        }));

    // Policy: Combines requirements
    options.AddPolicy("AdminOrManager", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim("role", "admin") ||
            context.User.HasClaim("role", "manager")));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// No authorization required
app.MapGet("/public", () => "Public endpoint");

// Requires authentication
app.MapGet("/authenticated", () => "Authenticated endpoint")
    .RequireAuthorization();

// Requires specific policy
app.MapGet("/admin", () => "Admin only endpoint")
    .RequireAuthorization("AdminOnly");

// Multiple policies
app.MapGet("/restricted", () => "Adults and admins only")
    .RequireAuthorization("Adults", "AdminOnly");

app.Run();

CORS Middleware

Maneja el uso compartido de recursos de origen cruzado:

var builder = WebApplication.CreateBuilder(args);

// Configure CORS policies
builder.Services.AddCors(options =>
{
    // Policy 1: Allow all
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });

    // Policy 2: Specific origin
    options.AddPolicy("AllowSpecificOrigin", policy =>
    {
        policy.WithOrigins("https://example.com", "https://app.example.com")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials(); // Important for cookies/auth
    });

    // Policy 3: Specific methods and headers
    options.AddPolicy("RestrictedAccess", policy =>
    {
        policy.WithOrigins("https://partner.com")
              .WithMethods("GET", "POST")
              .WithHeaders("Content-Type", "Authorization")
              .WithExposedHeaders("X-Custom-Header")
              .SetPreflightMaxAge(TimeSpan.FromMinutes(10));
    });

    // Default policy
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins("https://trustedsite.com")
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

var app = builder.Build();

app.UseRouting();

// Apply CORS globally
app.UseCors("AllowSpecificOrigin");

// Or apply per endpoint
app.MapGet("/api/data", () => new { data = "Hello" })
    .RequireCors("AllowAll");

app.MapGet("/api/restricted", () => new { data = "Restricted" })
    .RequireCors("RestrictedAccess");

app.Run();

Flujo de solicitud de prevuelo CORS:

sequenceDiagram
    participant Browser
    participant CORS as CORS Middleware
    participant Endpoint

    Browser->>CORS: OPTIONS /api/data<br/>(Preflight Request)
    Note over Browser,CORS: Origin: https://example.com<br/>Access-Control-Request-Method: POST<br/>Access-Control-Request-Headers: Content-Type

    Note over CORS: Check CORS policy

    alt Policy allows
        CORS-->>Browser: 204 No Content<br/>Access-Control-Allow-Origin: https://example.com<br/>Access-Control-Allow-Methods: POST<br/>Access-Control-Allow-Headers: Content-Type
        Browser->>CORS: POST /api/data<br/>(Actual Request)
        CORS->>Endpoint: Forward request
        Endpoint-->>CORS: Response
        CORS-->>Browser: Response + CORS headers
    else Policy denies
        CORS-->>Browser: 403 Forbidden
    end

Respuesta Caché Middleware

Respuestas Caches para mejorar el rendimiento:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseResponseCaching();

// Cache this endpoint
app.MapGet("/cached", (HttpContext context) =>
{
    context.Response.Headers.CacheControl = "public,max-age=60"; // Cache for 60 seconds
    return $"Generated at {DateTime.UtcNow:HH:mm:ss}";
});

// Don't cache this
app.MapGet("/no-cache", (HttpContext context) =>
{
    context.Response.Headers.CacheControl = "no-cache";
    return $"Generated at {DateTime.UtcNow:HH:mm:ss}";
});

// Conditional caching
app.MapGet("/data", (HttpContext context, string? cache) =>
{
    if (cache == "yes")
    {
        context.Response.Headers.CacheControl = "public,max-age=30";
    }

    return new
    {
        timestamp = DateTime.UtcNow,
        data = "Some data"
    };
});

app.Run();

Ensayo:

# First request - generates response and caches it
$ curl -i http://localhost:5000/cached
Date: Mon, 15 Jan 2024 10:30:00 GMT
Cache-Control: public,max-age=60

Generated at 10:30:00

# Second request within 60 seconds - served from cache
$ curl -i http://localhost:5000/cached
Date: Mon, 15 Jan 2024 10:30:00 GMT  # Same time!
Cache-Control: public,max-age=60
Age: 15  # Cache age in seconds

Generated at 10:30:00  # Same response!

Compresión de respuesta Middleware

Comprime las respuestas para reducir el ancho de banda:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true; // Enable for HTTPS (be aware of CRIME attack)

    // Providers (order matters - tried in sequence)
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();

    // MIME types to compress
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "application/json", "text/plain", "text/css", "application/javascript" });
});

// Configure compression levels
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Fastest; // Fastest, Optimal, SmallestSize
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Optimal;
});

var app = builder.Build();

app.UseResponseCompression();

app.MapGet("/large", () =>
{
    // Generate large response
    var data = string.Join("", Enumerable.Repeat("Hello World! ", 1000));
    return Results.Text(data, "text/plain");
});

app.Run();

Ensayo:

# Without compression
$ curl -H "Accept-Encoding:" http://localhost:5000/large
# Response: ~13KB

# With gzip
$ curl -H "Accept-Encoding: gzip" http://localhost:5000/large
# Response: ~100 bytes (compressed)
# Header: Content-Encoding: gzip

# With brotli (better compression)
$ curl -H "Accept-Encoding: br" http://localhost:5000/large
# Response: ~60 bytes (compressed)
# Header: Content-Encoding: br

Crear Middleware personalizado: Ejemplos del mundo real

Solicitar ID Middleware

Añade un identificador único a cada solicitud de rastreo:

public class RequestIdMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestIdMiddleware> _logger;

    public RequestIdMiddleware(RequestDelegate next, ILogger<RequestIdMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Check if request already has an ID (from client)
        var requestId = context.Request.Headers["X-Request-ID"].FirstOrDefault()
                       ?? Guid.NewGuid().ToString();

        // Add to response headers
        context.Response.Headers["X-Request-ID"] = requestId;

        // Store in HttpContext for other middleware/endpoints to access
        context.Items["RequestId"] = requestId;

        // Use scope for structured logging
        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["RequestId"] = requestId
        }))
        {
            _logger.LogInformation("Processing request {RequestId}", requestId);

            await _next(context);

            _logger.LogInformation("Completed request {RequestId}", requestId);
        }
    }
}

Autenticación de clave de API Middleware

Autenticación personalizada para claves API:

public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;
    private const string API_KEY_HEADER = "X-API-Key";

    public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
    {
        _next = next;
        _configuration = configuration;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Skip authentication for public endpoints
        if (context.Request.Path.StartsWithSegments("/public"))
        {
            await _next(context);
            return;
        }

        // Check for API key in header
        if (!context.Request.Headers.TryGetValue(API_KEY_HEADER, out var providedKey))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "API Key is missing" });
            return;
        }

        // Validate API key (in production, check against database)
        var validApiKey = _configuration["ApiKey"];
        if (providedKey != validApiKey)
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
            return;
        }

        // Set user identity based on API key
        var claims = new[] { new Claim("ApiKey", providedKey!) };
        var identity = new ClaimsIdentity(claims, "ApiKey");
        context.User = new ClaimsPrincipal(identity);

        await _next(context);
    }
}

Tasa de limitación de Middleware

Limita las solicitudes por dirección IP:

public class RateLimitingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _cache;
    private readonly int _requestLimit;
    private readonly TimeSpan _timeWindow;

    public RateLimitingMiddleware(
        RequestDelegate next,
        IMemoryCache cache,
        int requestLimit = 100,
        int timeWindowSeconds = 60)
    {
        _next = next;
        _cache = cache;
        _requestLimit = requestLimit;
        _timeWindow = TimeSpan.FromSeconds(timeWindowSeconds);
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var cacheKey = $"RateLimit_{clientIp}";

        // Get current request count
        var requestCount = _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = _timeWindow;
            return 0;
        });

        if (requestCount >= _requestLimit)
        {
            context.Response.StatusCode = 429; // Too Many Requests
            context.Response.Headers["Retry-After"] = _timeWindow.TotalSeconds.ToString();

            await context.Response.WriteAsJsonAsync(new
            {
                error = "Rate limit exceeded",
                retryAfter = _timeWindow.TotalSeconds
            });
            return;
        }

        // Increment request count
        _cache.Set(cacheKey, requestCount + 1, _timeWindow);

        // Add rate limit headers
        context.Response.Headers["X-RateLimit-Limit"] = _requestLimit.ToString();
        context.Response.Headers["X-RateLimit-Remaining"] = (_requestLimit - requestCount - 1).ToString();
        context.Response.Headers["X-RateLimit-Reset"] = DateTimeOffset.UtcNow.Add(_timeWindow).ToUnixTimeSeconds().ToString();

        await _next(context);
    }
}

// Usage
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
var app = builder.Build();

app.UseMiddleware<RateLimitingMiddleware>(100, 60); // 100 requests per 60 seconds

app.Run();

Ensayo:

# First request - OK
$ curl -i http://localhost:5000/api/data
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1705318860

# After 100 requests
$ curl -i http://localhost:5000/api/data
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705318860

{"error":"Rate limit exceeded","retryAfter":60}

Ramaje de Middleware

Puede ramificar el gasoducto en función de las condiciones:

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

// Map: Creates a branch based on path prefix
app.Map("/api", apiApp =>
{
    apiApp.UseMiddleware<ApiKeyMiddleware>();
    apiApp.MapGet("/data", () => new { data = "API data" });
});

// MapWhen: Creates a branch based on custom condition
app.MapWhen(
    context => context.Request.Headers.ContainsKey("X-Custom-Header"),
    customApp =>
    {
        customApp.Use(async (context, next) =>
        {
            context.Response.Headers["X-Custom-Response"] = "Matched!";
            await next(context);
        });

        customApp.MapGet("/special", () => "Special endpoint");
    });

// UseWhen: Rejoins the main pipeline after the branch
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/admin"),
    adminApp =>
    {
        adminApp.Use(async (context, next) =>
        {
            // Log admin access
            Console.WriteLine("Admin area accessed");
            await next(context);
        });
    });

app.MapGet("/", () => "Main pipeline");

app.Run();

Visualización de ramificaciones:

graph TD
    A[Request] --> B{Path?}
    B -->|/api/*| C[API Branch]
    B -->|/admin/*| D[Admin Branch UseWhen]
    B -->|Other| E[Main Pipeline]

    C --> F[API Key Middleware]
    F --> G[API Endpoints]
    G --> Z1[Response]

    D --> H[Admin Logging]
    H --> I[Rejoin Main Pipeline]
    I --> J[Main Pipeline Continues]
    J --> Z2[Response]

    E --> K[Main Endpoints]
    K --> Z3[Response]

Mejores prácticas de Middleware

1. Mantenga el enfoque de Middleware

Cada middleware debe tener una sola responsabilidad:

// ❌ Bad: Doing too much
public class BadMiddleware
{
    public async Task InvokeAsync(HttpContext context)
    {
        // Authentication
        // Authorization
        // Logging
        // Rate limiting
        // Response modification
        // ... too much!
    }
}

// ✅ Good: Single responsibility
public class AuthenticationMiddleware { /* Only authentication */ }
public class LoggingMiddleware { /* Only logging */ }
public class RateLimitingMiddleware { /* Only rate limiting */ }

2. Manejar adecuadamente las excepciones

public class SafeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<SafeMiddleware> _logger;

    public SafeMiddleware(RequestDelegate next, ILogger<SafeMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in middleware");

            // Don't swallow exceptions - let exception handler middleware handle them
            throw;
        }
    }
}

3. Respetar la respuesta

No modifique la respuesta después de que haya comenzado:

public class ResponseSafeMiddleware
{
    private readonly RequestDelegate _next;

    public ResponseSafeMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //  Good: Check before modifying
        if (!context.Response.HasStarted)
        {
            context.Response.Headers["X-Custom-Header"] = "Value";
        }

        await _next(context);

        //  Bad: Response might have already started
        // context.Response.Headers["X-After-Header"] = "Value"; // This might throw!

        // ✅ Good: Check first
        if (!context.Response.HasStarted)
        {
            context.Response.Headers["X-After-Header"] = "Value";
        }
    }
}

4. Utilice la inyección de dependencia sabiamente

public class DIAwareMiddleware
{
    private readonly RequestDelegate _next;
    //  Singleton services injected in constructor
    private readonly ILogger<DIAwareMiddleware> _logger;

    public DIAwareMiddleware(RequestDelegate next, ILogger<DIAwareMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    //  Scoped/Transient services injected in InvokeAsync
    public async Task InvokeAsync(HttpContext context, IMyService myService)
    {
        // myService is scoped to this request
        var data = await myService.GetDataAsync();

        await _next(context);
    }
}

Llaves para llevar

  • Middleware es el corazón del procesamiento de solicitudes en ASP.NET Core
  • Cada middleware envuelve al siguiente, creando una cadena anidada
  • El orden de Middleware es crítico: los manipuladores de excepción primero, los endpoints últimos
  • Middleware puede cortocircuito al no llamar next()
  • Middleware incorporado maneja preocupaciones comunes (archivos estáticos, auth, CORS, caché, compresión)
  • Middleware personalizado extiende el marco para las necesidades específicas de la aplicación
  • Uso app.Use() para middleware en línea, clases para middleware complejo
  • Middleware puede ramificar la tubería con Map() y MapWhen()
  • Comprobar siempre Response.HasStarted antes de modificar las cabeceras
  • Inyectar servicios de un soloton en el constructor, servicios con alcance en InvokeAsync()

Entender middleware le da un control completo sobre cómo se procesan las solicitudes. Cada preocupación transversal, desde el registro hasta la autenticación hasta la lógica de negocio personalizada, se puede manejar elegantemente como middleware.


Continuar a la Parte 4: Enrutamiento y Endpoints para saber cómo se corresponden las solicitudes con los puntos finales y cómo funciona el sistema de enrutamiento.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.