Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close
    Cheat sheets: all cheat sheets

    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 next returns, or
    • not call next at 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

    C#
    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

    RuleWhy
    Exception handler (or developer exception page) firstIt can only catch exceptions thrown after it in the pipeline
    Forwarded headers before anything that reads scheme, host or client IPHTTPS redirection, HSTS, auth redirects and logging all need the real values
    UseHttpsRedirection before anything that consumes the URLRedirect before doing real work
    Static files earlyCheap short-circuit. Files served this way are public: there are no authorization checks
    UseRouting before middleware that needs the endpointAuth, CORS, rate-limit and output-cache policies are read from endpoint metadata
    UseCors → UseAuthentication → UseAuthorizationDocumented required order; CORS preflight must be answered before auth rejects it
    UseAuthorization immediately after UseAuthenticationAuthorization needs HttpContext.User
    UseAntiforgery after authentication and authorizationDocumented placement
    UseCors before UseResponseCachingSo cached responses also carry CORS headers
    UseRouting, UseCors, UseAuthentication, UseAuthorization before UseOutputCacheOutput cache policies depend on them
    UseRequestTimeouts after UseExceptionHandler, UseDeveloperExceptionPage and UseRoutingDocumented placement
    UseRateLimiter after UseRouting when you use [EnableRateLimiting] or RequireRateLimitingEndpoint metadata is needed; global-only limiters may run before routing
    UseSession after cookie policy and before MVC/Razor PagesSession uses cookies

    What WebApplication adds for you

    With minimal hosting, if you don't call them yourself:

    • UseDeveloperExceptionPage is added first in Development.
    • UseRouting is added next (second) if endpoints are mapped, so routing runs at the start of the pipeline.
    • UseAuthentication / UseAuthorization are added right after routing if their services are registered.
    • UseEndpoints is 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

    APIBehaviour
    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

    • UseAuthorization before UseRouting: authorization can't see endpoint metadata, so [Authorize] policies don't apply as intended.
    • UseExceptionHandler registered 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. Check context.Response.HasStarted.
    • Expecting UseAuthentication to return 401. Authentication only identifies the user; authorization (or the endpoint) rejects.
    • Putting secrets under wwwroot and 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 InvokeAsync instead.

    Interview drill

    1. Why must the exception handler be registered first?
    2. A request for /css/site.css never reaches authorization. Why, and is that a problem?
    3. Where does UseCors go, and what breaks if it comes after authorization?
    4. What is the difference between Map, MapWhen and UseWhen?
    5. Which middleware short-circuits by design? (Static files, health checks, endpoint execution for matched routes, CORS preflight.)
    6. When can UseRateLimiter go before UseRouting?

    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 UseRouting first?
    • Can I explain what WebApplication adds 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.

    XML
    <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

    C#
    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:

    Text
    /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