Understanding the ASP.NET Core Middleware Pipeline
The middleware pipeline is the backbone of ASP.NET Core's request processing. Every HTTP request flows through a series of middleware components, each having the opportunity to process the request and response. Understanding this pipeline is crucial for building efficient web applications.
How the Pipeline Works
The middleware pipeline follows a Russian doll model. Each middleware:
- Can process the incoming request
- Can call the next middleware in the pipeline
- Can process the outgoing response
Request → [Middleware 1] → [Middleware 2] → [Middleware 3] → Endpoint
Response ← [Middleware 1] ← [Middleware 2] ← [Middleware 3] ← EndpointThe RequestDelegate
At its core, a middleware is just a function that takes HttpContext and returns a Task:
public delegate Task RequestDelegate(HttpContext context);Each middleware receives a RequestDelegate representing the next middleware in the pipeline.
Building Custom Middleware
Inline Middleware
The simplest way to add middleware is inline using Use:
app.Use(async (context, next) =>
{
// Before the next middleware
var stopwatch = Stopwatch.StartNew();
await next(context); // Call the next middleware
// After the next middleware
stopwatch.Stop();
context.Response.Headers["X-Response-Time"] = $"{stopwatch.ElapsedMilliseconds}ms";
});Class-Based Middleware
For reusable middleware, create a 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 stopwatch = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
stopwatch.Stop();
_logger.LogInformation(
"Request {Method} {Path} completed in {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
stopwatch.ElapsedMilliseconds);
}
}
}
// Extension method for clean registration
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestTimingMiddleware>();
}
}Middleware Order Matters
The order in which you register middleware is critical. Here's a typical order:
var app = builder.Build();
// 1. Exception handling (first to catch all errors)
app.UseExceptionHandler("/error");
// 2. HTTPS redirection
app.UseHttpsRedirection();
// 3. Static files (short-circuit for static content)
app.UseStaticFiles();
// 4. Routing
app.UseRouting();
// 5. CORS (after routing, before authentication)
app.UseCors();
// 6. Authentication (who are you?)
app.UseAuthentication();
// 7. Authorization (what can you do?)
app.UseAuthorization();
// 8. Custom middleware
app.UseRequestTiming();
// 9. Endpoints
app.MapControllers();Short-Circuiting the Pipeline
Middleware can short-circuit the pipeline by not calling next:
public class MaintenanceModeMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _config;
public MaintenanceModeMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
_config = config;
}
public async Task InvokeAsync(HttpContext context)
{
if (_config.GetValue<bool>("MaintenanceMode"))
{
context.Response.StatusCode = 503;
await context.Response.WriteAsJsonAsync(new
{
Message = "Service is under maintenance",
RetryAfter = "3600"
});
return; // Don't call next - short circuit!
}
await _next(context);
}
}Branching the Pipeline
Use Map and MapWhen to create pipeline branches:
// Branch based on path
app.Map("/api", apiApp =>
{
apiApp.UseAuthentication();
apiApp.UseAuthorization();
apiApp.UseRateLimiting();
});
// Branch based on condition
app.MapWhen(
context => context.Request.Headers.ContainsKey("X-Custom-Header"),
customApp =>
{
customApp.UseCustomProcessor();
});Real-World Middleware Examples
Global Exception Handler
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var response = new
{
Error = "An error occurred",
TraceId = Activity.Current?.Id ?? context.TraceIdentifier
};
await context.Response.WriteAsJsonAsync(response);
}
}
}Request/Response Logging
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Log request
_logger.LogInformation(
"HTTP {Method} {Path} started",
context.Request.Method,
context.Request.Path);
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
// Log response
_logger.LogInformation(
"HTTP {Method} {Path} completed with {StatusCode}",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode);
await responseBody.CopyToAsync(originalBodyStream);
}
}Correlation ID Middleware
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
private const string CorrelationIdHeader = "X-Correlation-ID";
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.Headers[CorrelationIdHeader] = correlationId;
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
}))
{
await _next(context);
}
}
}Performance Considerations
- Order matters for performance - Put fast, short-circuiting middleware early
- Avoid allocations - Reuse objects where possible
- Use async properly - Don't block with
.Resultor.Wait() - Consider pooling - Use
ObjectPool<T>for expensive objects
Conclusion
The middleware pipeline is a powerful abstraction that makes ASP.NET Core flexible and extensible. By understanding how requests flow through the pipeline and how to create custom middleware, you can implement cross-cutting concerns cleanly and efficiently.
Remember: the order of middleware registration determines the order of execution. Plan your pipeline carefully, and always consider both the request and response phases when designing middleware.
