ed Middleware Registration
Factory-based middleware decouples instantiation from pipeline configuration, enabling precise DI scope control and per-request resource management.
// Middleware implementation
public class AuditMiddleware : IMiddleware
{
private readonly IAuditRepository _auditRepo;
private readonly ILogger<AuditMiddleware> _logger;
public AuditMiddleware(IAuditRepository auditRepo, ILogger<AuditMiddleware> logger)
{
_auditRepo = auditRepo;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var watch = Stopwatch.StartNew();
try
{
await next(context);
}
finally
{
watch.Stop();
await _auditRepo.LogAsync(new AuditEntry
{
Path = context.Request.Path,
StatusCode = context.Response.StatusCode,
DurationMs = watch.ElapsedMilliseconds,
Timestamp = DateTimeOffset.UtcNow
});
_logger.LogInformation("Audit logged for {Path} in {Duration}ms",
context.Request.Path, watch.ElapsedMilliseconds);
}
}
}
// Factory registration in DI
builder.Services.AddTransient<AuditMiddleware>();
builder.Services.AddTransient<IMiddlewareFactory, MiddlewareFactory>();
Step 2: Pipeline Branching with MapWhen
Use MapWhen for conditional execution without polluting the main pipeline. This isolates path-specific logic while preserving cross-cutting concerns upstream.
var app = builder.Build();
// Core pipeline (applies to all requests)
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseMiddleware<RequestCorrelationMiddleware>();
// Branch: API-specific middleware
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api =>
{
api.UseMiddleware<ApiRateLimitingMiddleware>();
api.UseMiddleware<ApiVersioningMiddleware>();
api.UseRouting();
api.UseEndpoints(endpoints => endpoints.MapControllers());
});
// Branch: Static/Swagger
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/docs"), docs =>
{
docs.UseSwagger();
docs.UseSwaggerUI();
});
// Fallback
app.Run(async context =>
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Not Found");
});
Step 3: Safe Context Enrichment & State Passing
Avoid HttpContext.Items for long-lived or DI-dependent state. Use RequestServices for scoped resolution and AsyncLocal<T> only when context flows across async boundaries outside the request pipeline.
public static class HttpContextExtensions
{
public static T ResolveScoped<T>(this HttpContext context) where T : class
{
return context.RequestServices.GetRequiredService<T>();
}
}
// Usage inside middleware
var pricingService = context.ResolveScoped<IPricingService>();
Step 4: Terminal Error Handling Middleware
Global exception handling must be terminal, culture-aware, and structured. It should never swallow context or bypass response headers already set.
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
public GlobalExceptionHandlerMiddleware(RequestDelegate next, ILogger<GlobalExceptionHandlerMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception processing {Path}", context.Request.Path);
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
Error = "Internal Server Error",
TraceId = context.TraceIdentifier
});
}
}
}
Architecture Decisions & Rationale:
- Factory pattern over delegate: Explicit DI control prevents scope validation failures and enables deterministic resource disposal.
- Branching over linear chaining:
MapWhen reduces pipeline depth for non-matching requests, improving cold-start and routing performance.
- Terminal error handler placement: Must be first in the pipeline to catch exceptions from downstream components, but must respect already-started responses to avoid
InvalidOperationException.
- Context enrichment via
RequestServices: Guarantees scoped service alignment with the request lifecycle, avoiding singleton leakage.
Pitfall Guide
1. Pipeline Order Blindness
Mistake: Placing authentication middleware after routing, or logging after response generation.
Impact: Security boundaries are bypassed, and logs capture incomplete state. ASP.NET Core executes middleware in registration order. Authorization must follow routing but precede endpoint execution. Logging should wrap the entire pipeline to capture final status codes.
2. Async Blockers in Middleware
Mistake: Using .Result, .Wait(), or synchronous I/O (File.ReadAllText, HttpClient.Send).
Impact: Thread pool starvation under load. Middleware delegates run on ASP.NET Core's thread pool. Blocking calls reduce available threads for request processing, causing cascading timeouts. Always use async/await and HttpClientFactory for outbound calls.
3. DI Scope Leakage
Mistake: Registering middleware as singleton while injecting scoped services.
Impact: InvalidOperationException in development; silent memory leaks in production. Middleware instances are reused across requests. Scoped services must be resolved per-request via RequestServices or factory instantiation. Never inject scoped services into singleton middleware constructors.
4. HttpContext.Items Abuse
Mistake: Storing complex objects or DI-dependent state in HttpContext.Items without cleanup.
Impact: Cross-request pollution in connection pooling scenarios, increased GC pressure. Items is designed for lightweight, request-scoped data transfer. Use it only for simple values or middleware-to-middleware signaling. Dispose complex resources in finally blocks or via IAsyncDisposable.
5. Missing Terminal Middleware
Mistake: Omitting a fallback Run() or UseEndpoints() that terminates the pipeline.
Impact: Requests hang indefinitely, causing 504 Gateway Timeouts. The pipeline must always reach a terminal delegate. If no middleware matches, add app.Run(context => { ... }) to return 404 or default responses.
6. Ignoring IAsyncDisposable
Mistake: Allocating unmanaged resources (file handles, network streams, database connections) in middleware without async disposal.
Impact: Resource exhaustion and file descriptor leaks. Middleware that opens resources must implement IAsyncDisposable or ensure cleanup in finally blocks. Prefer dependency injection for resource management rather than inline allocation.
7. Overusing Map/MapWhen
Mistake: Creating deeply nested branches for minor path variations.
Impact: Routing fragmentation, duplicated cross-cutting concerns, and debugging complexity. Use Map only for distinct architectural boundaries (e.g., API vs Blazor vs Static). For minor variations, use route constraints or endpoint filters.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple header injection | Delegate (app.Use) | Low overhead, no DI required | Minimal |
| Scoped service dependency | Factory (IMiddleware + IMiddlewareFactory) | Explicit per-request scope control | Low (+0.7 μs/request) |
| Path-specific logic (>40% traffic) | MapWhen branching | Reduces pipeline traversal for non-matching requests | Medium (routing complexity) |
| Global error handling | Terminal middleware + UseExceptionHandler | Catches all downstream exceptions safely | Low |
| Cross-request state sharing | AsyncLocal<T> or distributed cache | HttpContext.Items is request-scoped only | High (memory/network) |
| High-throughput API gateway | Short-circuit routing + compiled middleware | Minimizes delegate chain evaluation | Medium (initial setup) |
Configuration Template
var builder = WebApplication.CreateBuilder(args);
// DI Registration
builder.Services.AddTransient<AuditMiddleware>();
builder.Services.AddTransient<SecurityHeadersMiddleware>();
builder.Services.AddTransient<ApiRateLimitingMiddleware>();
builder.Services.AddTransient<GlobalExceptionHandlerMiddleware>();
var app = builder.Build();
// Pipeline Configuration
app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseMiddleware<AuditMiddleware>();
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api =>
{
api.UseMiddleware<ApiRateLimitingMiddleware>();
api.UseRouting();
api.UseEndpoints(endpoints => endpoints.MapControllers());
});
app.Run(async context =>
{
context.Response.StatusCode = 404;
await context.Response.WriteAsJsonAsync(new { Error = "Not Found" });
});
app.Run();
Quick Start Guide
- Register middleware as transient services in
Program.cs using AddTransient<TMiddleware>() to enable factory resolution.
- Replace inline
app.Use() delegates with factory-registered IMiddleware implementations for any component requiring scoped DI or explicit disposal.
- Place terminal error handling at the top of the pipeline to catch exceptions from all downstream components while preserving response headers.
- Branch only for architectural boundaries using
MapWhen; avoid nesting beyond two levels to maintain pipeline predictability.
- Validate pipeline order by enabling
builder.Logging.AddConsole() and tracing RequestPath, StatusCode, and Duration in a test environment before production deployment.