DEV Community

Cover image for The only .NET middleware guide you’ll need
Andre Lopes
Andre Lopes

Posted on

The only .NET middleware guide you’ll need

Cover Image by Lukas from Pixabay

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:

app.UseAuthorization();
app.UseCors();
app.UseHttpsRedirection();
Enter fullscreen mode Exit fullscreen mode

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.

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);
});
Enter fullscreen mode Exit fullscreen mode

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:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you can register it in the Program.cs:

var app = builder.Build();

app.UseMiddleware<RequestLoggingMiddleware>();
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

builder.Services.AddTransient<ExampleMiddleware>();
Enter fullscreen mode Exit fullscreen mode

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 TransientInvokeAsync/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:

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

And then you can pass this argument during middleware registration:

app.UseMiddleware<InjectionMiddleware>("Production");
Enter fullscreen mode Exit fullscreen mode

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

Let’s take a look at the example middleware:

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

and

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let’s register the services with:

builder.Services.AddTransient<ExampleMiddleware>();
builder.Services.AddTransient<AnotherMiddleware>();
Enter fullscreen mode Exit fullscreen mode

And the middleware:

app.UseMiddleware<ExampleMiddleware>();
app.UseMiddleware<AnotherMiddleware>();
Enter fullscreen mode Exit fullscreen mode

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

-> Example Middleware BEFORE Dispatching request
-> Another Middleware BEFORE Dispatching request
...
<- Another Middleware AFTER dispatching
<- Example Middleware AFTER dispatching
Enter fullscreen mode Exit fullscreen mode

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:

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.\"}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And then you register it before all other middlewares with:

builder.Services.AddTransient<GlobalExceptionHandlingMiddleware>();

var app = builder.Build();

app.UseMiddleware<GlobalExceptionHandlingMiddleware>();
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)