First, let's remind ourselves how conventions-based middleware works. The two conventions that must be applied to a conventions-based middleware class
are to declare a constructor that takes a RequestDelegate
as a parameter
representing the next middleware in the pipeline, and a method named Invoke
or
InvokeAsync
that returns a Task
and has at least one parameter, the first being
an HttpContext
.
The following code illustrates a middleware class that implements these
conventions and logs the value of the visitor’s IP address:
public class ConventionalIpAddressMiddleware { private readonly RequestDelegate _next; public ConventionalIpAddressMiddleware(RequestDelegate next) => _next = next; public async Task InvokeAsync(HttpContext context, ILogger<ConventionalIpAddressMiddleware> logger) { var ipAddress = context.Connection.RemoteIpAddress; logger.LogInformation("Visitor is from {ipAddress}", ipAddress); await _next(context); } }
- The constructor takes a
RequestDelegate
as a parameter - The
InvokeAsync
method returns a Task and has anHttpContext
as a first parameter. Any additional services are injected into theInvoke/InvokeAsync
method after theHttpContext
- Processing happens within the
InvokeAsync
method - The
RequestDelegate
is invoked, passing control to the next middleware in the pipeline
The middleware class is added to the pipeline via the generic UseMiddleware
method on
the IApplicationBuilder
:
app.UseMiddleware<ConventionalIpAddressMiddleware>();
In this particular case, you will probably want to register this middleware after the static files middleware so that it doesn't log the IP address for the same visitor for every file that is requested.
Middleware that follows the conventions-based approach is created as a singleton
when the application first starts up, which means that there is only one
instance created for the lifetime of the application. This instance is used for
every request that reaches it. If your middleware relies on scoped or transient dependencies, you must inject them via the
Invoke
method so that they are resolved each time the method is
called by the framework. If you inject them via the constructor, they will be
captured as singletons. The logger in this example is itself a singleton, so it
can be provided via the Invoke
method or the constructor.
Implementing IMiddleware
The alternative approach to writing middleware classes involves implementing the
IMiddleware
interface. The IMiddleware
interface exposes one method:
Task InvokeAsync(HttpContext context, RequestDelegate next)
Here is the IpAddressMiddleware implemented as IMiddleware
:
public class IMiddlewareIpAddressMiddleware : IMiddleware { private readonly ILogger<IMiddlewareIpAddressMiddleware> _logger; public IMiddlewareIpAddressMiddleware(ILogger<IMiddlewareIpAddressMiddleware> logger) { _logger = logger; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var ipAddress = context.Connection.RemoteIpAddress; _logger.LogInformation("Visitor is from {ipAddress}", ipAddress); await next(context); } }
- The middleware class implement
IMiddleware
the interface - Dependencies are injected into the constructor
InvokeAsync
takes anHttpContext
and aRequestDelegate
as parameters
The InvokeAsync
implementation is very similar to the one that was written using the
conventions based approach, except that this time the parameters are an
HttpContext
and a RequestDelegate
. Any services that the class depends on are
injected via the middleware class constructor, so fields are required to hold
instances of the injected service. This middleware is registered in exactly the same way as
the conventions-based example, via the UseMiddleware
methods or an extension
method:
app.UseMiddleware<IMiddlewareIpAddressMiddleware>();But an additional step is also required for
IMiddleware
based components
- they must also be registered with the application’s service container. In this
example, the middleware is registered with a scoped lifetime.
builder.Services.AddScoped<IMiddlewareIpAddressMiddleware>();
So why are there two different ways to create middleware classes, and which one
should you use? Well, the convention-based approach requires that you learn the
specific conventions and remember them. There is no compile time checking to
ensure that your middleware implements the conventions correctly. This approach
is known as weakly-typed. Typically, the first time you discover that you forgot
to name your method Invoke
or InvokeAsync
, or that the first parameter should be
an HttpContext
will be when you try to run the application and it falls over. If
you are anything like me, you often find that you have to refer back to
documentation to remind yourself of the convention details, especially if you
don't author middleware that often.
The second approach results in strongly typed middleware because you have to
implement the members of the IMiddleware
interface otherwise the compiler
complains and your application won’t even build. So the IMiddleware
approach is
less error prone and potentially quicker to implement, although you do have to
take the extra step of registering the middleware with the service container.
There is another difference between the two approaches. I mentioned earlier that
convention-based middleware is always instantiated as a singleton when the pipeline is
first built. IMiddleware
components are retrieved from the service
container and instantiated whenever they are needed by a
component that implements the IMiddlewareFactory
interface and this
difference has ramifications for services that the middleware depends on, based
on their registered lifetime. In this example, the middleware has a scoped
lifetime, so it can safely accept scoped services via its constructor.
It should be pointed out that the majority of existing framework middleware is authored
using the convention-based approach. This is mainly because most of it was
written before IMiddleware
was introduced in .NET Core 3.0. Having said that, there is no
indication that the framework designers feel any need to migrate existing
middleware to IMiddleware
.