The ASP.NET Core middleware pipeline doesn’t just process requests; it is the request processing, a chain of components that each have a chance to act on an incoming HTTP request.
Imagine a conveyor belt in a factory. Each station on the belt does something to the product. Some add parts, some inspect, some polish. The ASP.NET Core pipeline is exactly like this, but for HTTP requests.
Here’s a simplified view of a request hitting the pipeline:
// Startup.cs (simplified)
public void Configure(IApplicationBuilder app)
{
app.UseRouting(); // Station 1: Figure out where this request is going
app.UseAuthentication(); // Station 2: Who is this user?
app.UseAuthorization(); // Station 3: Is this user allowed to do this?
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/hello", async context =>
{
await context.Response.WriteAsync("Hello, World!"); // The final destination
});
});
}
When a request for /hello arrives:
UseRoutinglooks at the URL (/hello) and matches it to an endpoint. It adds information toHttpContext.Request.RouteValuesandHttpContext.Request.HttpContext.GetEndpoint().UseAuthenticationchecks if there’s an authenticated user. If not, it might redirect or add aClaimsPrincipal.Currentto the context.UseAuthorizationchecks if the authenticated user has the necessary permissions for the matched endpoint.- If all checks pass, the request reaches the endpoint handler (the
MapGetdelegate), which writes "Hello, World!" to the response.
This sequential execution is key. Each middleware has the opportunity to:
- Perform work before calling the next middleware in the pipeline (
await _next(context);). - Perform work after the next middleware has completed its work (code after
await _next(context);). - Short-circuit the pipeline by not calling
_next(context);at all, directly writing a response.
The problem this solves is the chaotic mess of monolithic request handlers. Instead of one giant if/else block for every conceivable request type and processing step, you get a clean, modular, and extensible pipeline. You can add, remove, or reorder middleware easily.
Let’s look at a custom middleware example. Suppose we want to log the duration of each request.
// Custom middleware class
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
public RequestTimingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var startTime = DateTime.UtcNow;
await _next(context); // Pass control to the next middleware
var endTime = DateTime.UtcNow;
var duration = endTime - startTime;
// Log the duration (in a real app, this would go to a logger)
Console.WriteLine($"Request {context.Request.Path} took {duration.TotalMilliseconds:F2}ms");
}
}
// In Startup.cs Configure method:
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<RequestTimingMiddleware>(); // Add our custom middleware
// ... other middleware ...
}
When a request comes in, RequestTimingMiddleware records the start time, calls _next (which might be UseRouting, UseAuthentication, or another middleware), and after that entire chain completes, it records the end time and calculates the duration. The InvokeAsync method is the core logic, and the RequestDelegate next in the constructor is how it gets a reference to the rest of the pipeline.
The most surprising thing is how UseRouting and UseEndpoints work together. UseRouting doesn’t actually execute the endpoint handler; it just finds it and attaches it to the HttpContext. The actual execution of the endpoint handler happens later, often after UseAuthentication and UseAuthorization have done their jobs, and it’s triggered by the framework’s internal routing logic that recognizes an endpoint has been selected.
Consider this slightly more complex pipeline:
app.UseStaticFiles(); // Serve static files (images, CSS) first
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages(); // For Razor Pages
endpoints.MapControllers(); // For API controllers
endpoints.MapGet("/hello", async context =>
{
await context.Response.WriteAsync("Hello from minimal API!");
});
});
If a request comes in for /images/logo.png, UseStaticFiles will likely handle it directly by finding the file and writing its content to the response. It won’t even call _next, thus short-circuiting the rest of the pipeline. The request never reaches UseAuthentication or UseEndpoints. If the request is for /hello, UseStaticFiles won’t find anything, will call _next, and it will proceed down the chain until MapGet handles it.
The precise order of middleware matters immensely. For instance, you must call UseRouting before UseEndpoints so that endpoints are available when UseEndpoints is processed. Similarly, UseAuthentication and UseAuthorization should typically come after UseRouting so they can inspect the authenticated identity and authorization policies associated with the selected endpoint.
The actual processing of the request is managed by the IApplicationBuilder interface and implemented by ApplicationBuilder. When you call app.Use...(), you’re essentially adding delegates to a list that will eventually be compiled into a single RequestDelegate – the final pipeline function. The Build() method on IApplicationBuilder takes this list and constructs the executable RequestDelegate.
The true power lies in the RequestDelegate itself. Each middleware essentially wraps the RequestDelegate it receives from the previous middleware, and then passes its own RequestDelegate (which includes the next middleware) to the subsequent one. This creates the chain.
The next thing you’ll likely encounter is how to manage the complexity of multiple distinct pipelines or how to conditionally execute middleware based on request characteristics.