TL;DR: Middleware forms the backbone of the ASP.NET Core request pipeline. Order matters immensely. Always put error handling first and endpoint routing last. Use custom middleware for cross-cutting concerns like logging, correlation IDs, and global exception handling instead of polluting your controllers.
Every single HTTP request that hits your ASP.NET Core application passes through a pipeline of components. This pipeline is constructed using Middleware.
If you've ever wondered how authentication magically works before your controller is hit, or how static files are served without executing any C# business logic, the answer is middleware.
The Request Pipeline
The middleware pipeline works like a Russian nesting doll. Each middleware can run code before the next component, call next(), and then run code after the next component finishes.
graph TD
A[Incoming HTTP Request] --> B(Exception Handler Middleware)
B --> C(Authentication Middleware)
C --> D(Custom Logging Middleware)
D --> E(Endpoint Routing / Controller)
E -. Response .-> D
D -. Response .-> C
C -. Response .-> B
B -. Outgoing HTTP Response .-> A
Why Order Matters
Because middleware wraps the components that follow it, the order in which you register them in Program.cs is critical. If you register Authentication after Routing, your secure endpoints will execute before the user is authenticated, resulting in massive security vulnerabilities.
❌ The Bad Way (Incorrect Order)
var app = builder.Build();
// BAD: Routing happens before Authentication!
app.MapControllers();
app.UseAuthentication();
app.UseAuthorization();
✅ The Good Way (Correct Order)
var app = builder.Build();
// GOOD: The standard, secure order of middleware
app.UseExceptionHandler("/error"); // 1. Catch exceptions globally
app.UseHttpsRedirection(); // 2. Force HTTPS
app.UseRouting(); // 3. Match URL to endpoint
app.UseAuthentication(); // 4. Identify the user
app.UseAuthorization(); // 5. Verify permissions
app.MapControllers(); // 6. Execute business logic
Writing Custom Middleware
While ASP.NET Core comes with dozens of built-in middlewares, writing your own is incredibly powerful. Let's write a middleware that injects a X-Correlation-Id into every request for distributed tracing.
❌ The Bad Way (Inline Middleware)
Writing complex logic directly in Program.cs makes your startup file unreadable.
// BAD: Cluttering Program.cs
app.Use(async (context, next) =>
{
var correlationId = Guid.NewGuid().ToString();
context.Response.Headers.Append("X-Correlation-Id", correlationId);
await next();
});
✅ The Good Way (Strongly Typed Middleware)
Encapsulate the logic in its own class. This makes it highly testable and keeps Program.cs clean.
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 1. Logic BEFORE the next middleware
var correlationId = Guid.NewGuid().ToString();
context.Response.Headers.Append("X-Correlation-Id", correlationId);
// 2. Call the next middleware in the pipeline
await _next(context);
// 3. Logic AFTER the next middleware (if any)
}
}
// Extension method for clean registration
public static class CorrelationIdMiddlewareExtensions
{
public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CorrelationIdMiddleware>();
}
}
Now, in your Program.cs, you simply call:
app.UseCorrelationId();
Summary
Mastering middleware is the key to mastering ASP.NET Core. By extracting cross-cutting concerns (like logging, telemetry, and error handling) out of your controllers and into middleware, your application architecture remains incredibly clean and scalable.