Deep Dive - How is ASP.NET Core Middleware Pipeline Built - Header Image

Deep Dive: How is the ASP.NET Core Middleware Pipeline Built?

If you’ve ever used ASP.NET Core, then you’ll likely be familiar with the Startup class. This class, by convention, includes at least one method named Configure. Often, a ConfigureServices method is also included and used to register services with the Microsoft dependency injection container, but this isn’t an absolute requirement.

The configure method is where you define the app’s request processing pipeline, by registering framework or custom middleware components.

Have you ever wondered how the code you include in the Configure method in the Startup class is converted to a pipeline to handle requests? Wonder no more! In this post, we’ll explore some of the ASP.NET Core source code and answer this question.

This post dives into some of the implementation details of ASP.NET Core. This is not required reading to effectively use ASP.NET Core, and it’s middleware pipeline. However, I believe there is value in understanding these details to appreciate what is happening inside your applications. I’ll show some rather complicated looking code snippets in this post and explain them as best I can. Don’t worry if you don’t understand everything at first.

A Basic ASP.NET Core Application

We’ll use a simple application to demonstrate the flow which we can fit inside a single file.

Inside the Program class, the main method builds and runs a host. The host concept supports long-running .NET Core applications, providing features such as dependency injection and configuration.

To create the Host, the static CreateHostBuilder method is used. This first establishes a generic host builder using the Host.CreateDefaultBuilder method. A call to ConfigureWebHostDefaults is also made, to enable support for hosting ASP.NET Core applications. This method accepts an Action<IWebHostBuilder> which can be used to further configure the web host and enables ASP.NET Core to function.

In this example, an expression lambda is used to point the web host to the Startup class by calling UseStartup. The Startup class is, by convention, used to configure the services and the request pipeline.

The Startup class for this sample contains a lone Configure method. This method accepts an IApplicationBuilder parameter. The IApplicationBuilder may be used to configure the request handling pipeline for this ASP.NET Core application.

Introducing Request Delegates

The request pipeline is defined as a series of RequestDelegate components. Let’s first focus on that type.

The delegate keyword may be a new concept for some .NET developers as it’s not frequently required by user code. The Microsoft documentation defines a delegate as follows… 

“A delegate is a type that represents references to methods with a particular parameter list and return type.” 

You may think this sounds quite similar to an Action<T> or Func<T> and you’d be correct. Both of these are types are actually delegates and defined using the delegate keyword. For general use, Func and Action are easy to apply. In cases, as we have here with the RequestDelegate, where one wants to afford more meaning, a specific delegate signature can be defined. 

The RequestDelegate type represents any Task returning method, which has a single parameter of type HttpContext. A RequestDelegate can therefore operate on the HttpContext asynchronously.

ASP.NET Core request handling is achieved by chaining one or more RequestDelegate instances together. Each delegate may handle the request by writing to the response on the HttpContext. If the RequestDelegate is considered terminal, i.e. it has fully completed the response, it can simply return. In cases, where it has not entirely handled the request, it may pass the HttpContext onto the next RequestDelegate in the chain.

ASP.NET Core Middleware

At a higher level, we often see this as a sequence of middleware components. When defining a middleware class, one of the main requirements is that it include a method named either Invoke or InvokeAsync. Either is suitable, although, since ASP.NET Core 2.0, the later is more commonly used.

The signature for this method is as follows…

public Task InvokeAsync(HttpContext httpContext)

If you focus on the signature, you will see that it matches the signature of a RequestDelegate. It accepts a HttpContext and returns a Task.

Middleware classes are a common concept used to provide request handling functionality. If you’ve built any ASP.NET Core applications, you’ve used middleware, whether consciously or not. For common crosscutting concerns in your application, writing your own middleware is a relatively trivial exercise.

Inline Middleware

For this post, I want to focus on how we get from the Configure method, to a final request handling pipeline. We’ll ignore the middleware class concept and instead define our request pipeline by registering a single inline middleware component.

Inline middleware components are not really any different from creating and registering a middleware class. However, they require more ceremony to access application services from the dependency injection container. Ultimately, they are used to define a request delegate for your application.

In the above code, we’ve created and registered a single middleware delegate into the request pipeline. In real-world applications, you’ll usually have more than one middleware delegate defined. When registering middleware inside the Configure method, those registered first (at the top of the Configure method), run before those registered last. Often, the order is essential for middleware features to function correctly.

In our example, we have defined the inline middleware by calling the Use extension method on the IApplicationBuilder. 

This method extends the IApplicationBuilder and as its parameter, accepts a Func<HttpContext, Func<Task>, Task> representing a part of the middleware component chain. It’s a pretty nasty signature so let’s try to break it down.

The middleware parameter here is a Func which accepts two parameters, a HttpContext and another Func. The return type for this Func is a Task. At this point we have a representation of a wrapped RequestDelegate. At runtime, we can access the HttpContext in our middleware code, and optionally, invoke the Func<Task> which represents the next RequestDelegate.

The Use extension method calls the Use method defined directly on the IApplicationBuilder interface.

IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);

This method accepts a RequestDelegate and returns a RequestDelegate. This is how the chaining is achieved to form the request handling pipeline. Each piece of middleware needs to know about the next middleware component so that it can choose to call it, to continue request handling. This call to the next RequestDelegate is optional as a component may decide it has entirely handled the request and been able to produce a “terminal” HTTP response.

Let’s look at the Use implementation on the ApplicationBuilder. This type is the concrete implementation for the IApplicationBuilder abstraction.

It’s a pretty straightforward method. It adds the middleware Func to a list of components which it maintains. Each middleware is added into the list in the order in which they are registered in the Configure method.

Back in the extension method, it calls the Use method on the ApplicationBuilder, supplying the Func<RequestDelegate, RequestDelegate> using a lambda. The Func expects us to accept a RequestDelegate, which represents the next piece of middleware to run, and returns a RequestDelegate.

The RequestDelegate being returned is defined with a second lambda, accepting the HttpContext. Inside the statement body, the next RequestDelegate is wrapped into a Func<Task> with yet another lambda expression. This defines code which when called, invokes the RequestDelegate, passing in the current HttpContext.

This layering of lambdas is pretty confusing to look at, and even harder to explain in writing. Don’t worry if it’s not entirely clear. The summary is that the Use extension method supports registering code representing a middleware delegate. When called this acts on the HttpContext and if necessary, may invoke the next RequestDelegate.

Building the Final Request Handling Pipeline

When the application starts, the Main method is invoked, and the generic host is built and started. We’ll step through some of the critical sections of code inside ASP.NET Core which are involved in building the request handling pipeline.

Because we specified a Startup class for the host, during the build phase, ASP.NET core will attempt to invoke its methods. The last method which is invoked is the Configure method. 

At this point, we have a ConfigureBuilder instance which is used to register an Action to operate upon the GenericWebHostServiceOptions.

After the build stage for the host, the host is started. At this point, any hosted services are also started. In recent versions, ASP.NET Core is itself a hosted service and is started by the IHostedService mechanism. We won’t dive into that for this post, but it is something I’ll revisit at a later date.

If you’re interested in hosted services, I have a Pluralsight course which explains how they work and where they can be applied in your own applications.

The GenericWebHostService implements IHostedService and therefore, is started when the host StartAsync method is called. The
GenericWebHostService includes a dependency on IOptions<GenericWebHostServiceOptions> which is resolved from the dependency injection container. When it’s Value property is accessed, any registered configure Actions are invoked. This includes the Action<T> which causes the ConfigureApplication property to be set with an Action<IApplicationBuilder>. This Action is specified in the lambda above.

GenericWebHostService implements the StartAsync method, which is responsible for building the request pipeline and starting the server. The code we’re focused on is inside a try block.

The value of the GenericWebHostServiceOptions.ConfigureApplication property is stored into a variable called configure and then null checked.

Introducing the ApplicationBuilder

An ApplicationBuilderFactory is used to create an ApplicationBuilder instance. This type implements the IApplicationBuilder interface.

Next, any registered IStartupFilter instances are applied. We’ll skip over the IStartupFilter concept for this post since it’s more detail than we need here. Let’s assume for the sake of this post that none are registered, which is not true in the sample, but go with it!

Finally, the configure variable (an Action<IApplicationBuilder>) is invoked. This causes the GenericWebHostServiceOptions.ConfigureApplication Action to be invoked.

At this point, the Invoke method on the ConfigureBuilder is called…

The above code first creates a scope from the dependency injection container. Why is this necessary?

Well, the requirement for the Configure method is that it must accept at least one parameter, which is an IApplicationBuiler. It can, however, support parameters for other dependencies to be supplied from the service provider. This is where that magic happens.

Using reflection, all parameters of the method are identified. The code then loops over these, supplying them from the service provider if they are available. In the case where the type of the parameter is IApplicationBuilder, it is special-cased so that the current IApplicationBuilder is provided.

Once all parameters can be fulfilled, the method is invoked via reflection. This where your code, inside the Configure method, is executed. As a reminder, our code called the Use extension method on the IApplicationBuilder to register our inline middleware as a RequestDelegate.

Building the RequestDelegate Pipeline

Back inside the GenericWebHostedService, the Build method is called on the IApplicationBuilder.

This method causes the final pipeline of RequestDelegates to be produced.

An oft used analogy for this that of the traditional Matryoshka (Russian) doll. These are a set of wooden dolls which stack inside one another. The largest outer doll can be opened to reveal the doll nested inside, which in turn can be opened to access the next.

This is precisely how the RequestDelegate instances are treated at this stage. The first RequestDelegate registered at the top of the Configure method will be wrapped around the next RequestDelegate. When the pipeline handles a request, it flows through each layer starting with the outermost RequestDelegate. This would be the largest doll in our analogy. If that delegate can produce a response, it is free to do so and return directly. Otherwise, it will call the next delegate, opening up the doll to find the next layer inside.

Logically, since each middleware (RequestDelegate) in the pipeline needs to be able to optionally call the next RequestDelegate which it will wrap, the pipeline needs to be built up in reverse order, starting with the innermost delegate.

The code above handles this process and helps to answer a common question people ask about ASP.NET Core…

How Does ASP.NET Core Return a 404 Not Found Response?

This is a reasonable question since in most apps, we don’t see any middleware registered to handle the requests for paths which our application code does not expect. Inside MVC, for example, we define Actions or RazorPages. We may use convention-based or attribute-based routing to define the URLs which map to these endpoints. But we are not required to define a catch-all Action method for unknown paths. So where and how is a 404 response generated for unknown URLs?

Take a look again at the code snippet above.

It first defines a RequestDelegate using a lambda and stores it into the app variable. This first delegate is going to be our innermost wooden doll, which is, therefore, the last to be executed in the request pipeline. This is essentially a fall back which will be called in cases where all prior middleware have not been able to produce a response.

It performs a check to see if an Endpoint was matched for the request via the Endpoint Routing mechanism. This may occur in rare situations where the endpoint feature has not been applied correctly. Below this is the code we are most interested in. It sets the response status code to 404 Not Found and then returns. Any requests which make it this far, into the innermost RequestDelegate, therefore, return a 404 response to the caller.

With the innermost RequestDelegate defined, the remaining code begins the process of wrapping each RequestDelegate inside one another, working from the inside. To achieve this, the list of components is used. As a reminder, a component, in this case, is simply a Func<T, T> which accepts a RequestDelegate as a parameter and returns a RequestDelegate. This is a signature used to perform the wrapping of request delegates into a request handling pipeline.

Components were added to this list in the order that they are registered inside the Configure method. To perform the wrapping, the list is reversed so that we start with the innermost delegates first. 

The app variable is updated by taking the delegate from the reversed list and passing it the reference to the current value of the app variable. On the first iteration, this will be the 404 RequestDelegate, allowing it to be wrapped by one of the middleware components registered in the Configure method. This continues, progressively moving out until we have wrapped all delegates inside one another.

We are now at the outer RequestDelegate, which is the first component that we expect to be called when handling a request. This is returned from the Build method.

At this point, we find ourselves back in the StartAsync method of the GenericWebHostedService. A HostingApplication is constructed, accepting the RequestDelegate pipeline which will then be executed for each incoming request handled by ASP.NET Core.

We’ll end our journey here, as we’ve explored lots of code.

For the sample application, a request to the server with a path of “/” will be handled fully by our inline middleware component. It will set a response and return, without invoking any further middleware.

For requests where the URL is anything but “/”, our middleware is unable to handle it and cannot produce a response. In this case, the pipeline continues when our middleware component executes the next RequestDelegate. 

For this sample, there are no other middleware components registered, so the innermost RequestDelegate, defined in the ApplicationBuilder, will respond. It returns a status code of 404, not found.

Summary

In this post, we have taken a deep dive into ASP.NET Core implementation code. We set out to understand how the request handling pipeline is built so that an ASP.NET Core application can handle requests. 

We saw that some of this takes place when then Host is built, with the remainder occurring when the Host is started. ASP.NET Core is fundamentally a complex IHostedService. When is starts it identifies the Configure method of Startup class and invokes it to register one or more components into the pipeline.

When the ApplicationBuilder is used to build the pipeline, it forms a chain of RequestDelegates, wrapping them one inside the other, to produce a pipeline for request handling.

I hope this has been as interesting for you to read as it has for me to research and write!


Have you enjoyed this post and found it useful? If so, please consider supporting me:

Buy me a coffeeBuy me a coffee Donate with PayPal

Steve Gordon

Steve Gordon is a Pluralsight author, 6x Microsoft MVP, and a .NET engineer at Elastic where he maintains the .NET APM agent and related libraries. Steve is passionate about community and all things .NET related, having worked with ASP.NET for over 21 years. Steve enjoys sharing his knowledge through his blog, in videos and by presenting talks at user groups and conferences. Steve is excited to participate in the active .NET community and founded .NET South East, a .NET Meetup group based in Brighton. He enjoys contributing to and maintaining OSS projects. You can find Steve on most social media platforms as @stevejgordon

Leave a Reply

Your email address will not be published. Required fields are marked *