How the Middleware Pipeline Is Configured in ASP.NET Core: Inside ApplicationBuilder

ASP.NET Core configures the middleware pipeline using ApplicationBuilder to chain RequestDelegate instances, constructing the pipeline by wrapping components in reverse order when Build() is called.

The configuration of the middleware pipeline is a fundamental aspect of ASP.NET Core development, implemented within the dotnet/aspnetcore repository through the IApplicationBuilder abstraction. Understanding how ApplicationBuilder stores, orders, and compiles middleware components reveals why the ordering of statements in Program.cs directly impacts request processing behavior.

The ApplicationBuilder Foundation

The ApplicationBuilder class, located in src/Http/Http/src/Builder/ApplicationBuilder.cs, serves as the concrete implementation of IApplicationBuilder. This class maintains the ordered collection of middleware and handles the final compilation into an executable request processor.

Storing Middleware Components

Inside ApplicationBuilder.cs, the class maintains a private readonly list called _components that stores each middleware registration:

private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();

Every call to IApplicationBuilder.Use appends a Func<RequestDelegate, RequestDelegate> to this list. This delegate represents a middleware factory that accepts the next RequestDelegate in the chain and returns a new RequestDelegate wrapping the current middleware's logic.

Building the Pipeline in Reverse

When the application starts, ApplicationBuilder.Build() walks through the _components list backwards (lines 164-200 in ApplicationBuilder.cs). It begins with a terminal RequestDelegate that sets the __RequestUnhandled flag and returns a 404 response, then iteratively wraps each preceding middleware around it:

// Simplified logic from ApplicationBuilder.cs lines 164-200
RequestDelegate app = context =>
{
    context.Items[RequestUnhandledKey] = true; // __RequestUnhandled
    return Task.CompletedTask;
};

for (int c = _components.Count - 1; c >= 0; c--)
{
    app = _components[c](app);
}

return app;

This reverse construction ensures that the order of execution matches the registration order: the first middleware registered becomes the outermost wrapper, while the last registered sits closest to the terminal delegate.

Registering Middleware Components

ASP.NET Core offers multiple patterns for adding middleware to the pipeline, each resolved through different mechanisms in the UseMiddlewareExtensions class.

Inline Middleware with Lambda Expressions

The simplest approach uses app.Use with an inline lambda, which directly adds a delegate factory to the _components list:

app.Use(async (context, next) =>
{
    // Pre-processing logic executes first
    Console.WriteLine($"Request: {context.Request.Path}");
    
    await next(); // Calls the next middleware in the chain
    
    // Post-processing logic executes on the return trip
    Console.WriteLine($"Response: {context.Response.StatusCode}");
});

This delegates directly to the ApplicationBuilder.Use method without requiring type activation.

Typed Middleware Registration

For class-based middleware, the UseMiddleware<T>() extension method (defined in src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs) handles type discovery and activation. When you call:

app.UseMiddleware<MyLoggingMiddleware>();

The extension scans MyLoggingMiddleware for an Invoke or InvokeAsync method and validates its signature against the expected patterns.

Middleware Activation and Dependency Injection

The UseMiddlewareExtensions.cs file (lines 25-46) contains the binding logic that bridges the gap between your middleware class and the pipeline.

Constructor Injection and Method Binding

When adding typed middleware, the framework performs the following steps:

  1. Constructor Analysis: The middleware type is instantiated using the ASP.NET Core service provider, injecting constructor dependencies automatically.
  2. Method Discovery: The binder inspects the type for Task InvokeAsync(HttpContext) or Task Invoke(HttpContext) methods.
  3. Delegate Compilation: If the method accepts only HttpContext, it creates a direct delegate. For methods requiring additional services, it compiles an expression tree (or falls back to reflection) that resolves extra parameters from HttpContext.RequestServices at runtime.
public class MyLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<MyLoggingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogInformation("Handling request: {Path}", context.Request.Path);
        await _next(context);
        _logger.LogInformation("Finished handling request.");
    }
}

The IMiddleware Alternative

For middleware requiring per-request activation from the dependency injection container, ASP.NET Core supports the IMiddleware interface (defined in src/Http/Http.Abstractions/src/IMiddleware.cs). When using this pattern with IMiddlewareFactory, the middleware instance is resolved from DI for each request rather than being constructed as a singleton during pipeline building.

Pipeline Termination and Unhandled Requests

When the pipeline reaches its end without a middleware short-circuiting the request, ApplicationBuilder provides a default terminal delegate. As implemented in ApplicationBuilder.cs (lines 84-92), this delegate sets the __RequestUnhandled item in the HttpContext dictionary to true and returns a 404 response:

// From ApplicationBuilder.cs lines 84-92
if (app == null)
{
    return context =>
    {
        context.Response.StatusCode = 404;
        context.Items[RequestUnhandledKey] = true;
        return Task.CompletedTask;
    };
}

This flag allows higher-level frameworks like ASP.NET Core MVC to detect when no endpoint handled the request, enabling features like status code re-execution pages.

Summary

  • ApplicationBuilder stores middleware as Func<RequestDelegate, RequestDelegate> entries in an ordered list, with each entry representing a middleware factory that wraps the next component.
  • Reverse construction during Build() (lines 164-200 in ApplicationBuilder.cs) ensures execution order matches registration order by wrapping delegates from the inside out.
  • Type activation through UseMiddlewareExtensions.cs handles constructor injection, Invoke/InvokeAsync method discovery, and expression tree compilation for service injection.
  • Unhandled requests trigger a terminal delegate that sets __RequestUnhandled to true and returns HTTP 404, providing a hook for status code handling middleware.

Frequently Asked Questions

What is the difference between Use and UseMiddleware in ASP.NET Core?

Use accepts a lambda or delegate directly and adds it immediately to the _components list without type activation, while UseMiddleware resolves the specified type from dependency injection, validates its Invoke method signature, and handles constructor parameter injection. Use Use for simple inline logic; use UseMiddleware<T> when you need dependency injection and reusable middleware classes.

How does ASP.NET Core handle dependency injection in class-based middleware?

According to the UseMiddlewareExtensions.cs implementation (lines 25-46), the framework first instantiates the middleware class using the service provider to satisfy constructor dependencies, then analyzes the Invoke or InvokeAsync method. If the method accepts parameters beyond HttpContext, the binder compiles an expression tree that resolves those additional services from HttpContext.RequestServices at runtime, avoiding reflection overhead in the hot path.

Why is the middleware pipeline built in reverse order?

The Build() method in ApplicationBuilder.cs iterates backwards through the _components list (lines 164-200) so that the resulting RequestDelegate executes middleware in the same order it was registered. Because each middleware factory wraps the next delegate, starting from the end and working backwards creates a Russian doll structure where the first registered middleware becomes the outermost layer, intercepting requests first and finishing last during the response phase.

What happens if no middleware handles the request?

If the pipeline reaches the terminal delegate without a middleware short-circuiting the request (by not calling next() or writing to the response), the ApplicationBuilder default delegate sets an internal flag __RequestUnhandled to true in the HttpContext.Items dictionary and returns HTTP 404 (lines 84-92 in ApplicationBuilder.cs). This allows framework components like endpoint routing to detect unhandled requests and execute status code pages or fallback endpoints.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →