The ASP.NET Core Middleware Pipeline Checklist for .NET Teams

The ASP.NET Core middleware pipeline is one of the highest-leverage configuration decisions your team makes at startup. Get the ordering right, and your application is fast, secure, and easy to debug. Get it wrong, and you quietly ship authorization bypasses, broken error handling, and performance regressions that only surface under production load.
This checklist gives your team a concrete, ordered reference for configuring the middleware pipeline in any ASP.NET Core application โ whether you are working on a minimal API, a controller-based Web API, or a modular monolith. If you want to see the middleware pipeline wired correctly inside a complete, production-grade ASP.NET Core API โ alongside CQRS, EF Core, validation, and JWT auth โ the Zero to Production course covers the full Program.cs setup in context, so everything is connected from day one.
The full reference implementation โ including annotated Program.cs files for both minimal and controller-based APIs, custom middleware samples, and edge-case handling โ is available on Patreon, with the source code structured to map directly to what enterprise teams actually ship.
Why Middleware Ordering Matters More Than You Think
Middleware in ASP.NET Core executes as a chain. Each component runs in the order it is registered, processes the request, passes control to the next component, and then runs again on the way back through the response. This bidirectional flow means that a middleware registered after another can never affect how that earlier middleware processes the request โ only how it processes the response.
This has real security and correctness consequences:
Exception handlers registered too late will miss exceptions thrown by earlier middleware
Authentication registered after routing will fail silently on some routes
Rate limiting applied before routing cannot partition by endpoint
The checklist below follows the recommended registration order for a production ASP.NET Core API. Each item includes the reasoning, so your team knows why it goes there โ not just that it does.
The 12-Point ASP.NET Core Middleware Pipeline Checklist
โ 1. Exception Handling Goes First โ Always
UseExceptionHandler() (or a custom IExceptionHandler implementation for .NET 8+) must be the first middleware registered in the pipeline. It can only catch exceptions thrown by middleware that runs after it. Placing it anywhere else creates a silent blind spot.
In development, UseDeveloperExceptionPage() can replace UseExceptionHandler() to surface stack traces โ but never ship it to production.
Why it matters: A middleware registered at position 5 cannot catch an unhandled exception thrown at position 3. The exception propagates past the exception handler entirely.
For a detailed breakdown of how IExceptionHandler differs from traditional exception middleware and when to use each, see 7 Common ASP.NET Core Middleware Mistakes and How to Fix Them.
โ 2. HSTS and HTTPS Redirection Come Second
UseHsts() and UseHttpsRedirection() belong near the top, after exception handling but before any request-processing middleware. HSTS instructs browsers to only connect over HTTPS for a defined period. HTTPS redirection catches plain HTTP requests and issues a 301 before anything else has a chance to read request data over an unencrypted channel.
Skip UseHsts() in development (it persists in the browser and breaks local HTTP testing). The standard pattern uses an environment check: call UseHsts() only when app.Environment.IsProduction().
โ 3. Static Files Before Routing and Auth
UseStaticFiles() short-circuits the pipeline for static asset requests. There is no point running routing, authentication, or authorization for a request that just wants site.css. Register it before the routing middleware to keep those requests cheap.
If your API serves no static files, skip this entirely โ unnecessary middleware registrations add measurable overhead at scale.
โ 4. Routing Before Auth and Rate Limiting
UseRouting() must be registered before any middleware that needs to know which endpoint is being targeted. This includes authentication, authorization, rate limiting, CORS, and output caching when you want endpoint-aware policies.
Without routing resolved, middleware like UseRateLimiter() cannot apply per-endpoint policies โ it falls back to global policies only, which is usually not what you want.
Note: In .NET 6 and later, UseRouting() is often called implicitly when you call app.MapControllers() or app.MapGet(...). If you rely on implicit routing, be aware that any middleware registered before those calls runs before the route is resolved.
โ 5. CORS Middleware Before Authentication
UseCors() must be registered before UseAuthentication() and UseAuthorization(). A preflight OPTIONS request from a browser will not carry auth credentials โ if your auth middleware runs first and rejects the unauthenticated OPTIONS request with 401, the CORS handshake fails before the browser even sends the real request.
Register the default CORS policy with UseCors() here, or reference a named policy if you have multiple policies configured. Endpoint-level [EnableCors("PolicyName")] attributes still need the global middleware registered in the pipeline.
โ 6. Authentication Before Authorization โ Always
UseAuthentication() populates HttpContext.User from the incoming token or cookie. UseAuthorization() reads HttpContext.User to evaluate policies and roles. If they are swapped, every request arrives at the authorization check with an unauthenticated principal, and [Authorize] attributes silently fail.
This ordering mistake is one of the most common sources of "everything returns 401" bug reports on Stack Overflow. The ASP.NET Core docs are explicit about this ordering, but it is easy to miss in a fast-growing Program.cs.
โ 7. Rate Limiting After Routing, After Auth (Usually)
UseRateLimiter() should be registered after UseRouting() and typically after UseAuthentication(). This allows you to partition limits by authenticated user identity (e.g., per-user or per-subscription-tier policies) rather than just by IP address โ which is far more meaningful for most APIs.
If you only need IP-based rate limiting and want it as a pure traffic filter before any app logic runs, you can move it earlier โ but you lose the ability to read user claims for partitioning.
โ 8. Output Caching After Auth, Before Endpoint Middleware
UseOutputCaching() should run after UseAuthentication() and UseAuthorization(), so that cached responses are only served for requests that have already been authorized. Caching authenticated responses before authorization is checked can serve one user's data to another.
Tag-based invalidation, vary-by-query, and vary-by-header policies should be configured at registration time using AddOutputCache() in the service collection, then referenced by name at the endpoint level via [OutputCache(PolicyName = "...")].
โ 9. Custom Middleware in the Right Place
Custom middleware components โ correlation ID injection, request logging, tenant resolution, feature flag evaluation โ need to be placed deliberately:
Correlation ID / request context: Register early, after exception handling, so the correlation ID is available in all subsequent log entries including error logs
Tenant resolution: Register after routing (so the route is known) but before any data access middleware that needs the tenant context
Request/response logging: Register early but after exception handling, so the logger captures both successful and error responses
Feature flags: Register after routing and auth if the flag check is per-user; register before routing if it is a kill-switch for the entire endpoint
The rule: place custom middleware where it can see everything it needs from earlier middleware and affect everything that runs after it.
โ 10. Map Endpoints Last
app.MapControllers(), app.MapGet(...), app.MapHub(...), and similar calls should come at the end of the pipeline configuration. These define the terminal middleware โ the actual endpoints that handle the request. Everything registered before them acts as a pipeline gate or cross-cutting concern.
Adding a middleware after app.MapControllers() has no effect on controller requests because the request has already been handled before the middleware has a chance to run on the inbound path.
โ
11. Review Your Program.cs After Every Major Dependency Addition
Every time your team adds a new NuGet package that registers middleware (rate limiting libraries, telemetry SDKs, feature flag providers, API gateway SDKs), the package documentation may specify a required registration order. Blindly appending app.UseNewThing() at the bottom is a common source of subtle bugs.
Make it a habit to review the full middleware registration sequence in code review. If your team uses a code review checklist, add middleware ordering as a line item.
โ 12. Audit Against the Recommended Pipeline for Your Scenario
Microsoft publishes a canonical recommended middleware order for different application types. The most common one for Web APIs is:
UseExceptionHandler/UseDeveloperExceptionPageUseHstsUseHttpsRedirectionUseStaticFilesUseRoutingUseCorsUseAuthenticationUseAuthorizationUseRateLimiterUseOutputCachingMapControllers/MapGet/ endpoint mapping
Compare your Program.cs against this sequence regularly โ especially after refactoring or upgrading major framework versions. For a broader view of what a production-ready ASP.NET Core API needs beyond middleware, see The 12-Point ASP.NET Core Production Readiness Checklist for .NET Teams.
The Items Teams Get Wrong Most Often
Based on the patterns we see repeatedly, these three items cause the most production incidents:
Exception handler registered too late โ Registering UseExceptionHandler() after authentication means auth exceptions (like invalid token signatures or expired tokens from the JWT bearer handler) propagate unhandled and return a plain 500 with no ProblemDetails body.
CORS registered after authentication โ Browser preflight requests fail with 401 before the CORS handshake completes, causing the client to see a confusing CORS error when the real issue is middleware order.
Rate limiting placed before routing โ IP-based limits work, but per-user partitioning silently falls back to anonymous handling because the user identity is not yet available. The limits appear to apply, but the partitioning logic never fires.
What About IMiddleware vs RequestDelegate?
If your team writes custom middleware, prefer the IMiddleware interface (registered with AddTransient<T>() in the service collection) over the convention-based RequestDelegate approach for any middleware that takes dependencies. IMiddleware gets its dependencies from DI on each request, making it scoped-safe. Convention-based middleware receives its dependencies in the constructor, which means they are resolved once at startup โ creating the classic captive dependency problem for scoped services.
For a complete reference on the idempotency middleware pattern in ASP.NET Core โ including the full source code โ the dotnet-api-idempotency-middleware repo shows how a production-grade custom middleware handles dependency injection, request body hashing, and distributed cache integration.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
FAQ
What is the correct middleware order in ASP.NET Core?
The recommended order for a Web API is: exception handling first, then HSTS and HTTPS redirection, static files, routing, CORS, authentication, authorization, rate limiting, output caching, and finally endpoint mapping. This order ensures each component has access to the context it needs and that security gates run before business logic.
Why must UseAuthentication come before UseAuthorization?
UseAuthentication populates HttpContext.User by validating the incoming token or cookie. UseAuthorization reads HttpContext.User to evaluate policies. If authorization runs first, it reads an empty or unauthenticated principal and rejects the request with 401, regardless of whether the credentials in the request are valid.
Why does CORS need to be registered before authentication?
Browser preflight (OPTIONS) requests do not carry credentials. If authentication middleware runs before CORS and rejects the unauthenticated OPTIONS request, the browser never receives the CORS headers it needs to proceed, causing a CORS error on the client โ even though the actual request would have been authorised.
Can I register rate limiting before authentication in ASP.NET Core?
You can, but with a trade-off. Rate limiting before authentication can only partition by IP address or other request properties. If you need per-user or per-subscription-tier limits, rate limiting must run after authentication so it can read user identity from HttpContext.User.
Does middleware order matter for output caching?
Yes. Output caching should run after authentication and authorization to avoid serving a cached authenticated response to a different user. If caching runs before the auth check, a response cached for user A could be returned to user B on the next matching request.
What happens if I register a middleware after MapControllers()?
Middleware registered after MapControllers() will never execute on the inbound request path for controller actions, because MapControllers() defines the terminal middleware. The request is handled before the later-registered middleware runs inbound. Any code you intended to run as pre-processing will be silently skipped.
How do I debug middleware ordering issues in development?
Use the Microsoft.AspNetCore.MiddlewareAnalysis NuGet package in development. It logs each middleware component in pipeline order with its execution timing, making it easy to see both the ordering and where a request is short-circuited. Pair this with structured logging using Serilog's UseSerilogRequestLogging() to capture the full request lifecycle in your development logs.






