← Back to list

The ASP .NET Core Pipeline Order Nobody Explains Properly

Learn how ASP.NET Core middleware order really works, why Use, Run, and Map behave differently, and how one misplaced line can quietly…

Muhammad Waseem in Weekly .NET Newsletter · 2026-08-30 05:13 · 0 claps · 6.2 min read paywalled
#csharp #dotnet #dotnet-core #middleware #dot-net-framework
Open on Medium ↗
Wiki topics: 🔧 · Data Engineering

The ASP .NET Core Pipeline Order Nobody Explains Properly

Learn how ASP.NET Core middleware order really works, why Use, Run, and Map behave differently, and how one misplaced line can quietly bypass authentication.

*Sponsor this newsletter to reach 10,000+ readers** →***

The ASP .NET Core Pipeline Order Nobody Explains Properly

The ASP .NET Core Pipeline Order Nobody Explains Properly

A request coming into an ASP .NET Core app doesn’t go straight to your controller. It passes through a stack of components first, things like auth, CORS, rate limiting, and exception handling. That stack is the middleware pipeline.

Non-members read here

Each middleware sits in line, does its job, then either hands off to the next one or stops the request right there. That’s really it. Nest a few of these together, and you’ve got a pipeline.

A typical API pipeline might look like this:

Request
  ↓
Exception Handling
  ↓
HTTPS Redirection
  ↓
Routing
  ↓
CORS
  ↓
Authentication
  ↓
Authorization
  ↓
Endpoint
  ↓
Response

The response travels back through in reverse order:

Request
  ↓
Middleware A
  ↓
Middleware B
  ↓
Middleware C
  ↓
Endpoint
  ↑
Middleware C
  ↑
Middleware B
  ↑
Middleware A
  ↑
Response

That reverse trip is why middleware works so well for request timing, logging, and headers- anything that needs to wrap the whole request.

Types of Middleware: Built-in and Custom

Every middleware in your pipeline falls into one of two buckets: the ones ASP.NET Core ships for you, and the ones you write yourself.

Built-in middleware

ASP.NET Core ships middleware for most of the cross-cutting concerns you’ll actually need: exception handling, HTTPS redirection, static files, routing, CORS, authentication, authorization, rate limiting, response compression. I’ve covered several of these individually: built-in middleware, CORS, rate limiting, response compression, and global exception handling. Reach for these first; don’t hand-roll something the framework already gives you.

Custom middleware

Once you need logic specific to your own app- a header check, a tenant lookup, something tied to your domain- you write it yourself. There are three ways to do that.

1. Request delegate (inline)

The simplest option. Written directly in Program.cs with app.Use.

app.Use(async (context, next) =>
{
    // logic before the next middleware
await next(context);
    // logic after the next middleware
});

Skip the next call if you don’t want the pipeline to continue.

2. By convention

A dedicated class. ASP.NET Core recognizes it through its constructor and an Invoke or InvokeAsync method; no interface required.

public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;

public RequestTimingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        // logic before the next middleware
        await _next(context);
        // logic after the next middleware
    }
}

Registered in Program.cs:

app.UseMiddleware<RequestTimingMiddleware>();

3. Using a factory (IMiddleware)

Same idea, but the class implements IMiddleware and is activated through DI instead of by convention.

public class RequestTimingMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // logic before the next middleware

     await next(context);
        // logic after the next middleware
    }
}

Register the class in DI, then wire it into the pipeline the same way as before:

builder.Services.AddTransient<RequestTimingMiddleware>();
app.UseMiddleware<RequestTimingMiddleware>

Worth the extra step when your middleware needs scoped dependencies; factory-based activation plays nicer with the container than convention-based middleware does. For most day-to-day cases, though, convention-based is enough.

The Most Important Part: Calling the Next Middleware

Consider this small middleware:

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

The important line is await next(context);. It passes control to the next component. When the rest of the pipeline finishes, execution comes back to the line right after it.

With two middleware components, the order looks like this:

Middleware 1 - Before
Middleware 2 - Before
Endpoint Executes
Middleware 2 - After
Middleware 1 - After

Think of middleware as nested layers, not a one-way list. That mental model makes the whole pipeline click.

Use vs Run vs Map

You’ll see these three constantly while configuring an app. They solve different problems.

Use

Use adds middleware that can continue to the next component.

app.Use(async (context, next) =>
{
    // Before downstream middleware
await next(context);
    // After downstream middleware
});

Typical examples: request logging, correlation IDs, request timing, authentication, authorization, CORS, rate limiting, security headers. Anything where you want to do something around the request and then let it continue.

Run

Run adds terminal middleware. There’s no next delegate because processing ends here.

app.Run(async context =>
{
    await context.Response.WriteAsync("Request handled.");
});

Useful for a simple terminal response, maintenance mode, a fallback handler, or the end of a custom pipeline branch. Anything registered after it won’t run for requests that reach it, so use it deliberately.

Map

Map creates a separate pipeline based on the request path.

/api/*
    → API pipeline
    → API endpoints

/admin/*
    → Admin-specific middleware
    → Admin endpoints

Handy when one part of your app needs different processing: admin routes, API vs non-API, legacy sections, tenant-specific branches. One gotcha: Map strips the matched segment off Request.Path and moves it into Request.PathBase, so code inside the branch reading Request.Path sees it without that prefix.

Conditional Middleware: UseWhen vs MapWhen vs Map

Map only branches on path, and once a request goes down that branch, it never comes back to the main pipeline. Sometimes that’s too blunt. You want a middleware to apply only under some condition, without splitting your whole app into separate pipelines.

That’s what UseWhen and MapWhen are for, and they’re not interchangeable with each other either.

UseWhen: branches on any condition, and rejoins the main pipeline afterward as long as nothing inside the branch short-circuits.

app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/api/user"),
    branch => branch.UseMiddleware<YourMiddlewareName>()
);
app.UseAuthorization();
app.MapControllers();

Requests to /api/user/* pick up YourMiddlewareName on the way through, then continue on to authorization and the controllers exactly like every other request. Nothing else about the pipeline changes for them.

MapWhen branches on any condition too, but behaves like Map, the branch does not rejoin.

app.MapWhen(
    context => context.Request.Headers.ContainsKey("X-Beta-User"),
    betaApp =>
    {
        betaApp.UseMiddleware<BetaFeatureMiddleware>();

        betaApp.Run(async context =>
        {
            await context.Response.WriteAsync("Beta experience");
        });
    });

Two questions settle which one you need. Does the branch need to rejoin the main pipeline, or is it a dead end. And are you branching on the path, or on something else entirely: a header, a query string, the user’s claims. Map and MapWhen are dead ends and only Map is path-only. UseWhen rejoins and can check anything.

What Should Middleware Handle?

Middleware is built for cross-cutting concerns tied to the HTTP request itself. Say you want to time every request. Adding that logic to every controller duplicates infrastructure code across the app. Middleware gives you one place to do it once.

Good candidates: global exception handling, request/response logging, correlation IDs, authentication, authorization, CORS, rate limiting, request timing, security headers, response compression.

Ask yourself whether the concern applies to the HTTP request as a whole. If it does, middleware’s usually the right starting point. ASP.NET Core already ships middleware for most of these (see the built-in middleware section above), so don’t rebuild what’s already there.

Middleware Order Matters

Middleware runs in the order you register it.

app.UseAuthentication();
app.UseAuthorization();

Authentication figures out who the user is. Authorization decides whether that user can access what they’re asking for. Flip those two, and you get broken behavior that looks like a completely unrelated bug.

A simplified pipeline:

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();
app.MapControllers();

Don’t copy this into every project blindly. The right order depends on what middleware you’re actually using. What matters is the relationships: exception handling needs to sit early enough to catch what’s downstream, authentication comes before authorization, CORS has to run at the right point for the endpoints it covers, and anything that short-circuits blocks everything registered after it.

Order isn’t a formality here. Get auth and authorization backwards, and you’ll spend an hour debugging something that was never really a bug.

What Is Short-Circuiting?

A middleware doesn’t have to call the next component. Say your app requires a specific header:

app.Use(async (context, next) =>
{
    if (!context.Request.Headers.ContainsKey("X-API-Key"))
    {
        context.Response.StatusCode =
            StatusCodes.Status401Unauthorized;

      await context.Response.WriteAsync(
            "API key is missing.");
        return;
    }
    await next(context);
});

Missing header, response goes out immediately, nothing later in the pipeline runs. That’s short-circuiting. Useful for rate-limit rejection, maintenance mode, invalid requests, cached responses, security checks.

Watch out when writing custom middleware, though. Forgetting to call next when you meant to continue makes everything after it look broken for no obvious reason.

Middleware vs Filters: Which One Should You Use?

Both can wrap logic around something else, so they get confused a lot. The difference is where that logic runs.

Middleware only sees the HttpContext; it has no idea which controller or action got picked. Filters run after that decision’s already been made, so they’re the right call once you need the action’s arguments or its result.

Correlation IDs, request logging, global exception handling: middleware. Inspecting an action’s arguments or modifying its result: filter. A client IP restriction could go either way; I showed both approaches in my piece on client IP safelisting in ASP.NET Core.

Summary

  • Order isn’t cosmetic; get auth and authorization backwards, and the bug won’t look like an order problem
  • Short-circuit when finishing the pipeline would be wasted work
  • Middleware for anything that only needs HttpContext; filters once you need the action itself

Whenever you’re ready, there are 2 ways I can help you:

  1. I work with founders and growing businesses to design, build, and improve reliable web apps. *Let’s talk →*
  2. Promote yourself to 10,000+ subscribers by *sponsoring this newsletter** →***
  3. Boost your .NET skills by subscribing to my *YouTube Channel** →***

메타데이터
post_id
842ea41a4f8e
slug
the-asp-net-core-pipeline-order-nobody-explains-properly-842ea41a4f8e
url
https://medium.com/net-newsletter-by-waseem/the-asp-net-core-pipeline-order-nobody-explains-properly-842ea41a4f8e
canonical_url
https://medium.com/net-newsletter-by-waseem/the-asp-net-core-pipeline-order-nobody-explains-properly-842ea41a4f8e
author_url
https://medium.com/@mwaseemzakir
status
ok
fetched_at
2026-08-31 15:46:43