简体   繁体   中英

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

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, eg 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!

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 :

在此处输入图片说明

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

But I'm looking for some assurances that what I'm doing is OK.

Well, this is opinion-based. While everything is working in your case it is okay, I believe. But I would prefere using options pattern introduced in ASP.NET Core .

public class CustomMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<CustomMiddleware> _logger;
    private readonly CustomMiddlewareOptions _options;

    public CustomMiddleware(RequestDelegate next, ILogger<CustomMiddleware> logger, IOptions<CustomMiddlewareOptions> options)
    {
        _next = next;
        _logger = logger;
        _options = options.Value;
    }
    //...
}

For this one you will need to configure CustomMiddlewareOptions in Startup

services.Configure<CustomMiddlewareOptions>(options =>
{
    options.Id = 1;
    options.Name = "options";
    //set other properties
});

In this case you should add middleware without parameters

app.UseMiddleware<CustomMiddleware>();

Note

It's also worth to take a look at RequestLocalizationMiddleware

public RequestLocalizationMiddleware(RequestDelegate next, IOptions<RequestLocalizationOptions> options)

and how framework offers you to use the middleware in ApplicationBuilderExtensions

public static IApplicationBuilder UseRequestLocalization(this IApplicationBuilder app)
{
    //omitted
    return app.UseMiddleware<RequestLocalizationMiddleware>();
}

or

public static IApplicationBuilder UseRequestLocalization(this IApplicationBuilder app, RequestLocalizationOptions options)
{
    //omitted
    return app.UseMiddleware<RequestLocalizationMiddleware>(Options.Create(options));
}

As you can see ASP.NET Core developers also prefer to stick with IOptions in middleware constructor and while manually specifying additional parameter they just wrap them into Options

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM