ASP.NET Core Middleware Order Cheat Sheet
The ASP.NET Core pipeline order to memorise, the rule behind each position (exceptions, routing, CORS, auth, caching), and a runnable demo of in/out order, short-circuiting and exception capture.
Learning outcome
By the end of this cheat sheet you should be able to write a typical ASP.NET Core pipeline in the right order from memory, explain why each piece sits where it does, and predict what happens when a middleware short-circuits or an endpoint throws. For the full narrative walk-through of one request, read How an ASP.NET Core Request Actually Flows. This sheet is the one-page version you revise from.
Intuition
The pipeline is a set of nested function calls. Middleware runs in registration order on the way in and in reverse order on the way out. Each component can:
- do work before calling
next, - call
next(pass the request on), - do work after
nextreturns, or - not call
nextat all. This is called short-circuiting, and the middleware becomes terminal.
So order controls three things: what a middleware can see (route, user, culture), what it can catch (exceptions from later components), and what it can skip (static files never reach auth).
Quick reference: a typical API/MVC order
var app = builder.Build();
app.UseForwardedHeaders(); // behind a proxy: first, so scheme/host/IP are correct for everyone after
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage(); // WebApplication adds this automatically in Development
else
{
app.UseExceptionHandler("/error"); // early: catches exceptions from everything registered after it
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles(); // here, matching files short-circuit before authorization
app.UseRouting(); // selects the endpoint; later middleware can read it
app.UseRateLimiter(); // after UseRouting when using endpoint-specific limiters
app.UseRequestLocalization(); // before anything culture-sensitive
app.UseCors(); // after UseRouting, before auth and response caching
app.UseAuthentication(); // sets HttpContext.User; does not reject anonymous requests
app.UseAuthorization(); // enforces policies on the selected endpoint
app.UseAntiforgery(); // after auth, before endpoints
app.UseResponseCaching(); // or UseOutputCache(), after CORS/auth
app.MapControllers(); // endpoints run at the end of the pipeline
app.Run();You won't use every line in every app. What you must get right is the relative order.
The ordering rules worth memorising
| Rule | Why |
|---|---|
| Exception handler (or developer exception page) first | It can only catch exceptions thrown after it in the pipeline |
| Forwarded headers before anything that reads scheme, host or client IP | HTTPS redirection, HSTS, auth redirects and logging all need the real values |
UseHttpsRedirection before anything that consumes the URL | Redirect before doing real work |
| Static files early | Cheap short-circuit. Files served this way are public: there are no authorization checks |
UseRouting before middleware that needs the endpoint | Auth, CORS, rate-limit and output-cache policies are read from endpoint metadata |
UseCors → UseAuthentication → UseAuthorization | Documented required order; CORS preflight must be answered before auth rejects it |
UseAuthorization immediately after UseAuthentication | Authorization needs HttpContext.User |
UseAntiforgery after authentication and authorization | Documented placement |
UseCors before UseResponseCaching | So cached responses also carry CORS headers |
UseRouting, UseCors, UseAuthentication, UseAuthorization before UseOutputCache | Output cache policies depend on them |
UseRequestTimeouts after UseExceptionHandler, UseDeveloperExceptionPage and UseRouting | Documented placement |
UseRateLimiter after UseRouting when you use [EnableRateLimiting] or RequireRateLimiting | Endpoint metadata is needed; global-only limiters may run before routing |
UseSession after cookie policy and before MVC/Razor Pages | Session uses cookies |
What WebApplication adds for you
With minimal hosting, if you don't call them yourself:
UseDeveloperExceptionPageis added first in Development.UseRoutingis added next (second) if endpoints are mapped, so routing runs at the start of the pipeline.UseAuthentication/UseAuthorizationare added right after routing if their services are registered.UseEndpointsis added at the end.
The catch: if you call UseCors(), you also need to call UseAuthentication() and UseAuthorization() explicitly, after it. Otherwise the auto-added ones run before your CORS middleware.
Use, Run, Map at a glance
| API | Behaviour |
|---|---|
app.Use(...) | Gets next, so it can pass the request on or short-circuit |
app.Run(...) | Terminal. No next; anything registered after it never runs |
app.Map("/path", ...) | Branches on path prefix. The matched segment moves from Path to PathBase |
app.MapWhen(pred, ...) | Branches on a predicate. Does not rejoin the main pipeline |
app.UseWhen(pred, ...) | Conditional branch that rejoins the main pipeline unless it short-circuits |
Failure modes
UseAuthorizationbeforeUseRouting: authorization can't see endpoint metadata, so[Authorize]policies don't apply as intended.UseExceptionHandlerregistered late: exceptions from earlier middleware bypass it and return a bare 500.- Changing headers or the status code after
next()when the response has already started throws. Checkcontext.Response.HasStarted. - Expecting
UseAuthenticationto return 401. Authentication only identifies the user; authorization (or the endpoint) rejects. - Putting secrets under
wwwrootand assuming auth protects them. Static files short-circuit before auth. - Injecting a scoped service into a conventional middleware constructor. The middleware is created once, so the dependency is captured. Inject it into
InvokeAsyncinstead.
Interview drill
- Why must the exception handler be registered first?
- A request for
/css/site.cssnever reaches authorization. Why, and is that a problem? - Where does
UseCorsgo, and what breaks if it comes after authorization? - What is the difference between
Map,MapWhenandUseWhen? - Which middleware short-circuits by design? (Static files, health checks, endpoint execution for matched routes, CORS preflight.)
- When can
UseRateLimitergo beforeUseRouting?
Revision checklist
- Can I write the reference pipeline above from memory?
- Can I explain "in order on the way in, reverse order on the way out"?
- Do I know which middleware needs
UseRoutingfirst? - Can I explain what
WebApplicationadds automatically, and when I must call auth explicitly? - Can I describe a short-circuit and what still runs afterwards?
Worked executable example
Two inline middlewares log on the way in and out. A logs in, B logs in, the endpoint runs, then B and A log out in reverse order. /blocked is short-circuited by B, so the endpoint never runs but A still logs out. /boom shows that A catches an exception thrown by a later component. The app runs in memory with TestServer, so no port is opened.
Run this example
Use the .NET 10 SDK. In a new empty folder, save the project below as Example.csproj and the listing as Program.cs. Run dotnet run --project Example.csproj.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.0" />
</ItemGroup>
</Project>Program.cs
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
var log = new List<string>();
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseTestServer();
var app = builder.Build();
// Outermost: catches exceptions thrown by everything registered after it.
app.Use(async (context, next) =>
{
log.Add("A in");
try { await next(context); }
catch (InvalidOperationException)
{
log.Add("A caught");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
log.Add("A out");
});
app.Use(async (context, next) =>
{
log.Add("B in");
if (context.Request.Path == "/blocked")
{
context.Response.StatusCode = StatusCodes.Status403Forbidden; // short-circuit: next is not called
log.Add("B short-circuit");
return;
}
await next(context);
log.Add("B out");
});
app.MapGet("/ok", () => { log.Add("endpoint"); return "ok"; });
app.MapGet("/boom", string () => { log.Add("endpoint throws"); throw new InvalidOperationException(); });
await app.StartAsync();
var client = app.GetTestClient();
foreach (var path in new[] { "/ok", "/blocked", "/boom" })
{
log.Clear();
var response = await client.GetAsync(path);
Console.WriteLine($"{path} -> {(int)response.StatusCode}: {string.Join(" | ", log)}");
}
await app.StopAsync();Expected output
The host may also print its own startup log lines. The three request lines are:
/ok -> 200: A in | B in | endpoint | B out | A out /blocked -> 403: A in | B in | B short-circuit | A out /boom -> 500: A in | B in | endpoint throws | A caught | A out
Related on TechInterviewNotes
- How an ASP.NET Core Request Actually Flows (full article on the same topic)
- ASP.NET Core Validation and Problem Details Companion
- OAuth, OIDC, JWT and Cookie Security: A .NET Interview Decision Sheet
- Building ASP.NET Core APIs with Validation and Problem Details (Learn C# and .NET 10, lesson 23)
- Rate Limiting, Backpressure, and Overload Protection
Sources
Microsoft Learn: ASP.NET Core middleware (order, built-in middleware table)
Microsoft Learn: Write custom ASP.NET Core middleware
Microsoft Learn: Routing in ASP.NET Core
Microsoft Learn: Configure ASP.NET Core to work with proxy servers and load balancers
Microsoft Learn: Enable Cross-Origin Requests (CORS) in ASP.NET Core