Simple Injector,服务依赖注入和多实例

Simple Injector, dependency injection of services and multiple instances

我正在为我的 DI 库使用 Simple Injector。我的 asp.net MVC 站点中有控制器,它们通过此库在其构造函数中获取服务。当我查看 Visual Studio 中的诊断工具并查看我的托管内存时,我看到同一服务的多个实例。

var container = new Container();
container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();
    
container.RegisterWebApiControllers(GlobalConfiguration.Configuration);
container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
container.RegisterMvcIntegratedFilterProvider();

RegisterComponents(container);
    
container.Verify();
DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));

我的问题是,这是设计使然。我认为一个 IPaymentsService 将用于所有控制器,但我有 187 个?我认为应该是 1.

我正在考虑添加以下行。似乎工作正常,现在我看到使用的内存减少了 700,000 KB,站点加载时间加快了 10 秒以上。这有什么缺点吗?

container.Options.DefaultLifestyle = Lifestyle.Scoped;

让我从基础开始,您可能会熟悉其中的大部分内容,但为了完整性和正确性,我们还是要这样做。

  • 对于 Simple Injector,Transient 意味着 短暂存在 并且不被缓存。如果服务 X 注册为 Transient,并且多个组件依赖于 X,每个组件都有自己的新实例。即使 X 实现了 IDisposable,它也不会被 跟踪或处理。创建后,Simple Injector 会立即忘记 Transients。
  • 在特定定义的范围内(例如网络请求),Simple Injector 只会创建注册为 Scoped 的服务的单个实例。如果服务 XScoped,并且多个组件依赖于 X,则在同一范围内创建的所有组件都会获得 相同的 实例 XScoped 个实例被跟踪,如果它们实现了 IDisposable(或 IAsyncDisposable),Simple Injector 在处理范围时调用它们的 Dispose 方法。在 Web 请求处理范围(因此 Scoped 组件)的上下文中,这由 Simple Injector 集成包为您管理。
  • 使用 Singleton,简单注入器将确保在单个 Container 个实例中最多有一个该服务的实例。如果您有多个 Container 实例(您通常不会在生产中使用,但在测试期间更有可能)每个 Container 实例都会使用 Singletons 获得自己的缓存。 所描述的行为特定于 Simple Injector。当涉及到这些生活方式时,其他 DI 容器可能有不同的行为和定义:
  • Simple Injector 认为 Transient 组件 存在时间短,可能包括状态 ,而 ASP.NET Core DI (MS.DI) 认为 Transient组件是*无状态的,存在任何可能的持续时间。 * 由于这种不同的观点,使用 MS.DI,Transients 可以注入到 Singletons 中,而使用 Simple Injector 则不能。当您调用 Verify().
  • 时,Simple Injector 将给出诊断错误
  • Transients 而不是 由 Simple Injector 处理的。这就是为什么在注册实现 IDisposableTransient 时会出现诊断错误的原因。同样,这与其他一些 DI 容器非常不同。使用 MS.DI,瞬态 跟踪并在处理它们的作用域时被处理掉。这有利也有弊。重要的缺点是,当从根容器解析一次性瞬态时,这将导致意外内存泄漏,因为 MS.DI 将永远存储这些瞬态。

关于为组件选择正确的生活方式:

  • 为组件选择正确的生活方式 (Transient/Scoped/Singleton) 是委托事项。
  • 如果组件包含应在整个应用程序中重复使用的状态,您可能希望将该组件注册为 Singleton - 或者 - 将状态移出组件并将其隐藏在服务后面可以注册为Singleton.
  • 组件的预期寿命不应超过其使用者的寿命。 Transient 是最短的生活方式,而 Singleton 是最长的。这意味着 Singleton 组件应该只依赖于其他 Singletons,而 Transient 组件可以同时依赖 TransientScopedSingleton组件。 Scoped 组件通常应该只有 ScopedSingleton 依赖项。
  • 然而,之前的规则非常严格,这就是为什么我们决定使用 Simple Injector v5 允许 Scoped 组件也依赖于 Transient 组件。如果作用域只存在很短的时间(就像 Web 应用程序中的 Web 请求一样),这通常会很好。但是,如果您有在单个范围内运行的长 运行 操作,但会定期回调到容器中以解析新实例,使 Scoped 个实例依赖于(有状态的)Transients 肯定会惹麻烦;然而,这不是一个非常常见的用例。
  • 未能遵守“组件应仅依赖于同等或寿命更长的组件”的规则会导致 Captive Dependencies。 Simple Injector 称它们为“生活方式不匹配”​​,但其实是一回事。
  • 无依赖的无状态组件可以注册为Singleton.
  • 具有依赖项的无状态组件的生活方式取决于其依赖项的生活方式。如果它的组件是 ScopedTransient,它本身应该是 ScopedTransient。如果它被注册为 Singleton,这将导致它的依赖项成为俘虏依赖项。
  • 如果无状态组件只有 Singleton 个依赖项,它也可以注册为 Singleton

在为应用程序中的组件选择正确的生活方式时,有两种基本的组合模型可供选择,即环境组合模型和闭包组合模型。我写了关于这个开头的一系列五篇博文 here。使用环境组合模型,您可以使应用程序中的所有组件 无状态 并将状态存储在对象图之外。这允许您将几乎所有组件注册为 Singleton,但它确实会导致复杂化,并且可能会导致您的应用程序设计有所不同。

因此,您更有可能应用第二个组合模型:闭包组合模型。这是最常见的模型,被大多数开发人员使用并被大多数应用程序框架推动(例如 ASP.NET Core DI)。使用 Closure Composition Model,您通常会将大部分组件注册为 Transient。只有少数包含状态的应用程序组件会被注册为 ScopedSingleton。尽管您当然可以通过查看组件的消费者和依赖关系来“调整”组合,并决定增加生活方式(到 Scoped 甚至 Singleton)以防止创建不必要的实例,但这样做的缺点是比较脆弱。

例如,如果您有一个依赖于 Singleton 组件 Y 的无状态组件 X,您可以将组件 X 设为 Singleton以及。但是一旦 Y 需要自己的 ScopedTransient 依赖项,您不仅需要调整 Y 的生活方式,还需要调整 X 的生活方式.这可以级联依赖链。因此,相反,使用 Closure Composition Model 时,通常将事物保持为“瞬态,除非”。

关于性能:

Simple Injector 是高度优化的,如果创建一些额外的组件通常不会有太大区别。特别是如果他们是无国籍的。当 运行 一个 32 位进程时,这样的 class 会消耗“8 + (4 * number-of-dependencies)” 字节的内存。换句话说,具有 1 个依赖项的无状态组件消耗 12 个字节的内存,而具有 5 个依赖项的组件消耗 28 个字节(假设一个 32 位进程;在 64 位下将其乘以 2)。

另一方面,管理和组合 Scoped 个实例有其自身的开销。尽管 Simple Injector 在这方面进行了高度调整,但 Scoped 个实例需要缓存并从范围的内部字典中解析。这是有代价的。这意味着在图中创建一个没有依赖关系的组件几次 Transient 可能比将其解析为 Scoped.

更快

在正常情况下,您不必担心产生这些额外的 Transient 实例所需的额外内存量和额外 CPU 量。但也许你在正常情况下不是。以下异常情况可能会引起麻烦:

  • 如果您违反了 simple-injection-constructors rule: 当一个组件的构造函数不仅仅是简单地存储其提供的依赖项时(例如调用它们,执行 I/O 或CPU 密集型或内存密集型的东西)创建额外的瞬态实例会造成很大伤害。你绝对应该尽可能远离这种情况。
  • **您的应用程序创建了大量的对象图:**如果对象图真的很大,您可能会看到某些组件在图中被多次(甚至多次)依赖。如果图很大(数千个单独的实例),这可能会导致创建数百甚至数千个额外的对象,尤其是当这些组件具有自己的 Transient 依赖项时。当组件有很多依赖项时,经常会发生这种情况。例如,如果您的应用程序的组件经常有超过 5 个依赖项,您很快就会看到对象图的大小爆炸。这里需要注意的重要一点是,这通常是由于违反单一职责原则造成的。当组件做太多事情、承担太多责任时,它们会产生很多依赖关系。这很容易导致它们有很多依赖关系,当它们的依赖关系有很多依赖关系时,事情很容易爆炸。在这种情况下,真正的解决方案是使您的组件更小。例如,如果你有 class 像“OrderService”和“CustomerService”这样的东西,它们可能会有一大堆功能和一大堆依赖项。这会导致无数问题;大对象图就是其中之一。但是,在现有应用程序中修复此问题通常并不容易;它需要不同的设计和不同的思维方式。

在这些场景中,改变组件的生活方式可能对应用程序的性能有益。您似乎已经在您的应用程序中建立了这一点。一般来说,将生活方式从 Transient 更改为 Scoped 是一个非常安全的改变。这就是为什么当您将 Transient 依赖项注入 Scoped 消费者时,Simple Injector v5 不再抱怨。

然而,当您有一个有状态的 Transient 组件时,情况就不是这样了,而每个消费者确实希望获得自己的状态;在这种情况下,将其更改为 Scope 实际上会破坏您的应用程序。但是,这不是我通常认可的设计。虽然我在过去见过几次这种类型的构图,但我从来没有在我的应用程序中这样做过; IMO 它会导致不必要的复杂性。

TLDR;

长话短说,有很多因素需要考虑,也许应用程序中有很多地方可以批准设计,但总的来说(尤其是在网络请求的上下文中)改变生活方式从 TransientScoped 的无状态组件通常是非常安全的。如果这会在您的应用程序中带来巨大的性能提升,您当然可以考虑将 Scoped 作为默认生活方式。