How it works
The explanation, with examples you can run.
Watching the order
#:sdk Microsoft.NET.Sdk.Webvar app = WebApplication.CreateBuilder(args).Build(); app.Use(async (context, next) =>{ Console.WriteLine($"A in {context.Request.Path}"); await next(context); Console.WriteLine($"A out {context.Response.StatusCode}");});app.Use(async (context, next) =>{ if (context.Request.Path == "/blocked") { context.Response.StatusCode = 403; Console.WriteLine("B short-circuits"); return; // next is never called } Console.WriteLine("B in"); await next(context); Console.WriteLine("B out");});app.MapGet("/hello", () => { Console.WriteLine("endpoint"); return "hi"; });app.Run("http://127.0.0.1:5005");Requesting /hello, /blocked and /nope with curl, the .NET 10 app printed (framework log lines removed):
A in /helloB inendpointB outA out 200A in /blockedB short-circuitsA out 403A in /nopeB inB outA out 404The pipeline is nested calls, like layers of an onion: A wraps B, which wraps routing and the endpoint. /nope matched no endpoint, so the request fell off the end of the pipeline and the default 404 was returned on the way out.
A typical order
app.UseExceptionHandler("/error"); // first: catches exceptions from everything belowapp.UseHsts();app.UseHttpsRedirection();app.UseStaticFiles(); // can short-circuit before auth for public filesapp.UseRouting(); // chooses the endpoint (added implicitly if omitted)app.UseCors();app.UseAuthentication(); // who are you? sets HttpContext.Userapp.UseAuthorization(); // may you call this endpoint?app.UseRateLimiter();app.MapControllers(); // endpointsEach line can only see what the lines above it have done. Put the exception handler later and exceptions from earlier middleware escape it. Put authorization before authentication and every user looks anonymous. Because routing runs before authorization, the authorization middleware can read the [Authorize] metadata of the endpoint that is about to run.
Kestrel and what sits in front of it
Kestrel is a fast, cross-platform server that handles HTTP/1.1, HTTP/2 and HTTP/3 (over QUIC) and TLS. It is fine to expose directly, but production deployments often put a reverse proxy or load balancer in front (Nginx, YARP, a cloud load balancer, a Kubernetes ingress) for TLS termination, multiple apps on one port, or request buffering. Then the app sees the proxy's address, and you need the forwarded headers middleware (UseForwardedHeaders, early in the pipeline) to restore the original client IP and scheme.
Every request runs on a thread-pool thread without a synchronization context, so handlers should be async from top to bottom and pass HttpContext.RequestAborted to their I/O calls, so a client that disconnects stops the work.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.