In this post, I discuss the differences between convention and factory styles of writing middleware in ASP.NET Core along with the differences in how the instances are created and interact with dependency injection.
If you have been using ASP.NET Core for a while, you are probably familiar with the concept of middleware. If not, I would recommend reading the Microsoft Docs page that provides an overview of how middleware conceptually works
In this post, I will be delving deeper into how middleware is added into the request-response pipeline with references to the code in UseMiddlewareExtensions.cs.
The link I have used here is to the excellent https://source.dot.net/ web site where you can easily search for .NET Core/5 code by type or member name instead of trawling through the ASP.NET Core GitHub repos.
Before delving into the mechanics of how the pipeline is built and works, lets start with how the middleware gets registered with your application.
When you need to add middleware to your ASP.NET Core application, it is usually done within your Startup class in the Configure method.
There are three main ways of registering middleware in the Configure method, namely by using the generic and non-generic UseMiddleware extension methods and lastly the Use method on IApplicationBuilder.
Let's look at each of these in a bit more detail in order of ease of use (which also happens to be a top down order of execution).
In most cases, you will be encapsulating your middleware into a class which adheres to either a convention or an interface (more on this in a bit). This allows you to reuse your middleware code if it is in its own class library project.
The simplest way to register your middleware class within the Configure method is to use the UseMiddleware
To call this method, you need to supply a generic parameter
The method has an optional parameter (args) for passing in an array of objects that represent parameters that can be passed into the constructor of your class. This can be of use when not all of the constructor parameters can be resolved by dependency injection.
Behind the scenes, this method makes a call to the non-generic UseMiddleware method. To do this, the generic method gets a Type instance from the generic type parameter and passes it (with the optional args array if present) to the non-generic method which does the hard work.
Most of the time, you will be using the generic version, but it is worth knowing there is a non-generic version if you need to derive the type at runtime.
Let's look at the start of the method to see what is going on
In this method, the code checks to see if the middleware type passed in as a parameter implements the IMiddleware interface. If it does, the method branches off to use a middleware factory that will use dependency injection to create the instance of the middleware class
Otherwise, it makes a call to the IApplicationBuilder's Use method by passing in an anonymous function that uses reflection to create the required request delegate (not show here as it is a lot of code - if interested, look here).
The IApplicationBuilder.Use method is the 'raw' way of registering middleware. The method takes a single parameter of type
This delegate may be a method or an anonymous function created by a lambda expression, but must adhere to the delegate signature of
Task RequestDelegate(HttpContext context)
In this example from the Microsoft Docs web site, we are not actually doing anything, but it gives an example of an implementation where you may want to intercept the pipeline and do something before and/or after the next middleware in the pipeline is executed (by the next.Invoke() in this example).
What is interesting is seeing how the RequestDelegate is expressed as an anonymous function.
The context parameter gives us access to an HttpContext instance which in turn gives us access to the request and response objects. However, you must not make any changes to the response if another middleware is to be invoked as once the response has started (by a later middleware in the pipeline), any changes to it will cause an exception.
If you do want to return a response, your middleware becomes a terminating or a short-circuiting middleware and does not invoke any further middlewares in the pipeline.
In this post I want to keep the focus on understanding how middleware works with dependency injection lifetimes rather than the mechanics of how the middleware pipeline itself is built and executes each pipeline middleware in turn .
For a deeper dive into how request delegates interact with each other to form a pipeline, I highly recommend Steve Gordon's Deep Dive - How Is The Asp Net Core Middleware Pipeline Built? which goes deep into the chaining of request delegates.
Ultimately, all middleware eventually boils down to becoming a request delegate function that gets passed into the Use method on the IApplicationBuilder implementation instance.
You can write your middleware directly inside an anonymous function (or method) that gets passed to the Use method via a delegate parameter. The call to the Use method is written inside the Configure method of the Startup class (on via the Configure method on the HostBuilder).
This is the approach you usually take if want to do something simple in your middleware that has no dependencies that would need to be resolved by the dependency injection container as the delegate does not interact with the container.
That does not mean that you cannot get the container to resolve instances for you. It just means that you have to explicitly ask the container to resolve a service via the RequestServices property on the HttpContext instance (that is passed as a parameter into the delegate).
This moves you into the realms of the service locater anti-pattern, but given that you are usually creating the delegate within the confines of the application startup, this is not so much of a concern as doing it elsewhere in your application landscape.
As the code is all written in-line, it can become a bit of a pain to read and debug if it is doing many things or you have multiple Use entries as the Configure method becomes a but unwieldy.
To avoid this, you could extract the contents of the Use anonymous function to their own methods within the Startup. However this still limits your middleware to the confines of your project.
in most cases, you will want to make the middleware code self-contained and reusable and take advantage of getting the dependency injection container do the work of resolving instances without having to explicitly call the container (as we've had to in the above example).
This is where the other two styles of writing middleware come into their own by writing classes that encapsulate the functionality that will be called by the pipeline.
Now I have a confession. I have been using .NET Core for quite a while now and, until recently, this had passed me by.
This may be because I learnt about middleware with ASP.NET Core 1.1 and factory style middleware was not a 'thing' until ASP.NET Core 2.0, when it was introduced without any fanfare (it was not included in the What's New in ASP.NET Core for either version 2.0 or 2.1).
If you are interested in how the the factory style was introduced into ASP.NET Core, have a look at this Git issue which shows how it evolved with a lot of commentary from David Fowler
The introduction of factory style middleware brings the following benefits over convention style middleware:
- Activation per request, so allows injection of scoped services into the constructor
- The explicitly defined InvokeAsync method on the interface moves away from the unstructured Invoke/InvokeAsync methods in convention style middleware that allowed additional parameters to be added
- It is more aligned with the way you write other classes in your application as it is based on the premise of constructor injection over method parameter injection
- Whilst there is a default implementation of IMiddlewareFactory that gets registered by default by the default WebHostBuilder, you can write your own implementation of IMiddlewareFactory that may be of use if you are using another container such as AutoFac where you may want to resolve instances using functionality that is not present in the out-of-the-box Microsoft implementation of IServiceProvider
So how do we implement the factory style of middleware ?
The key to this is by writing your middleware class as an implementation of the IMiddleware interface which has a single method, InvokeAsync.
The method has two incoming parameters
- an instance of HttpContext this holds all the request/response information
- a RequestDelegate instance that provides the link to the next middleware in the pipeline to call
Inside the method is where you can do whatever it is that you need to do in your custom middleware. This may be either
- intercepting the request-response chain to do something before and after the next middleware in the chain, such as logging or manipulating the request (or response), or
- acting as terminating middleware that sends a response back (such as the static file middleware) and therefore does not proceed to the next middleware in the pipeline (unless it cannot handle the request).
Lastly, if you are not terminating the pipeline in your middleware, you need to ensure that the request delegate is invoked, passing the HttpContext instance to it as a parameter.
In the above example, I have taken the in-line style we used earlier and refactored it into its own factory-style middleware by implementing the IMiddleware interface. By doing this, I could put it into its own project and share between multiple other projects, avoiding the repetition of the in-line style.
We also benefit from not having to get the instance of ILogger
To use the factory style middleware, there are two things that need to be done to use it in your application.
The first, as with all middleware (regardless of style), is to register the middleware in the Configure method of your Startup class as discussed above (alternatively, you may want to use the Configure method directly on the host builder)
The next step is to go back up in to the ConfigureServices method (either in the StartUp class or directly on the HostBuilder) and register the middleware class with the dependency injection container.
To do this, you need to adhere to two simple rules
- You need to register the middleware with the concrete type as the service, not the IMiddleware interface (or any other interface it may implement)
- Think very carefully lifetime of the registration - for now, we will assume that the lifetime will be scoped (as opposed to transient or singleton) as the purpose of factory middleware is that is is invoked per request.
The second point is important as you could be in danger of creating a captured dependency if you register your middleware as a singleton, but the service types of the parameters in the constructor have shorter lifetimes.
Convention style middleware is the way you will find most examples are written and indeed how most Microsoft written middleware works (and as per the comments in the Git issue, remains for backward compatibility).
As it is well documented in many places on the internet (and of course, in the Microsoft Docs - see Writing Custom ASP.NET Core Middleware), I will concentrate on the key differences with factory style middleware here
The first obvious difference is that the class does not have to implement a specific interface (E.g. IMiddleware). Instead, you expected to adhere to a convention of implementing one of two methods Invoke or InvokeAsync.
- Both methods have the same signature, the naming choice is up to you, though given that both return a Task, it is usual to append Async to asynchronous method names
- You cannot have both Invoke and InvokeAsync methods - this will cause an exception to be thrown
- The first parameter must be of type HttpContext - if this is not present, an exception will the thrown
- The return type must be a Task
So given that the RequestDelegate for the next delegate is no longer passed into the InvokeAsync as a parameter, we need to obtain it from somewhere else. As we are adhering to a convention, we should stick with constructor injection and have the next delegate injected here.
We are still missing our ILogger
When it comes to interactions with dependency injection, the key thing to be aware of is that the instance of our convention-style middleware class is not created by the dependency injection container, even if you register the class in ConfigureServices. Instead, the UseMiddleware extension method uses a combination of reflection and the ActivatorUtilities class to create the instance - once, and only once! - so it is effectively a singleton.
The reason for this is that the code that is of interest to the middleware pipeline is the Invoke/InvokeAsync method, as it is a call to this will be wrapped inside the anonymous function that gets passed to the Use method in the IApplicationBuilder instance. In other words, creating the class instance is a stepping stone to creating the delegate and once created, the class constructor is never interacted with again.
Why does this matter? It comes back to understanding how we obtain dependencies in our custom middleware.
If we specified dependencies in the constructor that have been registered with shorter lifetime that singleton (transient and scoped), we end up with captured dependencies that are locked until the singleton is released (which in the case of middleware is when the web application shuts down).
If you require transient or scoped dependencies, these should be added as parameters to the Invoke/InvokeAsync method.
When the UseMiddleware scans the class through reflection, it does several things such as validating the class adheres to the expected conventions, but the main thing we are interested in is the mapping of our middleware's Invoke or InvokeAsync method to the RequestDelegate signature required by the ApplicationBuilder's Use method.
The mapping is decided by checking if the signature of the Invoke/InvokeAsync method exactly matches the RequestDelegate signature (e.g. it only requires a single parameter of type HttpContext), it will use the method directly as the RequestDelegate function
Otherwise, it will create a wrapping function that matches the RequestDelegate signature, but that then uses a the ActivatorUtilities class to resolve the method parameters from the dependency injection container accessed via the ApplicationBuilder's ApplicationServices property.
At this point, once the UseMiddleware has mapped the RequestDelegate that represents our class's middleware invocation method into the ApplicationBuilder, our middleware is baked into the pipeline.
Whether you use convention or factory based middleware is a design decision to be made based on what your middleware is trying to achieve and what dependencies it needs resolved from the dependency injection container.
- In-line style is find for 'quick and dirty' middleware that you do not plan to reuse and is either terminating or does very little when intercepting the pipelines with few dependencies
- If there are many scoped or transient dependencies, you may want to consider the Factory-style approach for coding simplicity as it aligns with the constructor injection pattern that you are probably more familiar with for getting dependencies from the container and if registered with the correct scope, avoids captured dependencies
- If there are no dependencies, or you know the only dependencies are guaranteed to have a singleton lifetime, you may lean towards convention-style middleware as these can be injected into the container when the pipeline is first built and, as there are no additional parameters to the InvokeAsync method, the method can be used as a direct match to the RequestDelegate function that gets used in the pipeline
- If you are already familiar with using convention-style middleware and specifying transient and scoped dependencies in the Invoke/InvokeAsync parameter list, there is no pressing need to change to the factory-style approach.
I hope this post has been of use. If so, please spread the word by linking to it on Twitter and mentioning me @stevetalkscode.
I have created a demo solution at https://github.com/stevetalkscode/middlewarestyles which you can download and have a play with the different styles of writing middleware and see the effects of different dependency injection lifetime registrations for the factory style vs the singleton captured in conventional style middleware.
I plan to revisit this topic in a future post to dig deeper into the how the different styles can affect start up and request performance and also the memory usage (which in turn affects garbage collection) which may sway the decision between using factory or convention style way of writing middleware.