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!
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
).
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.
After that, they can be added to the request pipeline as usual.
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).
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! 👨🏻💻