Introduction: Understanding Middleware in ASP.NET Core
Middleware in ASP.NET Core refers to software components that form a pipeline to handle HTTP requests and responses. Each middleware component in the request pipeline is responsible for invoking the next component in the pipeline or short-circuiting the pipeline. When a middleware short-circuits, it’s called a terminal middleware because it prevents further middleware from processing the request. This architecture allows developers to build flexible, modular web applications by composing different middleware components that each perform specific tasks in processing HTTP requests.
Core Middleware Concepts
Middleware Fundamentals
- Definition: Software components that handle HTTP requests and responses
- Pipeline Structure: Ordered sequence of middleware components
- Execution Flow: Bidirectional – processes requests on the way in and responses on the way out
- Capabilities: Can inspect, modify, or terminate request processing
- Component Types: Built-in middleware, custom middleware, and inline middleware
Key Characteristics
- Executes in the sequence it’s added to the pipeline
- Can perform operations before and after the next component
- Has access to both the incoming request and outgoing response
- Can short-circuit the pipeline by not calling the next middleware
- Can be registered with different extension methods (Use, Run, Map)
Middleware Pipeline Execution Flow
REQUEST RESPONSE
↓ ↑
Client → [Middleware 1] → [Middleware 2] → ... → [Middleware n] → Application Logic
↓ ↑ ↓ ↑ ↓ ↑
Request Processing Stages
- Incoming Request: Middleware executes in registration order (1 → 2 → … → n)
- Response Generation: After the endpoint/logic generates a response
- Outgoing Response: Middleware executes in reverse order (n → … → 2 → 1)
Built-in ASP.NET Core Middleware Components
Essential Middleware (in Recommended Order)
| Middleware | Registration Method | Purpose | Typical Position |
|---|
| Exception Handler | UseExceptionHandler() | Catches exceptions, logs them, and serves custom error pages | First |
| HTTPS Redirection | UseHttpsRedirection() | Redirects HTTP requests to HTTPS | Early |
| Static Files | UseStaticFiles() | Serves static files from wwwroot | Early |
| Routing | UseRouting() | Matches request to an endpoint | Before endpoint-specific middleware |
| CORS | UseCors() | Enables Cross-Origin Resource Sharing | Before authentication |
| Authentication | UseAuthentication() | Identifies the user | Before authorization |
| Authorization | UseAuthorization() | Determines if user has access | After authentication |
| Endpoint | UseEndpoints() | Executes the matched endpoint | Last |
Additional Middleware Components
| Middleware | Registration Method | Purpose |
|---|
| Response Caching | UseResponseCaching() | Caches responses to improve performance |
| Response Compression | UseResponseCompression() | Compresses response content |
| Session | UseSession() | Enables session state |
| Health Checks | UseHealthChecks() | Provides health monitoring endpoints |
| Request Localization | UseRequestLocalization() | Sets culture information for requests |
| Rewrite | UseRewriter() | Modifies request URLs based on rules |
| WebSockets | UseWebSockets() | Enables WebSocket support |
| Host Filtering | UseHostFiltering() | Restricts hosts that can access the app |
| Rate Limiting | UseRateLimiter() | Controls the rate at which clients can access resources |
Registering Middleware
Basic Registration Methods
// Program.cs in ASP.NET Core 6+
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 1. Using built-in middleware
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// 2. Using inline middleware with Use (non-terminal)
app.Use(async (context, next) => {
// Do something before the next middleware
Console.WriteLine("Before next middleware");
await next();
// Do something after the next middleware
Console.WriteLine("After next middleware");
});
// 3. Using inline middleware with Run (terminal)
app.Run(async context => {
await context.Response.WriteAsync("Hello World!");
});
app.Run();
Map and MapWhen For Branching
// Branch based on path
app.Map("/admin", adminApp => {
adminApp.Use(async (context, next) => {
// Admin-specific middleware
await next();
});
});
// Branch based on condition
app.MapWhen(
context => context.Request.Query.ContainsKey("api"),
apiApp => {
apiApp.Run(async context => {
await context.Response.WriteAsync("API Response");
});
}
);
UseWhen For Conditional Middleware
// Conditionally execute middleware but stay in same pipeline
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
appBuilder => {
appBuilder.UseMiddleware<ApiLoggingMiddleware>();
}
);
Creating Custom Middleware
Convention-Based Middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
// Constructor
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
// Method called by runtime
public async Task InvokeAsync(HttpContext context)
{
// Pre-processing logic
_logger.LogInformation($"Request: {context.Request.Method} {context.Request.Path}");
// Call the next middleware in the pipeline
await _next(context);
// Post-processing logic
_logger.LogInformation($"Response: {context.Response.StatusCode}");
}
}
// Extension method for cleaner registration
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// Usage in Program.cs
app.UseRequestLogging();
Factory-Based Middleware
public class FactoryMiddleware : IMiddleware
{
private readonly IServiceProvider _serviceProvider;
public FactoryMiddleware(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// Access scoped services from the container
var scopedService = context.RequestServices.GetRequiredService<IScopedService>();
// Process the request
await next(context);
}
}
// Registration in Program.cs
builder.Services.AddTransient<FactoryMiddleware>();
app.UseMiddleware<FactoryMiddleware>();
Inline Middleware
app.Use(async (context, next) => {
var timer = Stopwatch.StartNew();
// Call next component
await next();
timer.Stop();
Console.WriteLine($"Request took {timer.ElapsedMilliseconds}ms");
});
Common Middleware Patterns and Examples
Authentication and Authorization
// Add services
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JWT:Secret"]))
};
});
// Register middleware
app.UseAuthentication();
app.UseAuthorization();
Error Handling
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Custom Error Handler Middleware
public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ErrorHandlerMiddleware> _logger;
public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
var response = new {
error = app.Environment.IsDevelopment() ? ex.Message : "An error occurred"
};
await context.Response.WriteAsJsonAsync(response);
}
}
}
// Extension method
public static class ErrorHandlerMiddlewareExtensions
{
public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ErrorHandlerMiddleware>();
}
}
// Register in Program.cs
app.UseErrorHandler();
Request/Response Logging
app.Use(async (context, next) =>
{
// Log the request
var requestBodyStream = new MemoryStream();
var originalRequestBody = context.Request.Body;
try
{
await context.Request.Body.CopyToAsync(requestBodyStream);
requestBodyStream.Seek(0, SeekOrigin.Begin);
var requestBodyText = await new StreamReader(requestBodyStream).ReadToEndAsync();
Console.WriteLine($"Request: {requestBodyText}");
requestBodyStream.Seek(0, SeekOrigin.Begin);
context.Request.Body = requestBodyStream;
// Capture the response
var originalResponseBody = context.Response.Body;
using var responseBodyStream = new MemoryStream();
context.Response.Body = responseBodyStream;
await next();
responseBodyStream.Seek(0, SeekOrigin.Begin);
var responseBodyText = await new StreamReader(responseBodyStream).ReadToEndAsync();
Console.WriteLine($"Response: {responseBodyText}");
responseBodyStream.Seek(0, SeekOrigin.Begin);
await responseBodyStream.CopyToAsync(originalResponseBody);
}
finally
{
context.Request.Body = originalRequestBody;
}
});
API Key Validation
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-API-Key", out var extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key was not provided");
return;
}
var apiKey = _configuration["ApiKey"];
if (!apiKey.Equals(extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid API Key");
return;
}
await _next(context);
}
}
// Extension method
public static class ApiKeyMiddlewareExtensions
{
public static IApplicationBuilder UseApiKey(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ApiKeyMiddleware>();
}
}
// Register in Program.cs
app.UseApiKey();
Response Caching
// Add services
builder.Services.AddResponseCaching();
// Middleware registration
app.UseResponseCaching();
// In controller
[HttpGet]
[ResponseCache(Duration = 60)]
public IActionResult Get()
{
return Ok(DateTime.Now);
}
Culture Middleware
app.Use(async (context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
await next();
});
Performance Considerations
Middleware Performance Best Practices
- Order Matters: Place frequently used middleware earlier in the pipeline
- Short-Circuit When Possible: Don’t process the entire pipeline if unnecessary
- Avoid Blocking Operations: Use async/await for I/O-bound operations
- Minimize Middleware: Only include middleware you need
- Cache Where Appropriate: Use response caching for static or rarely changing content
- Avoid Capturing HttpContext: Don’t store or capture HttpContext for later use
- Use Memory Efficiently: Be careful with large request/response buffering
- Profile Your Middleware: Test performance in realistic scenarios
Common Performance Issues and Solutions
| Issue | Solution |
|---|
| Middleware executes for all requests | Use Map/MapWhen to branch for specific paths |
| Too many middleware components | Consolidate related functionality |
| Heavy processing middleware | Move to background processing |
| Memory leaks | Avoid static references to HttpContext |
| Unnecessary middleware | Remove middleware not needed for your scenario |
Debugging Middleware
Logging Pipeline Execution
app.Use(async (context, next) =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Middleware pipeline started");
await next();
logger.LogInformation("Middleware pipeline completed");
});
Middleware Diagnostic Tools
- Application Insights: Monitor request processing times
- Logging: Add detailed logs in each middleware
- Metrics: Track performance counters for requests
- Visual Studio Diagnostics: Use profiling tools
- Correlation IDs: Add request correlation for tracing
Advanced Middleware Scenarios
Middleware Scopes and DI
public class ScopedMiddleware
{
private readonly RequestDelegate _next;
public ScopedMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IScopedService scopedService)
{
// Use scoped service with proper lifetime
await _next(context);
}
}
Conditional Middleware Execution
public class ConditionalMiddleware
{
private readonly RequestDelegate _next;
private readonly Func<HttpContext, bool> _predicate;
public ConditionalMiddleware(RequestDelegate next, Func<HttpContext, bool> predicate)
{
_next = next;
_predicate = predicate;
}
public async Task InvokeAsync(HttpContext context)
{
if (_predicate(context))
{
// Execute middleware logic
}
await _next(context);
}
}
// Registration helper
public static class ConditionalMiddlewareExtensions
{
public static IApplicationBuilder UseWhen(
this IApplicationBuilder app,
Func<HttpContext, bool> predicate,
Action<IApplicationBuilder> configureMiddleware)
{
ArgumentNullException.ThrowIfNull(app);
ArgumentNullException.ThrowIfNull(predicate);
ArgumentNullException.ThrowIfNull(configureMiddleware);
var branchBuilder = app.New();
configureMiddleware(branchBuilder);
var branch = branchBuilder.Build();
app.Use(async (context, next) =>
{
if (predicate(context))
{
await branch(context);
}
await next();
});
return app;
}
}
Middleware vs. Filters
| Aspect | Middleware | Filters |
|---|
| Scope | Entire application | Specific controllers/actions |
| Pipeline | Request processing pipeline | MVC/API action pipeline |
| Execution | For all requests | For specific routing endpoints |
| Context | HttpContext | ActionContext |
| Typical Use | Cross-cutting concerns | Model validation, action results |
Common Middleware Extensions
Health Checks
// Add services
builder.Services.AddHealthChecks()
.AddCheck("Database", () => {
// Check database connection
return HealthCheckResult.Healthy();
})
.AddCheck<ExternalApiHealthCheck>("ExternalApi");
// Register middleware
app.MapHealthChecks("/health");
Rate Limiting
// Add services
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "localhost",
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});
// Register middleware
app.UseRateLimiter();
CORS Configuration
// Add services
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin", policy =>
{
policy.WithOrigins("https://example.com")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// Register middleware
app.UseCors("AllowSpecificOrigin");
Resources for Further Learning
Official Documentation
Community Resources
Books and Tutorials
- “ASP.NET Core in Action” by Andrew Lock
- “Pro ASP.NET Core” by Adam Freeman
- “Building Modern Web Applications with ASP.NET Core” – Microsoft Learn
This cheat sheet provides a comprehensive overview of middleware in ASP.NET Core. Remember that middleware order is critical for proper functioning of your application, as it determines how requests flow through your application pipeline.