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



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

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

请参阅此 SO 问题的已接受答案:ASP.NET 核心模型绑定错误消息本地化

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

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

每个服务提供商都有自己的缓存。因此,构建多个服务提供商实例可能会导致称为"撕裂的生活方式"的问题:

当具有相同生活方式的多个[注册]映射到同一组件时,该组件被称为具有撕裂的生活方式。该组件被视为已撕裂,因为每个 [注册] 都有自己的给定组件缓存,这可能会导致单个范围内的组件有多个实例。当注册被撕裂时,应用程序可能会接线不正确,这可能会导致意外行为。

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

但是还有其他类似的微妙错误可能会出现。例如,在解析包含作用域内依赖项的对象图时。为创建存储在下一个容器中的对象图构建单独的临时服务提供程序可能会导致这些作用域依赖项在应用程序期间保持活动状态。此问题通常称为捕获依赖项。

对于像Autofac或DryIoc这样的容器,这没什么大不了的,因为您可以在一行上注册服务,而在下一行上,您可以立即解决它。

此语句意味着,在注册阶段仍在进行时尝试解析容器中的实例没有问题。但是,这是不正确的 — 在已解析实例后通过向容器添加新注册来更改容器是一种危险的做法 — 它可能会导致各种难以跟踪的错误,与使用的 DI 容器无关。

特别是由于那些难以跟踪的错误,DI容器,如Autofac,Simple Injector和Microsoft.Extensions.DependencyInjection(MS.DI) 首先阻止您这样做。Autofac和MS。DI通过在"容器生成器"(AutoFac的ContainerBuilder和MS中进行注册来做到这一点。DI的ServiceCollection)。另一方面,简单注射器不会进行这种拆分。相反,它会在解析第一个实例后锁定容器,使其不进行任何修改。然而,效果是相似的;它会阻止您在解析后添加注册。

简单注入器文档实际上包含了一些体面的解释,说明为什么这种寄存器-解析-寄存器模式有问题:

想象一下,您希望将某些FileLogger组件替换为具有相同ILogger接口的不同实现。如果有一个组件直接或间接依赖于ILogger,替换ILogger实现可能无法按预期工作。例如,如果使用组件注册为单一实例,则容器应保证仅创建此组件的一个实例。当允许您在单一实例已包含对"旧"注册实现的引用后更改ILogger的实现时,容器有两个选择,这两个选项都不正确:

  • 返回使用组件的缓存实例,该实例引用了"错误"ILogger实现。
  • 创建并缓存该组件的新实例,这样做会破坏将类型注册为单一实例的承诺,以及容器始终返回相同实例的保证。

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

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

因此,一般的解决方案是将解析服务(如您的IStringLocalizerFactory)推迟到"使用"阶段,并随之推迟依赖于服务解析的事物的最终配置。

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

  • 配置ModelBindingMessageProvider需要使用MvcOptions类。
  • MvcOptions类仅在"添加"(ConfigureServices)阶段可用。
  • 在"添加"阶段,无法访问IStringLocalizerFactory,也无法访问容器或服务提供商,并且不能通过使用Lazy<IStringLocalizerFactory>创建此类值来推迟解析它。
  • 在"使用"阶段,IStringLocalizerFactory可用,但此时,没有可用于配置ModelBindingMessageProviderMvcOptions

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

public void ConfigureServices(IServiceCollection services) { 服务业。添加本地化(); 服务业。AddMvc(options => { 选项。ModelBindingMessageProvider.SetValueIsInvalidAccessor( _ => this.localizer["值'{0}'无效。 }); } 私有定位器本地化器; public void Configure(IApplicationBuilder app, IHostingEnvironment env) { this.localizer = app.应用服务 .GetRequiredService() .Create("ModelBindingMessages", "AspNetCoreLocalizationSample"); }

此解决方案的缺点是这会导致时间耦合,这是它自己的代码气味。

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

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

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

相关内容

最新更新