What is a Middleware and how to use it in ASP.NET Core?

10/02/2023
.NETASP.NETC#

Did you ever ask yourself: What is a middleware, and why should I use it?

If so, this blog post is exactly for you. We will see where we could use a middleware and how we can use the Dependecy Injection container of ASP.NET Core.

What is a middleware?

Middleware is a software component that acts as an intermediary between the request and response in the application’s request pipeline. In other words, it is a component that sits between the incoming HTTP request and the application’s response and performs some action on the request or response before passing it to the next component in the pipeline.

Middleware is used to perform a wide range of tasks such as authentication, logging, exception handling, and URL rewriting. You can think of middleware as an application’s plumbing that connects different components and services together to create a fully functional application.

Imagine it's Friday night and you want to go out to a dance club. Often you have a bouncer at the entrance that checks if you are in a "fitting" state. The bouncer would be a middleware. If we bring this analogy back to ASP.NET Core, we have an AuthenticationMiddleware that does the same. It checks if the user is authenticated and, if not, can cancel the request. The last bid is essential. A middleware can either do something and pass on the request to the next middleware or controller or it can also directly abort the request and send an appropriate HTTP Code (like 401 Unauthorized).

If you are using something like ASP.NET Core Web API, then at the end, it will land at the controller and goes back the chain in reversed direction.

Process

By the way the design principle behind this is called: Chain of responsibility.I have a eBook here that explains that design principle

Implementation

Let's assume you have the following requirement: Your client has to send you a specific header, in our case a "tenant id" that is essential for later processing. Now, of course we could add logic inside our controller to make that work. But you might have multiple controllers, so you must add that to every single controller. The solution to that: Let's make an abstract TenantController : Controller. But what if we also want log certain events? Create another abstract controller? How to combine them? As you can see, solving everything in our controller will not work. Especially because we violate some of the SOLID principles:

  • Single responsibility principle: Your controller has one concern and one concern only. For example retrieving blog posts or creating a user. Why should be also responsible for authentication or getting some tenant information from the header of a user request? A middleware would be responsible for exactly one concern and that concern only.
  • Open close principle: Adding new requirements would result in adding or adjusting a new controller. With a middleware you would add or remove a middleware to achieve your goals.

There are multiple ways to create a middleware, I will show you the "most versatile" way that gives you all the options without restricting you

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
      _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
      // Do something here
      await _next(context);
    }
}

The usage would be:

app.UseMiddleware<MyMiddleware>();

Keep in mind that the ordering of UseMiddleware matters. This is the exact order in which your middleware gets called:

app.UseMiddleware<Middleware1>(); // _next(context) would point to Middleware2
app.UseMiddleware<Middleware2>(); // _next(context) would point to Middleware3
app.UseMiddleware<Middleware3>(); // _next(context) would point to Middleware4
// ...

You might wonder, why there is no interface from which we have to inherit. The reason is simple: Flexibility. Our middleware now does basically nothing, but you might want to retrieve services from your DI container and here middleware is a bit different than your average controller.

Lifetime and Dependency Injection

The most important fact at the beginning: Your middleware is created at the startup of your application and lives until it dies, making it practically speaking a singleton. So whatever you pass in into your middleware as a constructor dependency has to be a singleton. You can not pass in scoped services in your constructor. Well, that is a pity. Don't worry - here is where the flexibility comes into play.

You can pass in arbitrary services in your InvokeAsync method. InvokeAsync gets called for every request, making this method scoped lifetime. Here is where we can set things specific to that request. Let's see our initial example of retrieving a tenant id per request. We can do the following to make that work:

public class TenantMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<TenantMiddleware> _logger;

    // The logger interface is registered as singleton,
    // so we can retrieve it as constructor dependency
    public TenantMiddleware(RequestDelegate next, ILogger<TenantMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    // The ITenantSetter is used per request --> scoped service
    public async Task InvokeAsync(HttpContext context, ITenantSetter tenantSetter)
    {
        // If the header is not present
        // we directly abort the request and return an error to the client
        if (!context.Request.Headers.TryGetValue("TenantId", out var header)
        {
            _logger.LogError("No tenant id present");
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("No tenant id present");
            return;
        }
        
        tenantSetter.SetTenant(header);
        await _next(context);
    }
}

Conclusion

In conclusion, middleware is a powerful and flexible concept in ASP.NET Core that provides a way to handle HTTP requests and responses in a modular and composable way. Using middleware, you can build complex and scalable applications that are easy to maintain and extend.

15
An error has occurred. This application may no longer respond until reloaded. Reload x