Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing dependency injection and manually-passed parameter in ASP.NET Core middleware constructor

Tags:

I'm writing a custom middleware for ASP.NET Core 2.2. According to Microsoft Docs on writing custom middlewares:

Middleware components can resolve their dependencies from dependency injection (DI) through constructor parameters. UseMiddleware<T> can also accept additional parameters directly.

This seems all good, but it doesn't say what happens when I mix the two ways, e.g. use DI and pass parameters in UseMiddleware<T>. For example, I have the following middleware:

public class CustomMiddleware
{
    public CustomMiddleware(RequestDelegate next, ILogger<CustomMiddleware> logger, CustomMiddlewareOptions options)
    { 
        ...
    }

    public async Task InvokeAsync(HttpContext context)
    {
        ...
    }

where logger is provided by DI and options is provided like the following:

app.UseMiddleware<CustomMiddleware>(new CustomMiddlewareOptions());

My own testing with 2.2 seems to show that this works fine, and the order of the parameters in the constructor doesn't matter (I can place DI parameter before or after manually-passed parameter, or even in between two manually-passed parameters). But I'm looking for some assurances that what I'm doing is OK. It would be really great if anyone could point to some docs or source code that supports this sort of usage. Thanks!

like image 613
scharnyw Avatar asked Dec 18 '19 09:12

scharnyw


1 Answers

My own testing with 2.2 seems to show that this works fine, and the order of the parameters in the constructor doesn't matter (I can place DI parameter before or after manually-passed parameter, or even in between two manually-passed parameters). But I'm looking for some assurances

Yes. After reading the source code, I would say it is fine.

How it works

Your CustomMiddleware is a by-convention Middleware (different from the Factory-based middleware), which is activated by ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs):

var ctorArgs = new object[args.Length + 1];
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length);     // 
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);

Here the the args (Given Arguments) is the argument array that you pass into the UseMiddleware<CustomMiddleware>(args) (without the next).

And there're two stages when preparing the constructor arguments:

  1. Match the given args against the construct parameter types. And set values when the type matches. See source code Here
  2. Fill the null element using ServiceProvider.GetRequiredService<SomeService>().See source code here. If the service instance is still null, then use the default value.

For example, let's say :

  1. You have a by-convention middleware whose constructor has the following signature:
    public CustomMiddleware(RequestDelegate next, A a, B b, C c, D d, E e){ ... }
    
  2. And then we pass in two arguments when registering the middleware :

    app.UseMiddleware(c, a) 
    

    where c is an instance of C Type, and a is an instance of A Type. So the givenParameters Array is [next,c, a]

To create an instance of CustomMiddleware, the compiler needs to know the complete constructor parameter values. DI extension gets this constructor parameter values array (_parameterValues) within two stages.See :

enter image description here

The stage2 works in a way like below:

b'= sp.GetService(B); 
if b' == null :
    b' = default value of B

As you can see above, the ActivatorUtilities.CreateInstance(sp,mw,args) API deals with the order and missing arguments automatically.


As a side note, the by-convention middlewares are activated at startup-time and will always be a singleton. If you want to use scoped service, see this thread

like image 144
itminus Avatar answered Sep 29 '22 22:09

itminus