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 callnext. -
app.Run(…)adds a terminal middleware — it never callsnext. -
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
See Middleware order.
Built-in middleware tour
| Middleware | Purpose |
|---|---|
|
Convert unhandled exceptions into a clean error response. See Error handling. |
|
Serve files from |
|
Match the request to an endpoint, then run it. |
|
Apply cross-origin policies. See What is CORS?. |
|
Establish |
|
Gzip/Brotli-compress responses. |
|
HTTP cache headers vs. server-side cached responses. See Performance and caching. |
|
Reject or queue requests above a configured rate. |
|
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
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.