Middleware Pipeline
Overview
Middleware is software assembled into an application pipeline to handle requests and responses. Each component:
- Chooses whether to pass the request to the next component
- Can perform work before and after the next component
How Middleware Works
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Middleware pipeline
app.UseHttpsRedirection(); // 1. Redirect HTTP → HTTPS
app.UseRouting(); // 2. Match endpoint
app.UseCors(); // 3. CORS policy
app.UseAuthentication(); // 4. Authenticate user
app.UseAuthorization(); // 5. Authorize user
app.UseRateLimiter(); // 6. Rate limiting
app.MapControllers(); // 7. Execute endpoint
app.Run();
Order Matters!
Correct Order:
Request → HTTPS → Routing → CORS → Auth(N) → Auth(Z) → Rate Limit → Endpoint
⚠️ Wrong Order = Security Issues:
// WRONG! Authorization before Authentication
app.UseAuthorization(); // ❌ Can't authorize without knowing who the user is
app.UseAuthentication(); // ❌ Too late!
Built-in Middleware
| Middleware | Purpose | Position |
|---|---|---|
UseHttpsRedirection | Redirect HTTP to HTTPS | Early |
UseStaticFiles | Serve static files | Early |
UseRouting | Match endpoints | Before auth |
UseCors | CORS policy | After routing |
UseAuthentication | Identify user | Before authorization |
UseAuthorization | Check permissions | Before endpoint |
UseSession | Session state | After routing |
UseResponseCaching | Cache responses | Early |
UseResponseCompression | Compress responses | Early |
Creating Custom Middleware
Method 1: Inline Lambda
app.Use(async (context, next) =>
{
// Before next middleware
var startTime = DateTime.UtcNow;
await next(context);
// After next middleware
var elapsed = DateTime.UtcNow - startTime;
context.Response.Headers.Add("X-Response-Time", elapsed.TotalMilliseconds.ToString());
});
Method 2: Custom Class
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)
{
_logger.LogInformation("Request: {Method} {Path}",
context.Request.Method,
context.Request.Path);
await _next(context);
_logger.LogInformation("Response: {StatusCode}",
context.Response.StatusCode);
}
}
// Extension method for cleaner registration
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// Usage
app.UseRequestLogging();
Method 3: Factory-based
public class RequestLoggingMiddleware : IMiddleware
{
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(ILogger<RequestLoggingMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
_logger.LogInformation("Handling request: {Path}", context.Request.Path);
await next(context);
_logger.LogInformation("Finished handling request.");
}
}
// Register as service
builder.Services.AddTransient<RequestLoggingMiddleware>();
// Use in pipeline
app.UseMiddleware<RequestLoggingMiddleware>();
Short-Circuiting
Middleware can short-circuit (stop) the pipeline:
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-API-Key"))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key required");
return; // Short-circuit! Don't call next()
}
await next(context);
});
Terminal Middleware
Terminal middleware doesn't call next():
app.Run(async context =>
{
await context.Response.WriteAsync("Final handler");
// No next() call - this ends the pipeline
});
Conditional Middleware
Map (Branch based on path)
app.Map("/admin", adminApp =>
{
adminApp.UseAuthentication();
adminApp.UseAuthorization();
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin area");
});
});
MapWhen (Branch based on predicate)
app.MapWhen(
context => context.Request.Query.ContainsKey("debug"),
app => app.Run(async context =>
{
await context.Response.WriteAsync("Debug mode");
})
);
UseWhen (Rejoin main pipeline)
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
app => app.UseRateLimiter()
);
Real-World Example: API Key Middleware
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 auth check for health endpoint
if (context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
if (!context.Request.Headers.TryGetValue(API_KEY_HEADER, out var providedKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "API Key missing" });
return;
}
var validKey = _configuration["ApiKey"];
if (providedKey != validKey)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
return;
}
await _next(context);
}
}
Exception Handling Middleware
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = exception switch
{
ArgumentException => 400,
UnauthorizedAccessException => 401,
KeyNotFoundException => 404,
_ => 500
};
var response = new
{
error = exception.Message,
statusCode = context.Response.StatusCode
};
return context.Response.WriteAsJsonAsync(response);
}
}
Standard Pipeline Order
var app = builder.Build();
// 1. Exception handling (must be first!)
app.UseExceptionHandler("/error");
// 2. HTTPS redirection
app.UseHttpsRedirection();
// 3. Static files (short-circuits for static content)
app.UseStaticFiles();
// 4. Routing (match endpoint)
app.UseRouting();
// 5. CORS (after routing)
app.UseCors();
// 6. Authentication (who are you?)
app.UseAuthentication();
// 7. Authorization (what can you do?)
app.UseAuthorization();
// 8. Rate limiting
app.UseRateLimiter();
// 9. Response caching
app.UseResponseCaching();
// 10. Session (if needed)
app.UseSession();
// 11. Endpoints (execute matched endpoint)
app.MapControllers();
app.Run();
Interview Questions
Q: Why does middleware order matter? A: Each middleware can process the request before passing to the next. If authentication runs after authorization, you're authorizing an unauthenticated user. If exception handling is last, unhandled exceptions won't be caught.
Q: What's the difference between Use, Run, and Map? A:
Use: Middleware that can call next()Run: Terminal middleware (doesn't call next)Map: Branches pipeline based on path
Q: How do you short-circuit the pipeline?
A: Don't call await next(context). Instead, write directly to the response and return.