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:
- Constructor Analysis: The middleware type is instantiated using the ASP.NET Core service provider, injecting constructor dependencies automatically.
- Method Discovery: The binder inspects the type for
Task InvokeAsync(HttpContext)orTask Invoke(HttpContext)methods. - 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 fromHttpContext.RequestServicesat 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
ApplicationBuilderstores middleware asFunc<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 inApplicationBuilder.cs) ensures execution order matches registration order by wrapping delegates from the inside out. - Type activation through
UseMiddlewareExtensions.cshandles constructor injection,Invoke/InvokeAsyncmethod discovery, and expression tree compilation for service injection. - Unhandled requests trigger a terminal delegate that sets
__RequestUnhandledtotrueand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →