DevgainsDevgainsDevgains
All articles

ASP.NET Core Middleware Pipeline Explained: Order, Use, Run, and Map

·9 min read·Updated Jul 24, 2026

ASP.NET Core middleware is the chain of components every HTTP request flows through before it reaches your endpoint and every response flows back through on the way out. Authentication, routing, CORS, exception handling, response compression — none of these are magic; each is a middleware registered in Program.cs, and the order you register them in decides how your app behaves. Get that order wrong and you ship a service that silently skips authorization, swallows exceptions, or returns 401 for requests that should have worked. This guide explains what the middleware pipeline is, how requests move through it, and how Use, Run, Map, and UseWhen differ — with the ordering rules that cause the most production bugs.

This is a supporting page in the Devgains .NET cluster. It builds on async/await in C# (middleware is asynchronous all the way down) and how services are resolved via dependency injection in ASP.NET Core, since middleware pulls its dependencies from the same container.

Quick answer: what is middleware in ASP.NET Core?

  • Middleware is a component that sits in the request pipeline. Each one receives the HttpContext, can inspect or modify the request, decide whether to call the next component, and then inspect or modify the response on the way back.
  • The pipeline is ordered. Middleware runs in the exact order you register it. Requests flow top-to-bottom; responses flow bottom-to-top — like a set of nested Russian dolls.
  • Each middleware can short-circuit. If it doesn't call next, the pipeline stops there and the response starts unwinding. That's how authentication rejects a request before it ever hits your code.
  • You register middleware in Program.cs with app.Use... calls between building the app and app.Run().

If you remember one thing: order is behavior. UseAuthentication() must come before UseAuthorization(), exception handling goes first, and endpoint execution goes last.

Why the middleware pipeline matters

Almost every cross-cutting concern in a web app — logging, security, error handling, compression, rate limiting — lives in middleware. That makes the pipeline the single most important piece of configuration in your service, and also the easiest to get subtly wrong. A misplaced UseRouting(), an exception handler registered after the middleware that throws, or CORS added after the endpoints will all produce behavior that looks like a framework bug but is really an ordering mistake. Because middleware wraps every request, a bug here is not isolated to one endpoint — it affects the whole application. Understanding the pipeline turns "why is my 401 happening" from guesswork into a five-second read of Program.cs.

How the middleware pipeline works

Conceptually, the pipeline is a chain of delegates. Each middleware is handed a reference to the next delegate — the rest of the pipeline. When a request arrives, the first middleware runs its "pre" logic, calls await next(context) to invoke everything downstream, then runs its "post" logic as the call returns.

Request  →  Exception handler  →  HTTPS redirect  →  Routing  →  AuthN  →  AuthZ  →  Endpoint
                     ↑                   ↑                ↑          ↑         ↑          │
Response ←───────────┴───────────────────┴────────────────┴──────────┴─────────┴─────────┘

Anything before your await next() call runs on the way in; anything after it runs on the way out. If a middleware never calls next, it becomes terminal — it produces the response itself and nothing downstream runs. This nesting is why order matters: a middleware can only affect the request for components registered after it, and can only affect the response for components registered before it.

Step-by-step: writing custom middleware

1. Inline middleware with app.Use

The simplest form is a lambda. This one logs how long each request takes — note that the code after await next() runs on the response side:

var app = builder.Build();
 
app.Use(async (context, next) =>
{
    var start = Stopwatch.GetTimestamp();
    await next(context);           // hand off to the rest of the pipeline
    var elapsed = Stopwatch.GetElapsedTime(start);
    context.Response.Headers["X-Response-Time-ms"] =
        elapsed.TotalMilliseconds.ToString("F1");   // runs on the way back out
});

The next delegate is the rest of the pipeline. Forget to await it and downstream middleware never runs; forget to call it at all and you've written a terminal component by accident.

2. Convention-based middleware class

For anything non-trivial, move the logic into a class. The runtime constructs it once and passes RequestDelegate next to the constructor, then calls InvokeAsync per request:

public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;
 
    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;          // captured once at startup
        _logger = logger;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var start = Stopwatch.GetTimestamp();
        await _next(context);
        _logger.LogInformation("{Method} {Path} took {Ms}ms",
            context.Request.Method, context.Request.Path,
            Stopwatch.GetElapsedTime(start).TotalMilliseconds);
    }
}
 
// A tidy extension method so registration reads well:
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
        => app.UseMiddleware<RequestTimingMiddleware>();
}

Because the class is a singleton, you cannot inject scoped services (like a DbContext) into its constructor — that's the classic captive-dependency trap. Inject scoped services as a parameter of InvokeAsync instead, where they're resolved per request. This is the same lifetime rule covered in dependency injection service lifetimes.

3. Register in the right order

Program.cs is where the pipeline is assembled. The order below is the one Microsoft recommends and the one that avoids the common security bugs:

var app = builder.Build();
 
app.UseExceptionHandler("/error");   // first: catches everything downstream
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();                    // matches the endpoint
app.UseCors();
app.UseAuthentication();             // MUST be before authorization
app.UseAuthorization();
app.UseRequestTiming();              // our custom middleware
app.MapControllers();                // endpoint execution — last
app.Run();

Move UseAuthorization() above UseAuthentication() and the framework can't identify the user before it checks permissions, so authenticated requests get rejected. Register UseExceptionHandler last and it can no longer catch the exceptions thrown above it.

Use vs Run vs Map vs UseWhen

These four methods cover almost every branching need. The difference is whether they call the next middleware and whether they branch the pipeline.

MethodCalls next?Purpose
UseYes (if you await)Add a middleware that can pass control downstream
RunNo — terminalEnd the pipeline; produce the response here
MapBranches on pathFork the pipeline for a path prefix (e.g. /health)
MapWhenBranches on predicateFork when a condition on HttpContext is true
UseWhenConditional, rejoinsRun extra middleware when a predicate matches, then rejoin

Run is terminal by design — it's the end of a branch. Map and MapWhen create a new branch that doesn't rejoin the main pipeline, which is perfect for a lightweight /health endpoint. UseWhen is the subtle one: it inserts middleware only when the predicate matches but then rejoins the main pipeline, so the request still reaches your normal endpoints.

Best practices

  • Register exception handling first so it wraps every other component.
  • Keep the recommended security order: routing → CORS → authentication → authorization → endpoints. Deviating is almost always a bug.
  • Put expensive or optional middleware behind UseWhen/MapWhen so it doesn't run for every request (for example, only compress responses over a size threshold).
  • Emit tracing from middleware, not scattered logs. A single request-logging middleware is the natural place to start a span; see distributed tracing for propagating context across services.
  • Prefer class-based middleware for anything with dependencies or more than a couple of lines.
  • Never do blocking work in middleware. It runs on every request; block the thread and you starve the thread pool — the same failure mode described in async/await in C#.

Common mistakes

  • Authorization before authentication. The user isn't populated yet, so everything looks anonymous.
  • CORS after the endpoints. Browsers get no CORS headers and preflight requests fail. UseCors must sit after UseRouting and before the endpoints.
  • Exception handler registered too late. It only catches exceptions from middleware below it.
  • Injecting a scoped service into a middleware constructor. The middleware is a singleton, so the scoped service is captured for the app's lifetime — inject it into InvokeAsync instead.
  • Forgetting to await next(). The rest of the pipeline silently never runs, and the response short-circuits with whatever the current middleware has written.

Key takeaways

  • Middleware is an ordered chain; requests flow in top-to-bottom and responses flow back bottom-to-top.
  • Order is behavior — authentication before authorization, exception handling first, endpoints last.
  • Use passes control on, Run is terminal, Map/MapWhen branch and don't rejoin, UseWhen branches and rejoins.
  • Class-based middleware is a singleton; resolve scoped dependencies inside InvokeAsync.
  • Every cross-cutting concern lives here, so the pipeline is the first place to look when behavior is global and mysterious.

FAQ

What is middleware in ASP.NET Core? Middleware is a software component assembled into the app's request pipeline. Each component receives the HttpContext, can inspect and modify the request and response, and either passes control to the next component with await next() or short-circuits the pipeline by not calling it.

How does the ASP.NET Core middleware pipeline work? Middleware runs in registration order. A request flows through each component top-to-bottom; when a component calls next, control moves downstream, and once the pipeline returns, each component runs its response-side logic bottom-to-top. Any component that doesn't call next ends the pipeline.

What order should middleware be registered in? Exception handling first, then HTTPS redirection, static files, routing, CORS, authentication, authorization, your custom middleware, and finally endpoint execution. The critical rule is that UseAuthentication must come before UseAuthorization.

What is the difference between Use and Run? Use adds a middleware that can call the next component and continue the pipeline. Run adds a terminal middleware — it never calls next, so it produces the response and nothing after it runs.

When should I use Map versus UseWhen? Use Map (or MapWhen) to fork a separate branch that handles a path or condition and does not rejoin the main pipeline — good for isolated endpoints like /health. Use UseWhen to run extra middleware only when a condition matches and then rejoin the main pipeline so normal endpoints still execute.

Conclusion

The ASP.NET Core middleware pipeline is not framework magic — it's an ordered chain of asynchronous delegates, each free to inspect the request, short-circuit, or decorate the response on its way back out. Once you internalize that requests flow down and responses flow up, most "impossible" behavior resolves into a single question: is this middleware registered in the right place? Keep exception handling first and endpoints last, keep authentication before authorization, and reach for Run, Map, or UseWhen when you need to end or branch the chain. From here the .NET cluster goes deeper into building fast services — browse the full .NET category and continue with Minimal APIs vs Controllers, dependency injection lifetimes, and EF Core performance.

References

9 min read

Read next