牢记 .NET 依赖注入指南建议:
- 避免使用服务定位器模式。例如,当您可以使用 DI 时,不要调用 GetService 来获取服务实例。
- 要避免的另一个服务定位器变体是注入一个在运行时解析依赖关系的工厂。这两种做法都混合了控制反转策略。
.NET 文档说我们可以通过
IServiceProvider
或 IServiceScopeFactory
使用范围服务,如 here 或 here,例如
public class ConsumeScopedServiceHostedService: BackgroundService
{
// ...
public ScopedProcessingService(IServiceProvider services) { // ... }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using (var scope = _services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<FooDbContext>();
// ...
}
}
另一种方式是工厂的一些变体,例如
public class ConsumeFactoryHostedService: BackgroundService
{
// ...
public ConsumeFactoryHostedService(Func<FooDbContext> dbContextFactory) { // ... }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using (var dbContext = _dbContextFactory()) { // ... }
}
}
这两种实现方式不都违背了前面提到的建议吗?可能在很多情况下“这并不重要”(特别是对于“简单”的微服务),但我仍然对“最正确”的方式感兴趣 - 那么,我们是否应该使用另一种模式? 我们应该如何在
DbContext
中使用短暂的
IHostedService
实例?
这两种实现方式不都违背了前面提到的建议吗?
是的,但建议并不完整。要理解为什么它们不完整,我们必须了解Mark Seemann的Composition Root概念。组合根是
是应用程序中模块组合在一起的单个逻辑位置。
在他的书《.NET 中的依赖注入》中,Mark Seemann 将组合根模式描述为依赖注入的基本概念。虽然我读过那本书几次,但我更熟悉第二版《依赖注入原则、实践和模式》,因为我是该版本的合著者。这就是为什么接下来我将仅引用该版本的内容。 在第二版中,马克和我提到:
如果您使用 DI 容器,则组合根应该是您使用 DI 容器的唯一位置。在组合根之外使用 DI 容器会导致服务定位器反模式 [§4.1]
“服务定位器反模式”是 Microsoft 文档建议反对的,正如我们在书中所做的那样。书中将服务定位器定义为:服务定位器为组合根外部的应用程序组件提供对无限的易失性依赖项的访问权限。 [§5.2]
在组合根之外使用 DI 容器会导致服务定位器反模式 [§4.1]
之所以在组合根内使用 DI 容器是可以的,并且不被视为服务定位器反模式的应用,是因为在这种情况下,它不会表现出服务定位器反模式的缺点。从某种意义上说,与在组合根之外使用相比,它在应用程序上的行为完全不同。或者,正如 Mark Seemann 过去优雅地描述的那样:服务定位器是关于它在应用程序中扮演的角色,而不是机制。或者,换句话说,如果您甚至无法在组合根中
使用 DI 容器,那么您最终将根本不使用 DI 容器。因此,这对于您的托管服务的特定情况意味着如下:
您的托管服务允许依赖于IServiceProvider
(或其他特定于容器的接口)...只要托管服务位于组合根内部。
但是,组合根不应包含任何应用程序或业务逻辑。它应该具有引导应用程序所需的基础设施。换句话说,允许将依赖项绑定在一起,管理它们的生命周期,定义后端系统的适配器,并实现横切关注点。
这意味着您的托管服务本身应该尽可能小,并且只处理基础设施。您可以通过从托管服务中提取所有应用程序逻辑并将其放置在单独的类中来实现此目的。该类本身可能有许多依赖项注入到其构造函数中。
这样,托管服务唯一要做的就是启动一个新的IServiceScope
,解析提取的类,调用它,并释放服务范围。例如:
// This class is part of the Composition Root
public class ConsumeScopedServiceHostedService : BackgroundService
{
public ScopedProcessingService(IServiceProvider services) ...
protected override async Task ExecuteAsync(CancellationToken token)
{
using (var scope = _services.CreateScope())
{
// ExtractClassWithTheLogic is part of the application
var service = scope.ServiceProvider
.GetRequiredService<ExtractClassWithTheLogic>();
service.Run(token);
}
}
}