每个服务提供商都有自己的缓存。因此,构建多个服务提供者实例可能会导致一个称为撕裂的生活方式:
当具有相同生活方式的多个[注册]映射到同一组件时,该组件被称为具有撕裂的生活方式。该组件被认为是撕裂的,因为每个[注册]都会有自己的给定组件的缓存,这可能会导致单个范围内出现该组件的多个实例。当注册被破坏时,应用程序可能会错误连接,这可能会导致意外行为。
这意味着每个服务提供商将拥有自己的单例实例缓存。从同一源(即从同一服务集合)构建多个服务提供者将导致单例实例被多次创建 - 这打破了给定单例注册最多有一个实例的保证。
但还可能出现其他同样微妙的错误。例如,在解析包含范围依赖项的对象图时。构建一个单独的临时服务提供程序来创建存储在下一个容器中的对象图可能会导致这些范围依赖项在应用程序的持续时间内保持活动状态。这个问题通常被称为强制依赖项.
对于像 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
method.
因此,一般的解决方案是推迟解析服务(例如您的IStringLocalizerFactory
)直到“使用”阶段,并随之推迟依赖于服务解析的事物的最终配置。
不幸的是,这似乎导致了先有鸡还是先有蛋配置时的因果困境ModelBindingMessageProvider
因为:
- 配置
ModelBindingMessageProvider
需要使用MvcOptions
class.
- The
MvcOptions
课程仅在“添加”期间可用(ConfigureServices
) phase.
- 在“添加”阶段,无法访问
IStringLocalizerFactory
并且无法访问容器或服务提供商,并且无法通过使用创建此类值来推迟解决该问题Lazy<IStringLocalizerFactory>
.
- 在“使用”阶段,
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");
}
该解决方案的缺点是这会导致时间耦合,这是它自己的代码味道。
当然,你可以认为这是一个丑陋的解决方法,解决了在处理问题时甚至可能不存在的问题。IStringLocalizerFactory
;在这种特殊情况下,创建一个临时服务提供商来解决本地化工厂可能会很好地工作。然而,事实是,实际上很难分析你是否会遇到麻烦。例如:
- 虽然
ResourceManagerStringLocalizerFactory
,这是默认的本地化器工厂,不包含任何状态,它确实依赖于其他服务,即IOptions<LocalizationOptions>
and ILoggerFactory
。两者都配置为单例。
- 默认
ILoggerFactory
实施(即LoggerFactory
),由服务提供商创建,并且ILoggerProvider
之后可以将实例添加到该工厂。如果你的第二个ResourceManagerStringLocalizerFactory
取决于它自己ILoggerFactory
执行?这样做会正确吗?
- 同样适用于
IOptions<T>
——实施者OptionsManager<T>
。它是一个单例,但是OptionsManager<T>
本身取决于IOptionsFactory<T>
并包含自己的私有缓存。如果有第二次会发生什么OptionsManager<T>
对于一个特定的T
?未来这种情况会改变吗?
- What if
ResourceManagerStringLocalizerFactory
被替换为不同的实现?这种情况并非不可能发生。依赖图会是什么样子?如果生活方式被破坏,这会带来麻烦吗?
- 一般来说,即使您现在可以得出结论,效果很好,您确定这在 ASP.NET Core 的任何未来版本中都适用吗?不难想象,对 ASP.NET Core 未来版本的更新将以极其微妙和奇怪的方式破坏您的应用程序,因为您隐式依赖于这种特定行为。这些错误将很难追踪。
不幸的是,当涉及到配置时ModelBindingMessageProvider
,似乎没有简单的出路。在我看来,这是 ASP.NET Core MVC 中的一个设计缺陷。希望微软能够在未来的版本中解决这个问题。