Create ASP.NET Core Middlewares for Reusable and Modular Code
Introduction
ASP.NET Core handles the incoming requests by using a pipeline (series) of middleware components (Figure 1). A middleware
or middleware component
is commonly implemented as a reusable class which performs a logic (for a specific purpose) to the incoming HTTP request. An in-line middleware
is specified in-line as an anonymous method, but it’s not commonly used.
As we have seen in .NET Nakama (2020, November), ASP.NET Core provides several built-in middleware components for the most common use-cases, such as: Authentication, Authorization, CORS, etc. There are scenarios where we would like to write our own custom middleware and include it to the request pipeline.
We can think of middleware, as a code (logic) that will be executed in every request, before and after our application code (e.g. our API Controller). Middleware can also modify the request to be passed in the next component or stop the pipeline (short-circuit) and return the final response.
By using middleware components in APIs we can:
- Include in a component, the repeated-code-execution from the API Controller operations.
- Ensure that all steps in our code logic will be executed (e.g. by forgetting to include the repeated-code-execution).
- Have reusable and modular code.
- Have clean/thin API Controllers, containing only the intended application code logic calls.
It may sound difficult to create a custom middleware, but it is really not that hard. In the following sections, we will see how to create, use and test a middleware component.
Middleware Categories
A middleware should follow the Explicit Dependencies Principle by exposing its dependencies in its constructor or methods. Meaning that the middleware’s constructor and methods should explicitly require any collaborating objects they need in order to function correctly. An ASP.NET Core middleware can be categforized based on its lifetime (Larkin K. et.al. 2020) as follows:
- Convention-based middleware
- Is constructed once per application lifetime (at application startup as singleton).
- It resolves its dependencies from dependency injection (DI) through constructor parameters.
- Can use and share scoped lifetime services between your middleware and other types. In this case, the scoped services must be added to the
Invoke
orInvokeAsync
method’s signature.
- Factory-based middleware
- Is registered as a scoped service in the application’s service container.
- Is activated per client request (connection). For that reason, the scoped services can be injected into the middleware’s constructor.
- Is a strong typed middleware (by implementing the IMiddleware interface).
Creating a Middleware (Convention-based)
Let’s create a simple convention-based middleware, as an example, which will read the user’s culture language from a custom URL parameter (named as culture
) and it will set it to the current request thread (Anderson R. and Smith S., 2020 May). The selected culture will be available in our application code (API Controller), which we will return as a response header (just to check our scenario). In addition, we will create and use an extension method, which will add our custom middleware component to the request pipeline (exposed through IApplicationBuilder). You can download the source code of this example from GitHub.
To create a middleware class, we must include the following (Anderson R. and Smith S., 2020 May):
- A public constructor with a parameter of type RequestDelegate.
- A public method named
Invoke
orInvokeAsync
. This method must:- Return a
Task
. - Accept a first parameter of type HttpContext.
- Return a
- Additional parameters for the constructor and for the
Invoke
orInvokeAsync
method are populated by DI depending on the middleware’s category. - Optional (but recommended): Create an extension method to expose the middleware through IApplicationBuilder.
In addition, when creating a middleware component, we can choose the following, depending on our scenario.
- Choose whether to pass the HTTP request to the next component in the pipeline or short-circuiting the pipeline.
- When a middleware short-circuits, it’s called a terminal middleware, because it prevents the following middleware from processing the request.
- Can perform work before and after the next component in the pipeline.
The Middleware Component
The conventional-based middleware component of this example passes the HTTP request to the next component and does not perform any work after the next component in the pipeline. The selected middleware’s logic/purpose is to read the culture language from the URL and set it to the current request thread.
In this example, we are returning the selected culture as a response header, just to check that the custom culture has been set, as shown in the following code:
The Middleware Extension
A C# extension method is a static method of a static class, where the “this” modifier is applied to the first parameter. The type of the first parameter will be the type that is extended. The middleware extension method is used to add/chain our custom middleware component to the request pipeline. This is performed by creating an extension method to expose the middleware through IApplicationBuilder. The following code example shows how to create such an extension method.
To use/chain our custom middleware component to the request pipeline, we will call the extension method UseCustomQueryMiddleware
from Startup.Configure
, as shown in the following code example.
Middleware Pipeline
The ASP.NET Core request pipeline consists of a sequence of request delegates (by using the IApplicationBuilder), called one after the other (Figure 1) to handle each HTTP request. Request delegates are configured using the Run
, Map
, and Use
extension methods (Anderson R. and Smith S., 2020 July):
- Use method: Chain the multiple request delegates together. The
next
parameter (in Figure 1) represents thenext
delegate in the pipeline. To short-circuit the pipeline, simply do not call thenext
parameter.- UseWhen method: Chain the request delegates based on the result of any predicate of type
Func<HttpContext, bool>
. This will create different pipeline paths (branches).
- UseWhen method: Chain the request delegates based on the result of any predicate of type
- Run method: Adds a terminal middleware delegate which doesn’t receive a
next
parameter. The firstRun
delegate is always terminal and terminates the pipeline. - Map method: The request pipeline is branched based on matches of the given request path. Meaning that, different code scenarios can be executed based on the comparison of the request path with a given path.
- MapWhen method branches the request pipeline based on the result of any predicate of type
Func<HttpContext, bool>
.
- MapWhen method branches the request pipeline based on the result of any predicate of type
You may think, “OK! But… in which order do I have to chain the request delegates?“. Figure 2 shows the recommended order for the existing middlewares and some custom middlewares at the end of the pipeline (as an example). We can reorder the existing middlewares and inject new custom middlewares as necessary, depending on our scenarios. The order of the middlewares is critical for security, performance, and functionality. For details read the Anderson R. and Smith S. (2020, July) article. The middleware named as Endpoint in Figure 2, executes the filter-pipeline, which we will investigate in a future article.
Testing a Middleware
There are different ways to code automated tests for a middleware component. One way is to use the TestServer class which is described in detail in Ross C. (2020), which allows you to:
- Instantiate an application pipeline containing only the components that you need to test your middleware.
- Send custom requests to verify middleware behavior.
For this example, we wanted to test the scenarios in which the culture was either empty or invalid. There are other scenarios that can be tested, but for the sake of simplicity of this example are not included. To perform these tests, we started by including an additional xUnit Test Project
in our solution with the name CustomMiddleware.Tests
.
To set up the TestServer we can create a function (e.g. named as StartTestWebHostAsync
) that will be used from the test scenarios to build and start a host that uses the TestServer, in which we can:
- Add the services that are needed from the middleware.
- Configure the processing pipeline to use the middleware for the test scenarios.
The following code shows how to test the scenario is which an invalid culture is provided. In this case, it’s expected from the code to throw a CultureNotFoundException
exception with a specific error message.
Summary
A middleware
is commonly implemented as a class to perform logic-actions to the incoming HTTP request. By using middlewares, we can create reusable and modular code that is injected in the request pipeline. This will help us to achieve clean/thin API Controllers, containing only the intended application code logic calls. Usually, in order to easily inject our custom middleware to the request pipeline, we are creating an extension method to expose the middleware through IApplicationBuilder
.
Middlewares can be categorized based on its lifetime as Convention-based middleware
when it’s registered as a singleton and Factory-based middleware
when it’s registered as a scoped service (to be activated per client request).
To test a middleware, the TestServer
class can be used to instantiate an application pipeline containing only the components that are needed to send custom requests to verify the middleware’s behavior.
So, now it is time to check your API controllers and see in which cases you can use middlewares to make your code reusable and modular.
References
- .NET Nakama (2020, November 4). ASP.NET Core Web API Fundamentals. https://www.dotnetnakama.com/blog/asp-dotnetcore-webapi-fundamentals/
- Larkin K. et.al. (2020, July 21). Dependency injection in ASP.NET Core - Service lifetimes. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1#service-lifetimes
- Anderson R. and Smith S. (2020, May 18). Write custom ASP.NET Core middleware. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-3.1
- Anderson R. and Smith S. (2020, July 20). ASP.NET Core Middleware. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1
- Ross C. (2020, May 12). Test ASP.NET Core middleware. https://docs.microsoft.com/en-us/aspnet/core/test/middleware?view=aspnetcore-3.1
If you liked this article (or not), do not hesitate to leave comments, questions, suggestions, complaints, or just say Hi in the section below. Don't be a stranger 😉!
Dont't forget to follow my feed and be a .NET Nakama. Have a nice day 😁.