The Middleware Pipeline
Build an ASP.NET Core request pipeline in the right order with Use, Run, Map and UseWhen, and know which pieces WebApplication adds for you.
app.Use(async (context, next) => { /* before */ await next(context); /* after */ });
app.Map("/internal", branch => branch.Run(ctx => ctx.Response.WriteAsync("ok")));Every request in ASP.NET Core flows through one function: a RequestDelegate, which is just Func<HttpContext, Task>. Middleware is a function that receives the next delegate and returns a new one. At start-up the host composes all registered middleware into a single nested delegate, once. Each component can do work, call next(context), do more work as the call returns, or not call next at all and end the request. That nesting is why order matters: the first middleware registered sees the request first and the response last.
Use, Run, Map and friends
| Method | What it does | Rejoins main pipeline? |
|---|---|---|
Use | Adds middleware that may call next | Yes |
Run | Adds terminal middleware; there is no next | Nothing runs after it |
Map("/prefix", ...) | Branches on a path prefix, moves the prefix into PathBase | No |
MapWhen(predicate, ...) | Branches on any condition | No |
UseWhen(predicate, ...) | Runs extra middleware for matching requests | Yes |
Prefer the app.Use(async (context, next) => await next(context)) overload that takes a RequestDelegate; the older Func<Task> form allocates a closure per request.
What WebApplication adds for you
WebApplication wraps your middleware. In Development it puts the developer exception page first. If you mapped endpoints and did not call UseRouting, it adds routing at the very start of your pipeline. If authentication or authorization services are registered and you did not call UseAuthentication/UseAuthorization, it adds them right after routing. It always appends the endpoint middleware at the end, which runs the matched handler. Your own middleware therefore sits between route matching and endpoint execution, and context.GetEndpoint() already returns the matched endpoint inside it.
The standard order
UseForwardedHeaders(when behind a proxy)UseExceptionHandler, thenUseHstsoutside DevelopmentUseHttpsRedirection- Static files (
MapStaticAssetsorUseStaticFiles) UseRoutingUseCorsUseAuthentication,UseAuthorizationUseRateLimiter, request timeouts, output cache- Your own middleware, then endpoints
Teams that call these explicitly get a pipeline that reads top to bottom in the order it runs, which is worth more than the few lines the implicit version saves.
How it fails in production
app.Run(...)at the end swallows every endpoint. WebApplication wires your pipeline in front of the endpoint middleware, so a terminalRunanswers 200 for/orders/7and the handler never executes. UseMapFallbackinstead.- CORS after authorization. A browser preflight has no token, gets 401, and the real call never happens.
- Exception handler registered late. Exceptions thrown by middleware above it become raw 500s with no ProblemDetails body.
- HTTPS redirection before forwarded headers. Behind a TLS-terminating load balancer every request looks like HTTP and redirects forever.
Example
When to use it
- Place UseCors before UseAuthentication so a browser preflight from https://shop.example.com is answered without a bearer token.
- Branch /internal/ping with Map so load balancer probes skip authentication, logging and rate limiting.
- Replace a catch-all app.Run that was hiding every mapped endpoint with MapFallback returning a 404 ProblemDetails.
More examples
The standard order, written out explicitly
Calling every middleware explicitly makes the order visible in code review. <code>UseCors</code> sits after <code>UseRouting</code> so endpoint policies like <code>RequireCors</code> are known, and before authentication so an unauthenticated preflight still succeeds. <code>UseRateLimiter</code> comes after routing so the per-endpoint <code>RequireRateLimiting</code> policy is visible to it. Cookie auth across origins needs <code>AllowCredentials()</code> and a <code>SameSite=None</code> secure cookie; credentials require explicit origins, so <code>AllowAnyOrigin</code> can never be combined with them.
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddCors(options => options.AddPolicy("storefront", policy =>
policy.WithOrigins("https://shop.example.com").AllowAnyHeader().AllowAnyMethod().AllowCredentials()));
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
// The storefront is another site, so the browser only sends the cookie with SameSite=None; Secure.
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
builder.Services.AddAuthorization();
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("orders", limiter =>
{
limiter.PermitLimit = 20;
limiter.Window = TimeSpan.FromSeconds(10);
limiter.QueueLimit = 0;
});
});
var app = builder.Build();
app.UseExceptionHandler();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();
app.MapGet("/orders/{id:int}", (int id) => TypedResults.Ok(new { id, status = "Shipped" }))
.RequireCors("storefront")
.RequireAuthorization()
.RequireRateLimiting("orders");
app.Run();Pitfall: a terminal Run that swallows endpoints
WebApplication places your pipeline in front of the endpoint middleware it appends at the end. A terminal <code>app.Run(handler)</code> never calls <code>next</code>, so matched endpoints never execute and clients get 200 with the fallback body. <code>MapFallback</code> registers a lowest-priority endpoint instead, so routing picks it only when nothing else matched.
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
app.MapGet("/orders/{id:int}", (int id) => TypedResults.Ok(new { id }));
// Wrong: this terminal middleware runs before the implicit endpoint middleware,
// so GET /orders/7 returns "Not here" with status 200.
// app.Run(context => context.Response.WriteAsync("Not here"));
// Right: a fallback endpoint only runs when no other endpoint matched.
app.MapFallback((HttpContext context) =>
TypedResults.Problem(
title: "No such resource",
detail: $"{context.Request.Method} {context.Request.Path} is not part of this API.",
statusCode: StatusCodes.Status404NotFound));
app.Run();UseWhen to buffer bodies only for webhooks
<code>UseWhen</code> adds middleware for matching requests and then continues with the rest of the main pipeline, so the webhook endpoint still runs through routing, auth and the endpoint middleware. <code>MapWhen</code> would branch off permanently and the endpoint would never be reached. Scoping buffering this way keeps ordinary <code>/orders</code> traffic streaming.
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
// Payment webhooks must be verified against the raw body, so only that path pays for buffering.
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/webhooks/payments"),
webhooks => webhooks.Use(async (context, next) =>
{
context.Request.EnableBuffering(bufferThreshold: 64 * 1024);
await next(context);
}));
app.MapPost("/webhooks/payments", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body, leaveOpen: true);
var raw = await reader.ReadToEndAsync();
request.Body.Position = 0;
return TypedResults.Ok(new { received = raw.Length });
});
app.MapPost("/orders", (OrderRequest order) => TypedResults.Created($"/orders/1001", order));
app.Run();
public record OrderRequest(string CustomerId, decimal Total);
Discussion