.NET Nakama

Improving your .NET skills

Create ASP.NET Core Middlewares for Reusable and Modular Code

December 04, 2020 (~14 Minute Read)
MIDDLEWARE ASP.NET CORE REQUEST PIPELINE EXTENSION TESTING

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.

The concept of the ASP.NET Core request pipeline.
Figure 1. - The concept of the ASP.NET Core request pipeline.

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 or InvokeAsync 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 or InvokeAsync. This method must:
    • Return a Task.
    • Accept a first parameter of type HttpContext.
  • Additional parameters for the constructor and for the Invoke or InvokeAsync 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.

// Project: CustomMiddleware

// Filename: CustomQueryMiddleware.cs

public class CustomQueryMiddleware
{
    private readonly RequestDelegate _next;

    public CustomQueryMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        // STEP 1.

        // Add your middleware's logic here (BEFORE the next delegate/middleware in the pipeline).        

        // Note: You can perform changes to the "Response" but the values may be replaced

        //  from the next delegate/middleware or from the application code.

        // Note: We have access to the HttpContext, thus, to the following:

        // - context.User

        // - context.Session

        // - context.Request.Query

        // - context.Request.Header

        // - context.Request.Form

        // - etc.



        // For example, the following code read the culture from the URL and set it to the current request thread.

        // Reference: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-3.1#middleware-class

        var cultureQuery = context.Request.Query["culture"];
    
        if (string.IsNullOrWhiteSpace(cultureQuery))
        {
            throw new CultureNotFoundException("The query parameter 'culture' has no value.");
        }
        else
        {
            var culture = new CultureInfo(cultureQuery);
            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }                        
                        
        // STEP 2.

        // Call the next delegate/middleware in the pipeline

        await _next(context);


        // STEP 3. (if needed)

        // Add your middleware's logic here (AFTER the next delegate/middleware in the pipeline),

        // that doesn't write to the Response (e.g. logging).

    }
}

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:

// Project: WebApi

// Filename: Controllers/ WeatherForecastController.cs

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    // ...just to check that the custom culture has been set.

    Response.Headers.Add("custom-culture", CultureInfo.CurrentCulture.Name);
            
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

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.

// Project: CustomMiddleware

// Filename: CustomQueryMiddlewareExtensions.cs

public static class CustomQueryMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomQueryMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CustomQueryMiddleware>();
    }
}

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.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    // Add our custom middleware component to the request pipeline.

    app.UseCustomQueryMiddleware();
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

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 the next delegate in the pipeline. To short-circuit the pipeline, simply do not call the next 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).
  • Run method: Adds a terminal middleware delegate which doesn’t receive a next parameter. The first Run 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>.

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.

The recommended order of the existing middlewares.
Figure 2. - The recommended order of the existing middlewares (Source).

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.
private Task<IHost> StartTestWebHostAsync()
{
    return new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    // Here we can add the services that are needed from the middleware, for example:

                    // services.AddMyServices();

                })
                .Configure(app =>
                {
                    app.UseCustomQueryMiddleware();
                });
        })
        .StartAsync();
}

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>

/// Scenario:

/// The "culture" query parameter contains an invalid culture name.

/// 

/// Action:

/// An CultureNotFoundException is thrown.

/// </summary>

[Fact]
public async Task AnCultureNotFoundExceptionIsThrown_WhenCultureIsInvalid()
{
    // Arrange

    string invalidCulture = "xx-XXX";
    using var host = await StartTestWebHostAsync();

    // Act:

    // Send a request using HttpClient

    var exception = await Assert.ThrowsAsync<CultureNotFoundException>(() => host.GetTestClient().GetAsync($"/?culture={invalidCulture}"));
    
    // Assert

    Assert.Equal("Culture is not supported. (Parameter 'name')\r\nxx-XXX is an invalid culture identifier.", exception.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

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 😁.