Middleware in ASP.NET Core — Complete Guide
In this tutorial, you will learn about Middleware in ASP.NET Core. We cover key concepts, practical examples, and best practices to help you master this topic.
Hook
Middleware is the heart of the ASP.NET Core request pipeline. Every request flows through a series of middleware components that can inspect, modify, or short-circuit the request and response. Understanding middleware enables you to handle cross-cutting concerns like logging, authentication, error handling, and caching in a modular, reusable way using C#.
Learning Path
graph LR A[Middleware] --> B[Pipeline Concept] B --> C[Custom Middleware] B --> D[Built-in Middleware] C --> E[Factory-based Middleware] C --> F[Convention-based Middleware] style A fill:#4a90d9,color:#fff style B fill:#4a90d9,color:#fff style C fill:#4a90d9,color:#fff style D fill:#4a90d9,color:#fff style E fill:#4a90d9,color:#fff style F fill:#4a90d9,color:#fff
The Pipeline Concept
ASP.NET Core middleware is assembled as a pipeline. Each component can process the request, pass it to the next component, and process the response on the way back.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("1. Before");
await next();
Console.WriteLine("1. After");
});
app.Use(async (context, next) =>
{
Console.WriteLine("2. Before");
await next();
Console.WriteLine("2. After");
});
app.Run(async context =>
{
Console.WriteLine("3. Terminal");
await context.Response.WriteAsync("Hello from pipeline");
});
app.Run();
Output (request to /):
1. Before
2. Before
3. Terminal
2. After
1. After
Built-in Middleware
ASP.NET Core includes many built-in middleware components.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Exception handling (should be early in pipeline)
app.UseExceptionHandler("/error");
// HTTPS redirection
app.UseHttpsRedirection();
// Static files
app.UseStaticFiles();
// Routing
app.UseRouting();
// Authentication and Authorization
app.UseAuthentication();
app.UseAuthorization();
// CORS
app.UseCors("AllowSpecificOrigin");
// Response caching
app.UseResponseCaching();
// Rate limiting (.NET 7+)
app.UseRateLimiter();
app.MapControllers();
app.Run();
Writing Custom Middleware
There are two approaches: convention-based and Factory-based.
Convention-based Middleware
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)
{
var start = DateTime.UtcNow;
_logger.LogInformation("Request: {Method} {Path}",
context.Request.Method, context.Request.Path);
await _next(context);
var duration = (DateTime.UtcNow - start).TotalMilliseconds;
_logger.LogInformation("Response: {StatusCode} in {Duration}ms",
context.Response.StatusCode, duration);
}
}
// Register in Program.cs
app.UseMiddleware<RequestLoggingMiddleware>();
Factory-based Middleware
Use IMiddleware for middleware with DI-scoped dependencies.
public class TenantResolutionMiddleware : IMiddleware
{
private readonly ITenantService _tenantService;
public TenantResolutionMiddleware(ITenantService tenantService)
{
_tenantService = tenantService;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenantId = context.Request.Headers["X-Tenant-ID"].FirstOrDefault();
if (!string.IsNullOrEmpty(tenantId))
{
_tenantService.SetCurrentTenant(tenantId);
}
await next(context);
}
}
// Register
builder.Services.AddScoped<TenantResolutionMiddleware>();
app.UseMiddleware<TenantResolutionMiddleware>();
Short-Circuiting the Pipeline
A middleware can short-circuit the pipeline by not calling next.
public class MaintenanceModeMiddleware
{
private readonly RequestDelegate _next;
private readonly bool _isUnderMaintenance;
public MaintenanceModeMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
_isUnderMaintenance = config.GetValue<bool>("MaintenanceMode");
}
public async Task InvokeAsync(HttpContext context)
{
if (_isUnderMaintenance && !context.Request.Path.StartsWithSegments("/health"))
{
context.Response.StatusCode = 503;
context.Response.ContentType = "application/json";
var response = JsonSerializer.Serialize(new
{
error = "Service temporarily unavailable",
retryAfter = 60
});
await context.Response.WriteAsync(response);
return; // Short-circuit
}
await _next(context);
}
}
Branching the Pipeline
Use Map and MapWhen to create pipeline branches.
app.Map("/api", apiApp =>
{
// Middleware only applies to /api paths
apiApp.UseAuthentication();
apiApp.UseAuthorization();
apiApp.Run(async context =>
{
await context.Response.WriteAsync("API endpoint");
});
});
app.MapWhen(context => context.Request.Query.ContainsKey("admin"), adminApp =>
{
adminApp.Use(async (context, next) =>
{
// Custom admin authentication
await next();
});
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin area");
});
});
Custom Middleware with Options
Use the options pattern for configurable middleware.
public class RateLimitingOptions
{
public int MaxRequestsPerMinute { get; set; } = 100;
public string ClientIdHeader { get; set; } = "X-Client-ID";
}
public static class RateLimitingMiddlewareExtensions
{
public static IApplicationBuilder UseRateLimiting(
this IApplicationBuilder builder,
Action<RateLimitingOptions> configureOptions)
{
var options = new RateLimitingOptions();
configureOptions(options);
return builder.UseMiddleware<RateLimitingMiddleware>(Options.Create(options));
}
}
// Usage
app.UseRateLimiting(options =>
{
options.MaxRequestsPerMinute = 50;
options.ClientIdHeader = "X-API-Key";
});
Common Mistakes
Wrong middleware order: Exception Handling must be first. Authentication before Authorization. CORS before routing.
Not calling next: Forgetting
await next()causes the pipeline to terminate silently. Only skipnextintentionally for short-circuiting.Modifying request/response after next: The response body cannot be modified after the next middleware has written to it. Buffer or intercept before
next.Disposing scoped services in Singleton middleware: Convention-based middleware is singleton. Use factory-based middleware (
IMiddleware) for scoped dependencies.Blocking async code: Always use async/await in middleware. Calling
.Resultor.Wait()on tasks causes deadlocks.
Practice Questions
Write middleware that blocks requests from specific IP addresses defined in configuration.
Create middleware that adds security headers (X-Content-Type-Options, X-Frame-Options) to every response.
Implement request validation middleware that ensures every POST request has a valid JSON body.
Challenge: Build a correlation ID middleware that generates or forwards a correlation ID through HTTP headers and makes it available via
IHttpContextAccessor.
FAQ
Mini Project: Request Timer Middleware
Build middleware that measures and logs request duration with configurable thresholds.
using System.Diagnostics;
public class RequestTimerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimerMiddleware> _logger;
private readonly long _slowThresholdMs;
public RequestTimerMiddleware(RequestDelegate next,
ILogger<RequestTimerMiddleware> logger,
long slowThresholdMs = 1000)
{
_next = next;
_logger = logger;
_slowThresholdMs = slowThresholdMs;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
await _next(context);
sw.Stop();
var elapsed = sw.ElapsedMilliseconds;
if (elapsed > _slowThresholdMs)
{
_logger.LogWarning("SLOW REQUEST: {Method} {Path} took {Duration}ms (threshold: {Threshold}ms)",
context.Request.Method, context.Request.Path,
elapsed, _slowThresholdMs);
}
else
{
_logger.LogInformation("Request: {Method} {Path} completed in {Duration}ms",
context.Request.Method, context.Request.Path, elapsed);
}
// Add timing header
context.Response.Headers["X-Request-Duration-Ms"] = elapsed.ToString();
}
}
// Extension method
public static class RequestTimerExtensions
{
public static IApplicationBuilder UseRequestTimer(
this IApplicationBuilder builder, long slowThresholdMs = 1000)
{
return builder.UseMiddleware<RequestTimerMiddleware>(slowThresholdMs);
}
}
// Program.cs
var app = WebApplication.Create(args);
app.UseRequestTimer(slowThresholdMs: 500);
app.MapGet("/", () => {
Thread.Sleep(200); // Simulate work
return "Hello!";
});
app.MapGet("/slow", async () => {
await Task.Delay(2000); // Simulate slow operation
return "Slow response";
});
app.Run();
Middleware is one of the most powerful concepts in ASP.NET Core. By mastering custom middleware, you can build clean, modular .NET web applications that handle cross-cutting concerns elegantly without cluttering your business logic.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro