ASP.NET Core Middleware & Request Pipeline

17 questions found

What is middleware in ASP.NET Core, and how does it form the request pipeline?

Beginner
Middleware are components assembled into a pipeline that handles every incoming HTTP request and outgoing response. Each middleware can perform work before and after calling the next component in the pipeline (via `next()`), or short-circuit the pipeline entirely by not calling next, letting you compose cross-cutting concerns like authentication, logging, and error handling as discrete, reusable, ordered steps.
var app = builder.Build();

app.Use(async (context, next) => {
    Console.WriteLine("Before");
    await next();
    Console.WriteLine("After");
});

app.MapGet("/", () => "Hello World");
app.Run();
Real-world example A request logging middleware wraps the entire pipeline, recording the request path before calling next() and the response status code and elapsed time after next() returns, for every single request the app handles.

Common follow-ups: What's the difference between app.Use and app.Run for terminal middleware?;How do you write middleware as a reusable class instead of an inline lambda?

Global Exception Handling & Middleware;Diagnostics & Performance

Why does the order in which middleware is registered matter so much in ASP.NET Core?

Intermediate
Middleware executes in exactly the order it's registered for the 'incoming' phase, then unwinds in reverse order for the 'outgoing' phase -- so registering exception handling before routing means it can catch errors from routing and everything after it, but registering authentication after authorization would mean requests are checked for permissions before their identity is even established, causing incorrect behavior.
app.UseExceptionHandler("/error");  // must be early to catch errors from everything after
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();  // must come before UseAuthorization
app.UseAuthorization();
app.MapControllers();
Real-world example A team debugging why authorization checks always fail discovers UseAuthorization() was accidentally registered before UseAuthentication(), meaning no user identity was ever established before the permission check ran.

Common follow-ups: What's the recommended standard middleware order for a typical web API?;How does UseRouting and UseEndpoints (or minimal API mapping) fit into this order?

Authentication & Authorization (Identity JWT OAuth);Global Exception Handling & Middleware

How do you write a custom middleware component as a reusable class instead of an inline lambda?

Intermediate
A class-based middleware implements a constructor accepting RequestDelegate next (and typically other injected services), and an InvokeAsync (or Invoke) method containing the actual per-request logic, then calling await next(context) to pass control to the next middleware. It's registered via a custom `app.UseMiddleware<T>()` call or, more idiomatically, an extension method wrapping that call.
public class RequestTimingMiddleware {
    private readonly RequestDelegate _next;
    public RequestTimingMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context) {
        var sw = Stopwatch.StartNew();
        await _next(context);
        Console.WriteLine($"{context.Request.Path} took {sw.ElapsedMilliseconds}ms");
    }
}

// Registration
app.UseMiddleware<RequestTimingMiddleware>();
Real-world example A team extracts an inline logging lambda into a proper RequestTimingMiddleware class once it grows complex enough to need dependency injection (like an ILogger) and unit testing, which inline lambdas can't easily support.

Common follow-ups: Why is a class-based middleware constructed once per app lifetime rather than per request?;How do you inject scoped services into middleware given this singleton-like construction?

Dependency Injection;Diagnostics & Performance

How do you inject scoped (per-request) services into middleware, given that middleware classes are constructed once at app startup?

Advanced
Since class-based middleware is instantiated once as a singleton for the app's lifetime, you can't inject scoped services (like a DbContext) directly into the constructor -- instead, inject them as parameters on the InvokeAsync method itself, which ASP.NET Core's DI container resolves per-request from the current request's scope, correctly respecting scoped service lifetimes.
public class AuditMiddleware {
    private readonly RequestDelegate _next;
    public AuditMiddleware(RequestDelegate next) => _next = next;

    // Scoped service injected per-request via method parameter, not constructor
    public async Task InvokeAsync(HttpContext context, AppDbContext db) {
        db.AuditLogs.Add(new AuditLog { Path = context.Request.Path });
        await db.SaveChangesAsync();
        await _next(context);
    }
}
Real-world example A middleware logging audit trails to a scoped DbContext initially crashes with a lifetime mismatch error until the team moves the DbContext injection from the constructor to the InvokeAsync method parameter list.

Common follow-ups: What error occurs if you inject a scoped service into a middleware constructor directly?;How does this differ for minimal API endpoint filters?

Dependency Injection;Entity Framework Core & Data Access

What's the difference between app.Use, app.Run, and app.Map in configuring the middleware pipeline?

Intermediate
app.Use adds middleware that can call next() to continue the pipeline (non-terminal). app.Run adds terminal middleware that never calls next() -- it always ends the pipeline at that point. app.Map (and MapWhen) branches the pipeline based on a request path or predicate, letting you build entirely separate sub-pipelines for different routes, such as a distinct branch for /admin requests.
app.Map("/admin", adminApp => {
    adminApp.UseMiddleware<AdminAuthMiddleware>();
    adminApp.Run(async context => await context.Response.WriteAsync("Admin area"));
});

app.Run(async context => await context.Response.WriteAsync("Main app"));
Real-world example An application branches its pipeline with app.Map("/health") to serve a lightweight health check response without running through the full authentication and logging middleware stack used by the main application routes.

Common follow-ups: How does MapWhen differ from Map for conditional branching?;Can branched pipelines rejoin the main pipeline afterward?

Health Checks & Readiness/Liveness Probes;Global Exception Handling & Middleware

How does UseRouting and endpoint routing (via MapControllers, MapGet, etc.) fit into the middleware pipeline conceptually?

Advanced
UseRouting middleware examines the incoming request and matches it against registered endpoint patterns, storing the matched endpoint on the HttpContext but not yet executing it. Middleware registered after UseRouting but before the endpoint execution point (implicitly at the end, or explicitly via UseEndpoints in older syntax) can inspect the matched endpoint's metadata (like authorization requirements) before the actual controller action or minimal API handler finally runs.
app.UseRouting();          // matches request to an endpoint, stores it on HttpContext
app.UseAuthorization();     // can inspect endpoint metadata (e.g., [Authorize]) before execution
app.MapControllers();       // implicitly marks where matched endpoints actually execute
Real-world example The [Authorize] attribute on a controller action works because UseAuthorization middleware, running after UseRouting, reads that action's endpoint metadata to decide whether to allow the request through to actual execution.

Common follow-ups: What is endpoint metadata and how do attributes like [Authorize] attach to it?;Why must UseRouting come before UseAuthorization/UseAuthorization?

RESTful Web APIs & Controllers;Authentication & Authorization (Identity JWT OAuth)

How does short-circuiting work in middleware, and when would you deliberately not call next()?

Intermediate
A middleware short-circuits the pipeline by writing a response and simply not calling await next(), preventing any subsequent middleware or the endpoint from ever executing for that request. This is commonly used for early validation failures, rate limiting rejections, maintenance-mode responses, or cached responses -- any case where you can definitively answer the request without needing the rest of the pipeline.
app.Use(async (context, next) => {
    if (context.Request.Headers["X-Maintenance-Bypass"] != "true" && IsMaintenanceMode()) {
        context.Response.StatusCode = 503;
        await context.Response.WriteAsync("Service under maintenance");
        return;  // short-circuit: next() never called
    }
    await next();
});
Real-world example A rate-limiting middleware short-circuits with a 429 Too Many Requests response the moment a client exceeds their quota, avoiding the cost of running authentication, routing, and the actual endpoint logic unnecessarily.

Common follow-ups: What happens to response headers already set if you short-circuit late in the pipeline?;How does this interact with the exception handling middleware wrapping everything?

Rate Limiting & Throttling;Global Exception Handling & Middleware

How do minimal API endpoint filters (IEndpointFilter) compare to traditional middleware for cross-cutting logic?

Advanced
Endpoint filters, introduced for minimal APIs, apply cross-cutting logic (validation, logging, authorization checks) at the granularity of individual endpoints or route groups rather than the entire pipeline, and get typed access to the endpoint's actual arguments via EndpointFilterInvocationContext -- offering finer control than pipeline-wide middleware while being lighter weight than writing a full middleware class for endpoint-specific concerns.
app.MapGet("/products/{id}", (int id) => GetProduct(id))
   .AddEndpointFilter(async (context, next) => {
       var id = context.GetArgument<int>(0);
       if (id <= 0) return Results.BadRequest("Invalid id");
       return await next(context);
   });
Real-world example A minimal API validates route parameters using an endpoint filter applied only to specific endpoints that need it, rather than writing pipeline-wide middleware that would need to parse and re-validate arguments for every route indiscriminately.

Common follow-ups: How do endpoint filters access strongly-typed endpoint arguments?;Can endpoint filters be reused as a filter factory across many endpoints?

RESTful Web APIs & Controllers;.NET CLI SDK & Project Structure (csproj)

What is the purpose of UseExceptionHandler and UseDeveloperExceptionPage middleware, and how should they differ between environments?

Intermediate
UseDeveloperExceptionPage shows detailed stack traces and exception information directly in the browser, intended only for local development since it leaks sensitive implementation details. UseExceptionHandler is the production-safe alternative, catching unhandled exceptions and routing to a generic error page or a custom error-handling endpoint without exposing internal details to end users, typically registered conditionally based on the hosting environment.
if (app.Environment.IsDevelopment()) {
    app.UseDeveloperExceptionPage();
} else {
    app.UseExceptionHandler("/error");
    app.UseHsts();
}
Real-world example A production incident is avoided when a code review catches that UseDeveloperExceptionPage was accidentally left unconditional, which would have exposed database connection strings in stack traces to any end user triggering an unhandled exception.

Common follow-ups: What information does UseDeveloperExceptionPage expose that's dangerous in production?;How does the newer IExceptionHandler interface (added in .NET 8) provide more structured error handling?

Global Exception Handling & Middleware;Diagnostics & Performance

How does the middleware pipeline interact with asynchronous streaming responses, and what precautions are needed?

Advanced
Since middleware runs both before and after calling next() (which may write directly to the response stream inside the endpoint handler), middleware that reads or buffers the response body (e.g., for logging or compression) must be careful with streaming scenarios -- reading the full response into memory before it's sent can break true streaming (like Server-Sent Events) by defeating its purpose, so such middleware often needs to wrap the response stream rather than buffer it entirely.
public class ResponseLoggingMiddleware {
    public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
        var originalBody = context.Response.Body;
        using var buffer = new MemoryStream();
        context.Response.Body = buffer;  // caution: breaks true streaming if not handled carefully
        await next(context);
        buffer.Seek(0, SeekOrigin.Begin);
        await buffer.CopyToAsync(originalBody);
    }
}
Real-world example A team debugging why their Server-Sent Events endpoint stopped streaming in real time discovers a response-logging middleware was fully buffering the response before forwarding it, defeating the whole point of streaming.

Common follow-ups: How would you rewrite this middleware to support true streaming while still logging?;What's the performance cost of response buffering middleware?

Diagnostics & Performance;gRPC Services

Showing 1–10 of 17