The Middleware Pipeline

Middleware components wrap each other in registration order, so order is behaviour, and any one of them can answer a request itself.

SyntaxIApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, RequestDelegate, Task> middleware)

Middleware is a component that sits in the path of every request. Each one receives the request and a reference to the next component in line. It may do work before calling the next one, after it returns, or it may not call it at all.

The shape is nesting, not a queue. The first middleware registered is the outermost; it wraps everything after it. A request travels inward through each layer to the endpoint, and the response travels back outward through the same layers in reverse. Code you write before await next(context) runs on the way in; code after it runs on the way out.

Order is behaviour

Because the order of registration is the order of nesting, moving a line changes what the program does. Exception handling must be outermost or it cannot catch what happens inside. Authorisation must come after authentication, because it needs to know who the caller is. These are not style choices.

Short-circuiting

A middleware that does not call next has short-circuited the pipeline: it writes the response itself and nothing further runs. That is how a rate limiter returns 429 without touching your endpoint. When a component never intends to call next, register it with app.Run(handler) instead, which is the terminal form.

Two overloads

Prefer Use(Func<HttpContext, RequestDelegate, Task>), where next takes the context. The older Use(Func<HttpContext, Func<Task>, Task>), where next takes nothing, still works and still appears in existing code; the documentation recommends the first for better performance.

Example

Example · csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Registered first, so this one wraps everything below it.
app.Use(async (context, next) =>
{
    // On the way in. Headers can still be changed here.
    context.Response.Headers["X-Stage"] = "outer";

    await next(context);

    // On the way out, after the inner layers have finished.
    app.Logger.LogInformation("outer finished with {Status}", context.Response.StatusCode);
});

// This one answers some requests itself and never calls next.
app.Use(async (context, next) =>
{
    if (context.Request.Headers.ContainsKey("X-Blocked"))
    {
        context.Response.StatusCode = 403;
        await context.Response.WriteAsync("blocked");
        return;
    }

    await next(context);
});

app.MapGet("/", () => "reached the endpoint");

app.Run();

When to use it

  • Add a request identifier header to every response in one place.
  • Reject requests that fail a check before they reach any endpoint.
  • Measure how long the whole pipeline takes, including serialisation.
  • Work out why an exception handler is not catching a particular failure.

More examples

The preferred overload next to the older one

Both forms do the same job, and you will meet the second one in any codebase older than a few years. Knowing that next() and next(context) are the same next saves a confusing minute when copying a snippet. Write new middleware with the first form.

Example · csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Preferred: Func<HttpContext, RequestDelegate, Task>. next takes the
// context, so nothing extra is allocated per request to capture it.
app.Use(async (context, next) =>
{
    await next(context);
});

// Older: Func<HttpContext, Func<Task>, Task>. next takes no arguments.
// The explicit parameter types are what select this overload.
app.Use(async (HttpContext context, Func<Task> next) =>
{
    await next();
});

app.MapGet("/", () => "ok");

app.Run();

Middleware as a class

An inline lambda is right for four lines. Past that, a class gives the component a name, a file and somewhere to put tests. The constructor is called once when the pipeline is built, not per request, so the RequestDelegate it stores is the fixed next layer. Note the file-scoped namespace and the _camelCase private field, which are the current conventions.

Example · csharp
// ServerHeaderMiddleware.cs
namespace Shop.Api;

public class ServerHeaderMiddleware
{
    private readonly RequestDelegate _next;

    // The framework hands the next component in as a RequestDelegate.
    public ServerHeaderMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    // InvokeAsync is the method name the framework looks for by convention.
    public async Task InvokeAsync(HttpContext context)
    {
        context.Response.Headers["X-Served-By"] = "shop-api";
        await _next(context);
    }
}

// In Program.cs:
//     app.UseMiddleware<ServerHeaderMiddleware>();

A realistic order, annotated

This is the skeleton most production services share. Move UseExceptionHandler below UseHttpsRedirection and a failure in the redirect becomes an unhandled 500 with no body. Move authorisation above routing and it has no endpoint to inspect. When a middleware seems to do nothing, check its position before checking its code.

Example · csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Each line wraps everything written after it.

// 1. Outermost, so it can catch failures from every layer below.
app.UseExceptionHandler("/error");

// 2. Tells browsers to use https for later requests to this host.
app.UseHsts();

// 3. Redirects this request from http to https.
app.UseHttpsRedirection();

// 4. Decides which endpoint matches. Added for you if omitted, but naming
//    it lets middleware below know which endpoint was chosen.
app.UseRouting();

// 5. Authentication and then authorisation belong here, once configured:
//        app.UseAuthentication();
//        app.UseAuthorization();

app.MapGet("/error", () => TypedResults.Problem(detail: "something failed"));
app.MapGet("/", () => "ok");

app.Run();

Discussion

  • Be the first to comment on this lesson.