首页 > 解决方案 > 在 ConfigureServices() 中调用 BuildServiceProvider() 的成本和可能的副作用是什么

问题描述

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

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

请参阅这个 SO 问题的公认答案: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通过基于当前服务集合构建的服务提供者来解决。

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

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

解决方案


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

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

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

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

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

此声明意味着在注册阶段仍在进行时尝试从容器解析实例没有问题。然而,这是不正确的——在你已经解决实例之后通过添加新注册来改变容器是一种危险的做法——它可能导致各种难以跟踪的错误,与使用的 DI 容器无关。

尤其是因为那些难以跟踪的错误,DI 容器,例如 Autofac、Simple Injector 和 Microsoft.Extensions.DependencyInjection (MS.DI) 会阻止您首先执行此操作。Autofac 和 MS.DI 通过在“容器构建器”(AutoFac'sContainerBuilder和 MS.DI's ServiceCollection)中进行注册来做到这一点。另一方面,Simple Injector 不会进行这种拆分。相反,它会在解决第一个实例后锁定容器以防止任何修改。然而,效果是相似的。它会阻止您在解决后添加注册。

Simple Injector 文档实际上包含了一些关于为什么这个 Register-Resolve-Register 模式有问题的解释:

想象一下您想用相同的接口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它是可用的,但此时,MvcOptions您不再可以使用它来配置ModelBindingMessageProvider.

解决这个僵局的唯一方法是在Startup类中使用私有字段并在AddOptions. 例如:

公共无效配置服务(IServiceCollection 服务)
{
    services.AddLocalization();
    services.AddMvc(选项 =>
    {
        options.ModelBindingMessageProvider.SetValueIsInvalidAccessor(
            _ => this.localizer["值 '{0}' 无效。"]);
    });
}

私有 IStringLocalizer 本地化器;

公共无效配置(IApplicationBuilder 应用程序,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 中的一个设计缺陷。希望微软会在未来的版本中解决这个问题。


推荐阅读