为什么依赖项解析在服务本身之后解析我的服务选项?

Why is dependency resolution resolving my service's options AFTER the service itself?

我有一个 .NET Core 2.2 WebAPI 项目,我在其中注册了三个服务(我们称它们为 MailerServiceTicketServiceAuditServce),以及一个依赖其中一项服务 (MailerService) 的中间件 (ExceptionMiddleware)。 MailerServiceTicketService 都依赖于我在 service.Configure<TOption>() 中注册的强类型选项对象。我已经确保选项对象在服务之前注册,并且选项依赖项本身连接到服务的构造函数中。

问题是 TicketService 从 DI 中很好地解析了它的选项对象,但由于某种原因 MailerService 的配置在服务本身之后解析。下面相关代码的粗略草图。

我设置了断点以观察解析顺序,并且用于设置 MailerConfig 的委托始终在 MailerService 构造函数之后触发。所以每次我得到一个MailerSerivce的实例,它的options参数都是NULL。然而,观察 TicketService 的相同解析,TicketConfig 在 TicketService 构造函数触发之前解析,并且 TicketService 获得正确配置的选项对象。除了 MailerService 是中间件的依赖项之外,我不知道它们之间可能有什么不同。

我已经为此苦思了几个小时,但找不到任何像样的文档来解释为什么 DI 解析顺序可能会出错,或者我在这里可能做错了什么。有没有人猜到我可能做错了什么?异常中间件是否也需要注册为服务?

启动

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvcCore()
      .AddAuthorization()
      .AddJsonFormatters()
      .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());

    services.Configure<MailerConfig>(myOpts =>
    {
      // this always resolves AFTER MailerService's constructor
      myOpts = Configuration.GetSection("MailerSettings").Get<MailerConfig>();
      myOpts.SecretKey = _GetApiKey(Configuration.GetValue<string>("MailerApiKeyFile"));
    });

    services.Configure<ExceptionMiddlewareConfig>(myOpts =>
    {
      myOpts.AnonymousUserName = Configuration.GetValue<string>("AnonymousUserName");
      myOpts.SendToEmailAddress = Configuration.GetValue<string>("ErrorEmailAddress");
    });

    services.Configure<TicketConfig>(myOpts =>
    {
      // this always resovles BEFORE TicketService's constructor
      myOpts.ApiRoot = Configuration.GetValue<string>("TicketApiRoot");
      myOpts.SecretKey = _GetApiKey(Configuration.GetValue<string>("TicketApiKeyFile"));
    });

    services.AddTransient(provider =>
    {
      return new AuditService
      {
        ConnectionString = Configuration.GetValue<string>("Auditing:ConnectionString")
      };
    });

    services.AddTransient<ITicketService, TicketService>();
    services.AddTransient<IMailerService, AuditedMailerService>();
  }

  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  {
    app.UseMiddleware<ExceptionMiddleware>();

    //app.UseHttpsRedirection();
    app.UseAuthentication();
    app.UseMvc();
  }
}

MailerService 构造函数

public AuditedMailerService(AuditService auditRepo, IOptions<MailerConfig> opts)
{
  // always gets a NULL opts object??????
  _secretKey = opts.Value.SecretKey;
  _defaultFromAddr = opts.Value.DefaultFromAddress;
  _defaultFromName = opts.Value.DefaultFromName;
  _repo = auditRepo;
}

TicketService 构造函数

public TicketService(IOptions<TicketConfig> opts)
{
  // always gets an initialized opts object with proper values assigned
  ApiRoot = opts.Value.ApiRoot;
  SecretKey = opts.Value.SecretKey;
}

中间件构造函数

public ExceptionMiddleware(RequestDelegate next, IMailerService mailer, IOptions<ExceptionMiddlewareConfig> config)
{
  _mailer = mailer;
  _next = next;
  _anonymousUserName = config.Value.AnonymousUserName;
  _sendToEmailAddress = config.Value.SendToEmailAddress;
}

因为你所做的有点没有意义。

您注册的中间件依赖于您标记为瞬态的服务,即按需创建。

但是middleware is always instantiated on app startup (singleton)。因此,任何依赖项也会在应用程序启动时实例化。因此,由您的中间件创建的 "transient" 服务实例也是单例!

此外,如果您的中间件是唯一依赖于该瞬态服务的东西,那么将该服务注册为除单例之外的任何东西都是没有意义的!

你所拥有的是依赖生活方式的不匹配,which is generally a bad idea for numerous reasons。如上所述,避免这种情况的方法是确保您的依赖链中的所有服务都在相同的范围内注册 - 即,您的 ExceptionMiddleware 所依赖的任何东西 - 在这种情况下,AuditedMailerService -应该是单例。

If - if - 你隐含地打算或需要让 AuditedMailerService 是瞬态的,然后而不是将它注入你的中间件的构造函数,inject it via the Invoke method

public ExceptionMiddleware(RequestDelegate next, IOptions<ExceptionMiddlewareConfig> config)
{
  _mailer = mailer;
  _anonymousUserName = config.Value.AnonymousUserName;
  _sendToEmailAddress = config.Value.SendToEmailAddress;
}

public async Task Invoke(HttpContext httpContext, IMailerService mailer)
{
  ...
}

但是,从这种生活方式不匹配的症状中可以得出一个更有趣的问题:为什么 IOptions<MailerConfig> 实例最终会变成 null

我的猜测 - 这只是一个猜测 - 你正在与 ASP.NET Core 2.x 的 WebHost(运行你的网络的组件)的事实发生冲突app) actually creates two IServiceProvider instances. There is an initial, "dummy" one that is created to inject services during the earliest stages of app startup, and then the "real" one that is used for the rest of the app's lifetime. The linked issue discusses why this is problematic: in short, it was possible to get an instances of a service registered by the dummy container, then a second instance of the same service would be created by the real container, causing issues. I believe that because middleware runs so early in the pipeline, the IoC container it uses is the dummy one with no knowledge of IOptions<MailerConfig>, and since the default service location in ASP.NET Core returns null when a requested service isn't found instead of throwing an exception,你得到 null 返回。

虽然这不是一个很好的答案(我仍然不知道为什么 DI 只在 服务之后解析选项),我找到了解决问题的方法。我只是在 Options Pattern 周围做一个 end-运行 并在我注册邮件程序服务的委托中明确解决所有依赖关系。我还调整了 ExceptionMiddleware 以将邮件程序服务作为 InvokeAsync 中的方法参数,而不是构造函数参数。服务是瞬态的还是单例的并不是特别重要,但目前我更喜欢瞬态。

这种方法的显着缺点是我无法再使用内置于选项系统中的实时更新机制 - 如果我即时更改我的应用程序设置中的值,则需要回收该应用程序以进行选择它了。这不是我的应用程序的实际需要,所以我可以接受它,但其他人在遵循我的方法之前应该注意。

新的 MailerService 注册委托:

  services.AddTransient<IMailerService>(provider =>
  {
    var cfg = Configuration.GetSection("MailerSettings").Get<MailerConfig>();
    cfg.SecretKey = _GetApiKey(Configuration.GetValue<string>("MailerApiKeyFile"));

    var auditor = provider.GetService<AuditService>();

    return new AuditedMailerService(auditor, Options.Create(cfg));
  });