The ASP.NET Core Middleware Pipeline Explained

Every HTTP request that reaches your ASP.NET Core application flows through a series of middleware components before a response is returned. Understanding this pipeline is fundamental to building well-structured web applications.

What Is Middleware?

Middleware is a component that sits in the request pipeline. Each piece of middleware can inspect, modify, or short-circuit requests and responses. They execute in the order they are registered, forming a pipeline that resembles a set of nested Russian dolls — each wrapping the next.

When a request arrives, it passes through each middleware in sequence. When the final middleware (or an endpoint) generates a response, that response travels back through the same middleware in reverse order.

The Basics: Use, Map, and Run

ASP.NET Core provides three fundamental extension methods for building the pipeline:

Example.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Use: adds middleware that calls the next component
app.Use(async (context, next) =>
{
    Console.WriteLine("Before next middleware");
    await next(context);
    Console.WriteLine("After next middleware");
});

// Map: branches the pipeline based on the request path
app.Map("/health", branch =>
{
    branch.Run(async context =>
    {
        await context.Response.WriteAsync("Healthy");
    });
});

// Run: terminal middleware — does not call next
app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from terminal middleware");
});

app.Run();

Use is the most common — it processes the request, optionally calls next to pass control downstream, and then processes the response on the way back. Run is terminal; it never calls next. Map creates a branch in the pipeline based on the request path.

Order Matters

The order in which you register middleware is critical. Consider this common setup:

Program.cs
var app = builder.Build();

app.UseExceptionHandler("/error");
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Exception handling is registered first so it can catch errors thrown by any downstream middleware. Static files are served before routing kicks in, avoiding unnecessary overhead. Authentication runs before authorisation because you must know who the user is before checking what they can do.

If you swapped UseAuthentication and UseAuthorization, authorisation checks would run without a known identity — every request would fail.

Writing Custom Middleware

For anything beyond a quick inline delegate, create a proper middleware class:

RequestTimingMiddleware.cs
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next,
        ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();

        await _next(context);

        stopwatch.Stop();
        _logger.LogInformation("Request {Method} {Path} completed in {Elapsed}ms",
            context.Request.Method,
            context.Request.Path,
            stopwatch.ElapsedMilliseconds);
    }
}

Register it with an extension method for a clean API:

RequestTimingMiddlewareExtensions.cs
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestTimingMiddleware>();
    }
}

// In Program.cs
app.UseRequestTiming();

The framework resolves ILogger<RequestTimingMiddleware> from the DI container automatically. The constructor receives RequestDelegate next, which represents the next middleware in the pipeline.

Conditional Middleware with UseWhen

Sometimes you want middleware to execute only for certain requests without branching the entire pipeline:

Example.cs
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/api"),
    appBuilder =>
    {
        appBuilder.UseMiddleware<ApiKeyValidationMiddleware>();
    });

Unlike Map, UseWhen rejoins the main pipeline after the conditional branch executes, so downstream middleware still runs.

Key Takeaways

The middleware pipeline is the backbone of request processing in ASP.NET Core. Registration order defines execution order, and getting it wrong leads to subtle bugs. Keep middleware focused on a single concern, use the convention-based class approach for anything non-trivial, and always consider where in the pipeline your component needs to sit relative to others.