Writing factory-based ASP.NET Core middleware - part 4

Writing factory-based ASP.NET Core middleware - part 4

Introduction

The previous articles discovered how middleware components for ASP.NET Core can be written either inline by using lambda expressions or by writing convention-based middleware classes. This post will introduce factory-based middleware components.


Articles in this series

Part 1 - Introduction to ASP.NET Core middleware
Part 2 - Writing inline ASP.NET Core middleware using lambda expressions
Part 3 - Writing convention-based ASP.NET Core middleware
Part 4 - Writing factory-based ASP.NET Core middleware
Part 5 - Testing ASP.NET Core middleware


Factory-based middleware

Since ASP.NET Core 2, there has been a new way to write middleware, which are components that are getting instantiated by ASP.NET Core due to the factory pattern.

The interfaces IMiddleware and IMiddlewareFactory are at its core. The interface IMiddlewareFactory defines the factory method Create, which returns a new instance of type IMiddleware. This happens for each new request at runtime!

// Request handling method
Task IMiddleware.InvokeAsync(HttpContext context, RequestDelegate next);

// Creates a middleware instance for each request
public IMiddleware? IMiddlewareFactory.Create(Type middlewareType);
IMiddleware & IMiddlewareFactory interfaces

Let's look at an example. The following middleware implements the IMiddleware interface and creates a new log entry for each HTTP response (e.g. GET /foobar => 200).

// LoggingMiddleware.cs
public class LoggingMiddleware : IMiddleware
{
    private readonly ILogger _logger;
    
    public LoggingMiddleware(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<LoggingMiddleware>();
    }
    
    // Fulfill interface contract
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // Pass control to next middleware
        await next.Invoke(context);
        
        // Do some logging on returning call
        _logger.LogInformation($"{context.Request?.Method} {context.Request?.Path.Value} => {context.Response?.StatusCode}");
    }
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);

ConfigureConfiguration(builder.Configuration);
ConfigureServices(builder.Services);

var app = builder.Build();

ConfigureMiddleware(app, app.Services);
ConfigureEndpoints(app, app.Services);

app.Run();

void ConfigureConfiguration(ConfigurationManager configuration) {} 
void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<LoggingMiddleware>();
}
void ConfigureMiddleware(IApplicationBuilder app, IServiceProvider services)
{
    app.UseMiddleware<LoggingMiddleware>();
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Terminal middleware\n");
    });
}
void ConfigureEndpoints(IEndpointRouteBuilder app, IServiceProvider services){}
Exemplary factory-based middleware

The current HTTP context and subsequent middleware are getting passed via InvokeAsync. Afterward, execution control is passed onto the next middleware in the pipeline without further action.  Only after the terminating endpoint has created a response, the LoggingMiddleware gets to action and generates a log entry based on the HTTP response.

Lifetime & Registration

What is special about this type of middleware is its lifetime. Using the internal IMiddlewareFactory implementation, a new instance of the LoggingMiddleware is created for each HTTP request.

This behavior is in contrast to a convention-based middleware, which is created once when starting the ASP.NET Core application with a singleton lifetime. Further, factory-based middleware classes require explicit registration with the DI container.

services.AddScoped<LoggingMiddleware>();
Registering middleware

After that, they can be added to the request pipeline as usual.

app.UseMiddleware<LoggingMiddleware>();
Adding middleware to the pipeline

Injecting dependencies

By implementing the IMiddleware interface, no additional dependencies can be introduced via the InvokeAsync method, as otherwise, the interface's contract wouldn't be fulfilled.

However, the constructor can be used to inject dependencies, especially with a scoped lifetime. This isn't possible with convention-based middleware classes because of the resulting captive-dependencies-anti-pattern.

Below is an example to clarify, in which a singleton and scoped service are introduced via the constructor. The dependencies must be registered accordingly with the DI container.

// LoggingMiddleware.cs
public class LoggingMiddleware : IMiddleware
{
    private readonly ILogger _logger;
    private readonly IAmScoped _service1; 
    private readonly IAmSingleton _service2;
    
    public LoggingMiddleware(ILoggerFactory loggerFactory, IAmScoped service1, IAmSingleton service2)
    {
        _logger = loggerFactory.CreateLogger<LoggingMiddleware>();
        _service1 = service1; 
        _service2 = service2; 
    }
    
    // Fulfill interface contract
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // make use of service1/service2
    
        // Pass control to next middleware
        await next.Invoke(context);
        
        // Do some logging on returning call
        _logger.LogInformation($"{context.Request?.Method} {context.Request?.Path.Value} => {context.Response?.StatusCode}");
    }
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);

ConfigureConfiguration(builder.Configuration);
ConfigureServices(builder.Services);

var app = builder.Build();

ConfigureMiddleware(app, app.Services);
ConfigureEndpoints(app, app.Services);

app.Run();

void ConfigureConfiguration(ConfigurationManager configuration) {} 
void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IAmScoped, Service1>(); 
    services.AddSingleton<IAmSingleton, Service2>();
    services.AddScoped<LoggingMiddleware>();
}
void ConfigureMiddleware(IApplicationBuilder app, IServiceProvider services)
{
    app.UseMiddleware<LoggingMiddleware>();
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Terminal middleware\n");
    });
}
void ConfigureEndpoints(IEndpointRouteBuilder app, IServiceProvider services){}

Passing parameters

We've already seen how UseMiddleware can be used to add a middleware component to the pipeline. We have also seen how primitive data types can be passed to a convention-based middleware for configuration purposes.

There is a small limitation here for factory-based middleware. Primitive data types such as strings, integers, etc. cannot easily be passed via the UseMiddleware method. However, there are several alternatives at hand.

On the one hand, the registration with the DI container can be changed.

services.AddScoped(x => ActivatorUtilities.CreateInstance<LoggingMiddleware>(x, 2, … ));

On the other hand, the configuration parameters can be encapsulated in a separate class and then registered regularly with the DI container (of course, this also applies to convention-based middleware).

// MyConfig.cs
public class MyConfig { public int Count { get; set; }}

// MyMiddleware.cs
public class MyMiddleware : IMiddleware
{
    private readonly MyConfig _config
    public class MyMiddleware(MyConfig config) => _config = config;
    // … 
}

// Program.cs
builder.Services.AddSingleton<MyConfig>();
builder.Services.AddScoped<MyMiddleware>();
// … 
Using a configuration class

Conclusion

In general, you should use these factory-based middleware components if they require dependencies with a scoped lifetime. Let me summarize the most important aspects:

  • Allows for an easy unit- & integration testing
  • Each HTTP request will create a new instance of a factory-based middleware (scoped)
  • Factory-based middleware classes require explicit registration
  • Dependency injection only by the constructor (singleton & scoped)
  • You can't pass primitive types via UseMiddleware

Thanks for reading! 👨🏻‍💻

Further reading

Introduction to ASP.NET Core middleware - part 1
This article explains the high-level concepts of an ASP.NET Core middleware and is the first in a series of three articles.
Writing inline ASP.NET Core middleware using lambda expressions - part 2
This article is part of a series about ASP.NET Core middleware and explains how inline-middleware can be written using lambda expressions
Writing convention-based ASP.NET Core middleware - part 3
This article is part of a series that explains how to develop convention-based middleware components with ASP.NET Core
Writing factory-based ASP.NET Core middleware - part 4
This article is part of a series that explains how to develop factory-based ASP.NET Core middleware
Testing ASP.NET Core middleware - part 5
Introduction This is the last in a series of posts that explains how ASP.NET Core middleware components can be written. Articles in this series Part 1 - Introduction to ASP.NET Core middleware Part 2 - Writing inline ASP.NET Core middleware using lambda expressions Part 3 - Writing
Show Comments