在ConfigureServices()中调用BuildServiceProvider()的成本和可能产生的副作用

时间:2019-05-08 14:24:15

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

有时,在服务注册期间,我需要从DI容器解析其他(已注册)服务。借助Autofac或DryIoc这样的容器,这没什么大不了的,因为您可以在一行上注册该服务,而在下一行上可以立即解决该问题。

但是使用Microsoft的DI容器,您需要注册服务,然后构建服务提供者,然后才可以从该IServiceProvider实例解析服务。

请参见已接受的回答这样的问题:ASP.NET Core Model Binding Error Messages Localization

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(options => { options.ResourcesPath = "Resources"; });
    services.AddMvc(options =>
    {
        var F = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
        var L = F.Create("ModelBindingMessages", "AspNetCoreLocalizationSample");
        options.ModelBindingMessageProvider.ValueIsInvalidAccessor =
            (x) => L["The value '{0}' is invalid."];

        // omitted the rest of the snippet
    })
}

要能够定位ModelBindingMessageProvider.ValueIsInvalidAccessor消息,答案建议通过基于当前服务集合构建的服务提供商来解析IStringLocalizerFactory

那时“构建”服务提供者的成本是多少,这样做会有任何副作用,因为将至少再次构建一次服务提供者(在添加所有服务之后)?

1 个答案:

答案 0 :(得分:5)

每个服务提供商都有自己的缓存。因此,构建多个服务提供者实例可能会导致称为Torn Lifestyles的问题:

  

当具有相同生活方式的多个[注册]映射到同一组件时,该组件被称为生活方式破损。该组件被认为是已损坏的,因为每个[注册]将拥有其自己的给定组件缓存,这可能会导致在单个作用域内出现该组件的多个实例。当注册被撕毁时,应用程序可能无法正确连接,这可能导致意外行为。

这意味着每个服务提供商都将拥有自己的单例实例缓存。从同一来源(即从同一服务集合)构建多个服务提供者将导致多次创建一个单例实例,这打破了对于给定单例注册最多存在一个实例的保证。

但是还有其他一些细微的错误可能会出现。例如,当解析包含范围相关性的对象图时。为创建存储在下一个容器中的对象图而创建单独的临时服务提供程序,可能会导致这些范围内的依赖项在应用程序期间保持有效。此问题通常称为Captive Dependencies

  

对于诸如Autofac或DryIoc之类的容器,这没什么大不了的,因为您可以在一行上注册该服务,而在下一行上可以立即解决该问题。

此语句表示在注册阶段仍在进行时,尝试从容器解析实例没有问题。但是,这是不正确的-在解决实例之后通过在容器中添加新的注册来更改容器是一种危险的做法-可能导致各种难以跟踪的错误。

尤其是由于那些难以跟踪的错误,DI容器(例如Autofac,Simple Injector和Microsoft.Extensions.DependencyInjection(MS.DI))一开始就阻止您执行此操作。 Autofac和MS.DI通过在“容器构建器”(AutoFac的ContainerBuilder和MS.DI的ServiceCollection)中进行注册来做到这一点。另一方面,简单注入器不会进行此拆分。相反,它会在解析第一个实例后锁定容器,使其无法进行任何修改。但是,效果是相似的。解决后,它会阻止您添加注册。

Simple Injector文档中实际上包含一些decent explanation,说明为什么此Register-Resolve-Register模式存在问题:

  

想象一下您想用相同的FileLogger接口将某些ILogger组件替换为其他实现的情况。如果存在直接或间接依赖于ILogger的组件,则替换ILogger的实现可能无法如您所愿。例如,如果使用方组件注册为单例,则容器应保证将仅创建此组件的一个实例。在单例实例已经拥有对“旧”注册实现的引用之后,允许您更改ILogger的实现时,容器有两种选择-都不正确:

     
      
  • 返回引用了“错误” ILogger实现的消费组件的缓存实例。
  •   
  • 创建并缓存该组件的新实例,这样做会违反将类型注册为单例的承诺,并保证容器将始终返回同一实例。 < / li>   

出于同样的原因,您会看到ASP.NET Core Startup类定义了两个单独的阶段:

  • “添加”阶段(ConfigureServices方法),将注册添加到“容器构建器”(也称为IServiceCollection
  • “使用”阶段(Configure方法),在该阶段您通过设置路由来声明要使用MVC。在此阶段,IServiceCollection已变成IServiceProvider,甚至可以将这些服务注入到Configure方法中。

因此,一般的解决方案是将解析服务(如您的IStringLocalizerFactory)推迟到“使用”阶段,然后将取决于服务解析的事物的最终配置推迟。

不幸的是,在配置ModelBindingMessageProvider时,这似乎导致了鸡肉或鸡蛋因果关系的困境,因为:

  • 配置ModelBindingMessageProvider需要使用MvcOptions类。
  • MvcOptions类仅在“添加”(Configure)阶段可用。
  • 在“添加”阶段,不能访问IStringLocalizerFactory,也不能访问容器或服务提供商,并且不能通过使用Lazy<IStringLocalizerFactory>创建这样的值来推迟解析它。 / li>
  • 在“使用”阶段,IStringLocalizerFactory可用,但是到那时,不再可以使用MvcOptions来配置ModelBindingMessageProvider

解决这种僵局的唯一方法是使用Startup类内部的私有字段,并在AddOptions的闭包中使用它们。例如:

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization();
    services.AddMvc(options =>
    {
        options.ModelBindingMessageProvider.SetValueIsInvalidAccessor(
            _ => this.localizer["The value '{0}' is invalid."]);
    });
}

private IStringLocalizer localizer;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    this.localizer = app.ApplicationServices
        .GetRequiredService<IStringLocalizerFactory>()
        .Create("ModelBindingMessages", "AspNetCoreLocalizationSample");
}

该解决方案的缺点在于,这会导致Temporal Coupling,这是它自己的代码味道。

您当然可以争辩说,这是解决在处理IStringLocalizerFactory时可能根本不存在的问题的丑陋解决方法;在这种情况下,创建一个临时服务提供商来解决本地化工厂可能就可以了。然而,实际上很难分析您是否会遇到麻烦。例如:

  • 即使默认本地化程序工厂ResourceManagerStringLocalizerFactory不包含任何状态,它也确实依赖于其他服务,即IOptions<LocalizationOptions>ILoggerFactory。两者都配置为单例。
  • 默认的ILoggerFactory实现(即LoggerFactory)是由服务提供商创建的,之后可以将ILoggerProvider实例添加到该工厂。如果您的第二个ResourceManagerStringLocalizerFactory依赖于自己的ILoggerFactory实现,将会发生什么?可以正常工作吗?
  • IOptions<T>成立,由OptionsManager<T>实现。它是一个单例,但是OptionsManager<T>本身取决于IOptionsFactory<T>并包含其自己的专用缓存。如果某个特定的OptionsManager<T>还有第二个T会发生什么?将来会改变吗?
  • 如果将ResourceManagerStringLocalizerFactory替换为其他实现怎么办?这是一个不太可能的情况。依赖图看起来会是什么样子,如果生活方式被破坏会引起麻烦?
  • 通常,即使您可以得出结论说效果很好,您是否确定在将来的任何ASP.NET Core版本中都可以使用?不难想象,对ASP.NET Core未来版本的更新将以完全微妙和怪异的方式破坏您的应用程序,因为您隐式地依赖于此特定行为。这些错误很难找到。

不幸的是,在配置ModelBindingMessageProvider时,似乎没有简单的出路。这是IMO在ASP.NET Core MVC中的一个设计缺陷。希望Microsoft在将来的版本中可以解决此问题。