The only .NET middleware guide you’ll need
Published: 2024-01-09 17:35:03

> Cover Image by <a href="https://pixabay.com/users/goumbik-3752482/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2717063">Lukas</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2717063">Pixabay</a>

Olá humanos!

In a nutshell, middleware is any service that runs between the client and server. For example, an authorization server that will verify if the client has the right to access a resource.

In the .NET API domain, just like with NodeJS, it is any code built to run between when your API receives a request from the client until it returns a response.

A good example is the CORS middleware. If enabled, it verifies if the origin, method, headers, and credentials are allowed to process with your API. If the request is invalid, it prevents it from reaching the requested endpoint.

To use middleware, you need to do that in the `Program.cs` right after your web application is built with `builder.Build()` and a `WebApplication` instance is generated.

Now you can add middlewares, like:
```cshaprt
app.UseAuthorization();
app.UseCors();
app.UseHttpsRedirection();
```

## Custom middleware

Previously, we talked about how to use pre-built middlewares. But what if you want to write your own implementation?

A custom middleware will give you the power to read and manipulate the request and the response.

To do that, we have multiple ways of doing it.

### Inline middleware

The simplest way of writing a custom middleware is to write it directly in the `WebApplication` through the method Use, in the `Program.cs`.

```csharp
var app = builder.Build();

app.Use(async (context, next) =>
{
    var method = context.Request.Method;
    var path = context.Request.Path;
    
    var startTime = Stopwatch.GetTimestamp();
    
    await next(context);
    
    var responseCode = context.Response.StatusCode;

    Console.WriteLine("Request: {0} {1} response with Status {2} within {3} ms", method, path, responseCode, elapsedTime.TotalMilliseconds);
});
```

The middleware above prints to the console how long it took for a request to respond.

### Middleware class

A better and more recommended approach to writing your custom middleware is to define it in a class.

To do that, you can create a class and implement the method `InvokeAsync`:

```csharp
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;

    public RequestLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
      var method = context.Request.Method;
      var path = context.Request.Path;
    
      var startTime = Stopwatch.GetTimestamp();
    
      await _next(context);
    
      var elapsedTime = Stopwatch.GetElapsedTime(startTime);

      var responseCode = context.Response.StatusCode;

      Console.WriteLine("Request: {0} {1} response with Status {2} within {3} ms", method, path, responseCode, elapsedTime.TotalMilliseconds);
    }
}
```

Now you can register it in `the Program.cs`:

```csharp
var app = builder.Build();

app.UseMiddleware<RequestLoggingMiddleware>();
```

With a middleware class, you get:

* Better separation of responsibilities
* Better code organization
* Dependency injection usage

### Factory-based activated

This is similar to how the middleware class works, but it adds a strong-typed layer to it.

Here, you make your middleware class implement the `IMiddleware` interface, which has the `InvokeAsync` method to be implemented.

Another difference is that instead of having the `RequestDelegate` in the constructor, it will always be passed in the `InvokeAsync` method.

```csharp
public class RequestLoggingMiddleware : IMiddleware
{    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
      var method = context.Request.Method;
      var path = context.Request.Path;
    
      var startTime = Stopwatch.GetTimestamp();
    
      await next(context);
    
      var elapsedTime = Stopwatch.GetElapsedTime(startTime);

      var responseCode = context.Response.StatusCode;

      Console.WriteLine("Request: {0} {1} response with Status {2} within {3} ms", method, path, responseCode, elapsedTime.TotalMilliseconds);
    }
}
```

And then, you just need to register it in the DI container.

```csharp
builder.Services.AddTransient<ExampleMiddleware>();
```

## Middleware Class

As mentioned above, using middleware class you can get more out of designing a custom middleware.

### Lifecycle and dependency injection

A middleware class is built during application startup, which means that the instance itself is a Singleton and the InvokeAsync method is called on every request.

We have two methods of dependency injection:

* Constructor — The dependencies are injected during middleware construction
* InvokeAsync / Invoke — The dependencies are injected each time the middleware is invoked

For `Singleton` and `Transient`, you can use both types of injection

For `Scoped` you must use `InvokeAsync`/`Invoke` method injection. It will throw a runtime error when invoking the middleware.

So the best practice for injecting are:

* `Singleton` — Constructor injection
* `Scoped` and `Transient` — `InvokeAsync`/`Invoke` injection.

The argument for the `Transient` instance scope is that you don’t want to extend its lifecycle to a singleton if not required.

An example of how to inject:

```csharp
public class InjectionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ISingletonService _singletonService;

    public InjectionMiddleware(
      RequestDelegate next,
      ISingletonService singletonService)
    {
        _next = next;
        _singletonService = singletonService;
    }

    public async Task InvokeAsync(
      HttpContext context,
      IScopedService scopedService,
      ITransientService transientService)
    {
      _singletonService.DoSomething();
      scopedService.DoSomething();
      transientService.DoSomething();
    }
}
```

### Middleware custom arguments
You can also pass custom arguments to your custom middleware during application startup, like a configuration, the deployment environment, etc.

To do that, you can add it to the middleware class constructor after the dependencies that the DI container will handle.

So, taking the previous example, we can do:

```csharp
public class InjectionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ISingletonService _singletonService;
    private readonly string _environment;

    public InjectionMiddleware(
      RequestDelegate next,
      ISingletonService singletonService,
      string environment)
    {
        _next = next;
        _singletonService = singletonService;
        _environment = environment;
    }

    public async Task InvokeAsync(
      HttpContext context,
      IScopedService scopedService,
      ITransientService transientService)
    {
      Console.WriteLine("Doing something in environment {0}", _environment);

      _singletonService.DoSomething();
      scopedService.DoSomething();
      transientService.DoSomething();
    }
}
```

And then you can pass this argument during middleware registration:

```csharp
app.UseMiddleware<InjectionMiddleware>("Production");
```

## Factory-based middleware

As exemplified earlier, it is easy to define a strong-typed middleware with the factory-based approach. The advantages of using this over the conventional are:

* Strong-typed class — It is much easier to get the middleware correctly built when you need to implement the IMiddleware interface.
* Testability - enables you to test, inject, and mock your middleware in unit or integration tests to validate behaviors.

It is also important to note that injecting the middleware as a service removes the service lifecycle constraints from the conventional method. This means you can inject dependencies of all lifecycles in the constructor.

### Drawbacks

One of the drawbacks is that you are not able to pass custom parameters to the middleware in the `UseMiddleware` method anymore.

## Order matters! The request/response flow

When registering middlewares, you need to remember that the order of registration matters. The ones registered first will process the request first and the response last.

See the flow graph below:

![Middleware flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lgrfd9pwc0hckw2klszn.png)

Let’s take a look at the example middleware:

```csharp
public class ExampleMiddleware : IMiddleware
{    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        Console.WriteLine("I run before the request is processed.");
        
        await next(context);
        
        Console.WriteLine("I run after I receive a response.");
    }
}
```

Anything that runs before `await _next(context)` will process anything before it dispatches the request to the next middleware.

Anything after `await _next(context)` will process anything after the subsequent middlewares have finished processing.

Let’s say we have two middleware:

```csharp
public class ExampleMiddleware
{    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        Console.WriteLine("-> Example Middleware BEFORE Dispatching request");
        
        await next(context);
        
        Console.WriteLine("<- Example Middleware AFTER dispatching");
    }
}
```

and

```csharp
public class AnotherMiddleware
{    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        Console.WriteLine("-> Another Middleware BEFORE Dispatching request");
        
        await next(context);
        
        Console.WriteLine("<- Another Middleware AFTER dispatching");
    }
}
```

Now let’s register the services with:

```csharp
builder.Services.AddTransient<ExampleMiddleware>();
builder.Services.AddTransient<AnotherMiddleware>();
```

And the middleware:

```csharp
app.UseMiddleware<ExampleMiddleware>();
app.UseMiddleware<AnotherMiddleware>();
```

When you send a request to the API, you’ll see the following in the terminal:

```text
-> Example Middleware BEFORE Dispatching request
-> Another Middleware BEFORE Dispatching request
...
<- Another Middleware AFTER dispatching
<- Example Middleware AFTER dispatching
```

This means that `ExampleMiddleware` is first to process the request but last to process the response. While `AnotherMiddleware` is last to process the request but first to process the response.

Knowing that, you can use this to your advantage in many ways, like creating the following middleware for global exception handling:

```csharp
public class GlobalExceptionHandlingMiddleware : IMiddleware
{    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception exception)
        {
            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync("{\"message\": \"Something went wrong.\"}");
        }
    }
}
```

And then you register it before all other middlewares with:

```csharp
builder.Services.AddTransient<GlobalExceptionHandlingMiddleware>();

var app = builder.Build();

app.UseMiddleware<GlobalExceptionHandlingMiddleware>();
```

With this, any unhandled exception thrown during the request processing will be caught through this middleware that will return a proper JSON response.

> Note that this is just an example of global exception handling.

## Conclusion

In the domain of .NET APIs, middleware acts as a critical intermediary between client requests and server responses. It plays a pivotal role in managing and altering these interactions. CORS middleware, for instance, ensures the validity of requests, preventing unauthorized access to API endpoints.

The implementation of middleware involves various approaches. While inline middleware offers simplicity by embedding code directly into the application, creating custom middleware through classes provides better code organization, and separation of concerns, and promotes the utilization of dependency injection.

Understanding the order of middleware registration is crucial, as it directly impacts the flow of requests and responses. The sequence determines which middleware processes requests first and responses last. This order can be strategically used, such as employing global exception handling middleware to capture and manage unhandled exceptions across the application.

Middleware can be tailored with custom arguments, allowing for flexibility in handling different environments or configurations. Proper dependency injection methods, aligned with the lifecycle of services (Singleton, Transient, Scoped), are essential for efficient middleware implementation.

By comprehending and harnessing the power of middleware, developers can streamline request-response processes, manage exceptions, and fine-tune their .NET API applications, enhancing overall performance and security.