Request Pipeline and Middleware

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every HTTP request flows through an ordered chain of middleware components. Each one can inspect and modify the request, call the next component, and then inspect and modify the response on the way back out.

The pipeline as a chain of delegates

A middleware is a function that takes the next middleware (RequestDelegate) and returns a RequestDelegate. Calling await next(context) passes control down the chain; not calling it short-circuits, so nothing after it runs.

var app = builder.Build();

app.Use(async (context, next) =>
{
    // runs on the way in
    var start = TimeProvider.System.GetTimestamp();
    await next(context);                 // invoke the rest of the pipeline
    // runs on the way out
    var ms = TimeProvider.System.GetElapsedTime(start).TotalMilliseconds;
    app.Logger.LogInformation("{Path} took {Elapsed} ms", context.Request.Path, ms);
});

app.Run();

Use, Run, Map

  • app.Use(…​) adds a middleware that may call next.

  • app.Run(…​) adds a terminal middleware — it never calls next.

  • app.Map("/admin", branch ⇒ …​) / app.MapWhen(predicate, branch ⇒ …​) fork the pipeline into a branch for matching requests.

app.Map("/health", branch => branch.Run(async ctx => await ctx.Response.WriteAsync("OK")));

app.MapWhen(ctx => ctx.Request.Query.ContainsKey("debug"),
    branch => branch.Use(async (ctx, next) => { /* extra logging */ await next(ctx); }));

Recommended order (register in this sequence):

app.UseExceptionHandler("/error");   // outermost: catches everything below
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();                // or app.MapStaticAssets()
app.UseRouting();
app.UseCors();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.UseOutputCache();
app.MapControllers();                // endpoint execution

Built-in middleware tour

Middleware Purpose

UseExceptionHandler

Convert unhandled exceptions into a clean error response. See Error handling.

UseStaticFiles / MapStaticAssets

Serve files from wwwroot; MapStaticAssets (.NET 9+) adds build-time fingerprinting and compression.

UseRouting + endpoint execution

Match the request to an endpoint, then run it.

UseCors

Apply cross-origin policies. See What is CORS?.

UseAuthentication / UseAuthorization

Establish HttpContext.User, then enforce policies.

UseResponseCompression

Gzip/Brotli-compress responses.

UseResponseCaching / UseOutputCache

HTTP cache headers vs. server-side cached responses. See Performance and caching.

UseRateLimiter

Reject or queue requests above a configured rate.

UseRequestLocalization

Resolve culture from the request.

Writing custom middleware

Inline lambda — for one-off logic (shown above with app.Use).

Convention-based class — a class with a constructor taking RequestDelegate and an InvokeAsync(HttpContext) method. It is instantiated once (singleton); inject scoped services as InvokeAsync parameters, not constructor parameters.

public sealed class RequestIdMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context, ILogger<RequestIdMiddleware> logger)
    {
        context.Response.Headers["X-Request-Id"] = context.TraceIdentifier;
        using (logger.BeginScope("ReqId:{Id}", context.TraceIdentifier))
            await next(context);
    }
}

// a small helper extension keeps Program.cs readable:
public static class RequestIdMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestId(this IApplicationBuilder app)
        => app.UseMiddleware<RequestIdMiddleware>();
}

// registration in Program.cs:
app.UseRequestId();          // or: app.UseMiddleware<RequestIdMiddleware>();

Factory-based — implement IMiddleware and register it in DI (AddTransient / AddScoped); the framework resolves a fresh instance per request, so constructor injection of scoped services is safe.

public sealed class AuditMiddleware(IAuditSink sink) : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        await sink.RecordAsync(context.Request.Path);
        await next(context);
    }
}

builder.Services.AddScoped<AuditMiddleware>();
app.UseMiddleware<AuditMiddleware>();

HttpContext

HttpContext carries everything about the current request:

app.Use(async (context, next) =>
{
    var path   = context.Request.Path;                 // request line + headers + body
    context.Response.StatusCode = StatusCodes.Status202Accepted;
    var user   = context.User;                         // ClaimsPrincipal (after auth)
    context.Items["startedAt"] = DateTimeOffset.UtcNow; // per-request scratch dictionary
    var clock  = context.RequestServices.GetRequiredService<TimeProvider>(); // request-scoped DI
    var feature = context.Features.Get<IHttpConnectionFeature>();            // low-level server features
    await next(context);
});

Read the request body once (it is a forward-only stream); call context.Request.EnableBuffering() first if you need to read it more than once. IHttpContextAccessor exposes the current HttpContext to non-request types, but it uses an AsyncLocal and adds overhead — prefer passing what you need explicitly. See Access HttpContext.

One request through the pipeline

flowchart LR R([Request]) --> EH[UseExceptionHandler] EH --> HR[UseHttpsRedirection] HR --> SF[UseStaticFiles] SF --> RT[UseRouting] RT --> AUTH[UseAuthentication / UseAuthorization] AUTH --> EP[Endpoint] EP --> AUTH AUTH --> RT RT --> SF SF --> HR HR --> EH EH --> RESP([Response]) SF -. file found: short-circuit .-> RESP

Each component wraps the next; the response unwinds back through every component that called next. Static files, when a file matches, write the response and never call routing.