The middleware pipeline

ASP.NET Core · Essentials · 12 min

Exception handling, HTTPS, CORS, auth and static files are all middleware; order bugs are common and subtle.

Every request passes through a pipeline of middleware components. Each one can inspect the request, call the next component, and act on the response on the way back, or short-circuit by writing a response without calling next.

app.Use(async (context, next) =>
{
    // before
    await next(context);
    // after: the response has been produced
});

app.Run(...) adds terminal middleware, and endpoints (MapGet etc.) are executed by the routing middleware at the end of the pipeline.

Order matters, because middleware runs in the order you add it. A typical order:

1. UseExceptionHandler (first, so it catches everything after it) 2. UseHsts / UseHttpsRedirection 3. UseStaticFiles (can short-circuit for files) 4. UseRouting (added implicitly by WebApplication if you don't) 5. UseCors 6. UseAuthentication then UseAuthorization 7. Endpoints

Putting UseAuthorization before UseAuthentication, or CORS after auth, are classic mistakes.

Once the response has started (headers sent), you can't change the status code or headers; check context.Response.HasStarted. For reusable middleware, write a class with an InvokeAsync(HttpContext) method and register it with app.UseMiddleware<T>(). Middleware classes are singletons in effect, so take scoped services as InvokeAsync parameters rather than constructor parameters.

Example

using System.Diagnostics;

var app = WebApplication.Create(args);

app.Use(async (ctx, next) =>                 // timing middleware
{
    var sw = Stopwatch.StartNew();
    ctx.Response.OnStarting(() =>
    {
        ctx.Response.Headers["X-Elapsed-Ms"] = sw.ElapsedMilliseconds.ToString();
        return Task.CompletedTask;
    });
    await next(ctx);
});

app.Use(async (ctx, next) =>                 // short-circuit middleware
{
    if (!ctx.Request.Headers.ContainsKey("X-Block")) { await next(ctx); return; }
    ctx.Response.StatusCode = 403;           // next is never called
    await ctx.Response.WriteAsync("blocked");
});
app.MapGet("/", () => "hello");
app.Run();
// curl -i /             -> 200 hello, header X-Elapsed-Ms: 0
// curl -H "X-Block: 1" / -> 403 blocked

Remember

  • Middleware runs in registration order, then in reverse on the way out
  • Call next to continue; skip it to short-circuit
  • Exception handler first, authentication before authorization
  • Headers can't change after the response has started

Watch out for

  • Registering UseAuthorization before UseAuthentication, so the user is never authenticated when policies run.

Check yourself

  1. Why is UseExceptionHandler usually registered first?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/