在 ASP.NET 核心中间件构造函数中混合依赖注入和手动传递的参数

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

我正在为 ASP.NET Core 2.2 编写自定义中间件。根据 Microsoft Docs 关于编写自定义中间件的说法:

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

这看起来一切都很好,但它并没有说明当我混合使用这两种方式时会发生什么,例如使用 DI 并在 UseMiddleware<T> 中传递参数。例如,我有以下中间件:

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

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

其中 logger 由 DI 提供,options 提供如下:

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

我自己对 2.2 的测试似乎表明这工作正常,构造函数中参数的顺序无关紧要(我可以将 DI 参数放在手动传递的参数之前或之后,甚至在两个参数之间手动传递的参数)。但我正在寻找一些保证,以确保我所做的一切正常。如果有人能指出一些支持这种用法的文档或源代码,那就太好了。谢谢!

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

好吧,这是基于意见的。我相信,尽管您的情况一切正常,但没关系。但我更喜欢使用 ASP.NET Core 中介绍的 options pattern

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;
    }
    //...
}

为此,您需要在 Startup

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

在这种情况下你应该添加不带参数的中间件

app.UseMiddleware<CustomMiddleware>();

备注

也值得一看RequestLocalizationMiddleware

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

以及框架如何让您使用 ApplicationBuilderExtensions

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

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

正如您所看到的 ASP.NET Core 开发人员也更喜欢在中间件构造函数中坚持使用 IOptions 并且在手动指定附加参数时他们只是将它们包装到 Options

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

是的。看完源码我觉得还行

工作原理

您的 CustomMiddleware 是一个约定俗成的中间件(不同于基于工厂的中间件),它由 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);

此处 args(给定参数)是您传递给 UseMiddleware<CustomMiddleware>(args)(没有 next)的参数数组。

准备构造函数参数时有两个阶段:

  1. 将给定的 args 与构造参数类型相匹配。并在类型匹配时设置值。参见 source code Here
  2. 使用 ServiceProvider.GetRequiredService<SomeService>() 填充 null 元素。参见 source code here。如果服务实例仍然是 null,则使用 default 值。

例如,假设:

  1. 您有一个符合惯例的中间件,其构造函数具有以下签名:
    public CustomMiddleware(RequestDelegate next, A a, B b, C c, D d, E e){ ... }
    
  2. 然后我们在注册中间件的时候传入两个参数:

    app.UseMiddleware(c, a) 
    

    其中 cC 类型的实例,aA 类型的实例。所以 givenParameters 数组是 [next,c, a]

要创建 CustomMiddleware 的实例,编译器需要知道完整的构造函数参数值。 DI 扩展在两个 stages.See :

内获取此构造函数参数值数组 (_parameterValues)

stage2 的工作方式如下:

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

正如您在上面看到的,ActivatorUtilities.CreateInstance(sp,mw,args) API 会自动处理顺序和缺少的参数。


附带说明一下,按照惯例,中间件在启动时被激活,并且始终是单例。如果您想使用范围服务,请参阅