【问题标题】:Why is dependency resolution resolving my service's options AFTER the service itself?为什么依赖解析会在服务本身之后解决我的服务选项?
【发布时间】:2020-07-05 07:35:31
【问题描述】:

我有一个 .NET Core 2.2 WebAPI 项目,我在其中注册了三个服务(我们将它们称为 MailerServiceTicketServiceAuditServce),以及一个依赖于的中间件 (ExceptionMiddleware)在其中一项服务上 (MailerService)。 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;
}

【问题讨论】:

  • 不确定,但如果您想“共享”相同的选项,为什么不将它们注册为单例...。这样可以省去很多麻烦
  • 确认Configuration.GetSection("MailerSettings").Get&lt;MailerConfig&gt;(); 返回一个实际值。您似乎覆盖了提供给代表的选项。默认情况下,它会消耗任何错误,因此不会抛出异常。
  • @NKosi:GetSection() 调用确实有效。获取 api 密钥的下一行也可以正常工作。委托在触发时会设置一个适当的选项对象。令人困惑的是,委托仅在 AuditedMailerService 的构造函数之后才被触发 - 因此服务总是获得一个空选项对象。我很困惑。
  • @NateKennedy 检查我的provided answer

标签: c# .net-core dependency-injection asp.net-core-webapi .net-core-2.2


【解决方案1】:

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

这种方法的显着缺点是我不能再使用选项系统中内置的实时更新机制 - 如果我动态更改我的 appsettings 中的值,则需要回收该应用程序以获取它.这不是我的应用程序的实际需求,因此我可以接受它,但其他人在遵循我的方法之前应该注意。

新的 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));
  });

【讨论】:

    【解决方案2】:

    因为你在做的事情有点没有意义。

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

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

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

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

    如果 - 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&lt;MailerConfig&gt; 实例最终会变成null

    我的猜测 - 这只是一个猜测 - 是您与 ASP.NET Core 2.x 的 WebHost(运行您的 Web 应用程序的组件)actually creates two IServiceProvider instances 相冲突。在应用程序启动的最初阶段创建了一个初始的“虚拟”服务以注入服务,然后在应用程序的剩余生命周期中使用“真实”服务。链接的问题讨论了为什么会出现问题:简而言之,可以获取由虚拟容器注册的服务实例,然后由真实容器创建相同服务的第二个实例,从而导致问题。我相信由于中间件在管道中运行得这么早,它使用的 IoC 容器是不知道 IOptions&lt;MailerConfig&gt;since the default service location in ASP.NET Core returns null when a requested service isn't found instead of throwing an exception 的虚拟容器,你会得到 null 返回。

    【讨论】:

    • 两种建议的方法都没有解决问题。我尝试将 AuditService 和 AuditedMailerService 都注册为单例 - 仍然得到一个空选项对象。尝试将每个服务恢复为瞬态并将 AuditMailerService 注入 InvokeAsync - 仍然获得空选项对象。无论发生什么,它都会导致选项在 AuditedMailerService 之后始终解决。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多