e on the return path. The pipeline executes sequentially on the request path and reverses on the response path. Order is not configurable; it is structural.
Step-by-Step Implementation
-
Initialize the Host Builder
Use the minimal hosting model introduced in .NET 6 and refined in .NET 8. This model consolidates configuration, services, and pipeline registration into Program.cs.
-
Register Services First
All middleware dependencies must be registered in the DI container before pipeline construction. Middleware extension methods like builder.Services.AddAuthentication() configure services, not the pipeline.
-
Build the Application
var app = builder.Build(); creates the WebApplication instance. The pipeline is not yet active.
-
Configure the Environment Pipeline
Development vs production behavior must be isolated. Exception pages should never run in production. HTTPS redirection and HSTS are production-only concerns.
-
Register Middleware in Canonical Order
Execute extension methods in the exact sequence dictated by framework mechanics. Each call appends a delegate to the internal pipeline chain.
-
Map Endpoints
app.MapControllers(), app.MapRazorPages(), or app.MapGet() must be the final pipeline stage. They consume the request and terminate the chain.
Architecture Decisions and Rationale
The canonical order exists to satisfy three constraints: security boundaries, routing resolution, and error propagation.
- Exception handling must be first because it wraps the entire pipeline. If placed later, exceptions thrown by preceding middleware bypass the handler, resulting in unhandled 500 responses without structured logging or client-friendly error payloads.
- Static files precede routing to avoid unnecessary pipeline execution. Static assets do not require routing, authentication, or CORS evaluation. Placing them first allows the framework to short-circuit and serve files directly.
- Routing must precede authentication and authorization because endpoint metadata (controllers, Razor pages, minimal APIs) is resolved during routing. Authentication policies often depend on endpoint attributes (
[Authorize], [AllowAnonymous]). Running auth before routing forces blanket policy evaluation, breaking attribute-based security.
- CORS must precede authentication because preflight
OPTIONS requests must receive Access-Control-* headers before identity evaluation. If auth runs first, the framework rejects preflight requests with 401, breaking cross-origin clients.
- Endpoints must be last because they are terminal. They match routes, execute handlers, and write responses. Placing middleware after endpoints results in dead code that never executes.
Code Implementation (.NET 8)
var builder = WebApplication.CreateBuilder(args);
// 1. Register services
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
policy.WithOrigins("https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod());
});
var app = builder.Build();
// 2. Environment-specific pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// 3. Routing resolves endpoint metadata
app.UseRouting();
// 4. Security & cross-origin boundaries
app.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization();
// 5. Terminal routing
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
This configuration satisfies all framework constraints. The pipeline executes predictably, security policies align with endpoint metadata, and exceptions are captured before response serialization.
Pitfall Guide
1. Placing UseAuthentication() Before UseRouting()
Impact: Authentication middleware evaluates policies before endpoint metadata is resolved. This forces blanket authentication checks, ignoring [AllowAnonymous] attributes. Requests to public endpoints return 401 Unauthorized.
Fix: Always run UseRouting() before UseAuthentication(). Routing populates HttpContext.GetEndpoint(), which authorization and authentication middleware inspect to apply attribute-based policies.
2. Putting UseStaticFiles() After UseAuthorization()
Impact: Every static asset request triggers identity evaluation. Public images, CSS, and JavaScript files return 401 or 403. Performance degrades due to unnecessary token validation and policy evaluation.
Fix: Place UseStaticFiles() immediately after environment configuration and before routing. Use StaticFileOptions with RequestPath to restrict access to sensitive directories if needed.
3. UseExceptionHandler() After UseCors() or UseRouting()
Impact: Exceptions thrown by downstream middleware bypass the handler. CORS headers are not attached to error responses, causing browser clients to block error payloads. Debugging becomes impossible because stack traces never reach logging infrastructure.
Fix: UseExceptionHandler() must be the first middleware registered. It wraps the entire pipeline and ensures all exceptions, including those from routing and CORS, are captured and formatted.
4. Logging Middleware Positioned After Exception Handling
Impact: Unhandled exceptions are caught by the exception handler and transformed into error responses. Logging middleware never sees the original exception context. Production monitoring loses critical failure signals.
Fix: Register logging middleware before UseExceptionHandler(), or rely on built-in ILogger injection within the exception handler. Framework logging is typically configured via builder.Logging.ClearProviders() and AddApplicationInsights() or AddSerilog(), which operate outside the middleware chain.
5. Misordering UseRouting() and UseEndpoints()
Impact: In older .NET Core versions, UseEndpoints() was separate. In .NET 6+, MapControllers() and MapRazorPages() replace it. Placing endpoint mapping before routing causes route table population failures. The framework cannot match requests to handlers.
Fix: UseRouting() must precede all Map*() calls. The routing middleware builds the EndpointDataSource collection and attaches it to HttpContext. Endpoint mapping consumes this collection.
6. Forgetting Terminal Middleware
Impact: If no endpoint mapping is registered, the pipeline completes without writing a response. Clients receive empty 200 OK responses or timeout. The framework does not automatically return 404.
Fix: Always end the pipeline with app.MapControllers(), app.MapRazorPages(), or app.Run(context => ...). Terminal middleware ensures request lifecycle completion.
7. Overusing Map and MapWhen Without Pipeline Isolation Awareness
Impact: app.Map("/api", branch => branch.UseMiddleware<X>()) creates a separate pipeline branch. Middleware registered in the main pipeline does not execute for branched requests. Developers assume global middleware applies universally, leading to missing CORS, auth, or logging on branched paths.
Fix: Use branching only for path-specific isolation. Duplicate necessary middleware in branches, or use UseWhen for conditional execution within the main pipeline. Document branch boundaries in architecture reviews.
Best Practices from Production Experience
- Validate pipeline order using
IApplicationBuilder inspection in integration tests.
- Use
WebApplication.CreateBuilder() exclusively; avoid legacy Startup.cs patterns.
- Isolate environment-specific middleware in
if (app.Environment.IsDevelopment()) blocks.
- Never register middleware that modifies
HttpContext.Items after routing if downstream components depend on it.
- Use
IHostedService or BackgroundService for non-request-bound work; keep middleware stateless.
- Run pipeline order validation in CI using
Microsoft.AspNetCore.TestHost to simulate cross-origin, authenticated, and static file requests.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Public API with JWT auth | Canonical order + UseCors before UseAuthentication | Preflight requests require CORS headers before identity evaluation | Low (standard configuration) |
| SPA hosting with static files | UseStaticFiles first, then routing, then SPA fallback | Prevents auth overhead on assets; enables client-side routing fallback | Low (performance gain) |
| Multi-tenant API versioning | Branch-first with Map("/v1", ...) and isolated middleware | Prevents version-specific middleware from leaking to other tenants | Medium (maintenance overhead) |
| Health check endpoints | Map("/health", ...) before main pipeline | Short-circuits pipeline for monitoring; avoids auth/routing overhead | Low (infrastructure efficiency) |
| Legacy migration to .NET 8 | Canonical order + UseStatusCodePages for backward compatibility | Replaces legacy app.Use(async (ctx, next) => ...) patterns safely | Medium (refactoring effort) |
Configuration Template
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// Services registration
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
});
builder.Services.AddAuthorization();
builder.Services.AddCors(options =>
{
options.AddPolicy("Production", policy =>
policy.WithOrigins(builder.Configuration["Cors:AllowedOrigins"]?.Split(',') ?? Array.Empty<string>())
.AllowAnyHeader()
.AllowAnyMethod());
});
var app = builder.Build();
// Environment isolation
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// Routing & Security
app.UseRouting();
app.UseCors("Production");
app.UseAuthentication();
app.UseAuthorization();
// Endpoints
app.MapControllers();
app.MapGet("/health", () => Results.Ok("OK"));
app.Run();
Quick Start Guide
- Create a new .NET 8 Web API project using
dotnet new webapi -n MiddlewareOrderDemo. Open Program.cs and verify the default pipeline matches the canonical sequence.
- Add a test controller with
[Authorize] and [AllowAnonymous] attributes. Run the application and request both endpoints. Confirm 401 is returned only for authorized routes.
- Inject a custom middleware that logs request headers. Place it before and after
UseRouting(). Observe how HttpContext.GetEndpoint() is null before routing and populated after.
- Validate CORS preflight by sending an
OPTIONS request from a browser or curl. Verify Access-Control-Allow-Origin headers are present in the response. If missing, confirm UseCors() precedes UseAuthentication().
- Run pipeline integration tests using
WebApplicationFactory<T>. Assert status codes for static files, authenticated routes, and exception paths. Commit the pipeline configuration once all assertions pass.