依赖注入
服务生存期
不要从单一实例解析范围内服务。 当处理后续请求时,它可能会导致服务处于不正确的状态。 可以:
- 从范围内或暂时性服务解析单一实例服务。
- 从其他范围内或暂时性服务解析范围内服务。
默认情况下在开发环境中,从具有较长生存期的其他服务解析服务将引发异常。
要在中间件中使用范围内服务,请使用以下方法之一:
- 将服务注入中间件的
Invoke
或InvokeAsync
方法。 使用构造函数注入会引发运行时异常,因为它强制使范围内服务的行为与单一实例类似。 - 使用基于工厂的中间件。 使用此方法注册的中间件按客户端请求(连接)激活,这也使范围内服务可注入中间件的
InvokeAsync
方法。
public async Task InvokeAsync(HttpContext context, IOperationScoped scopedOperation) { _logger.LogInformation("Transient: " + _transientOperation.OperationId); _logger.LogInformation("Scoped: " + scopedOperation.OperationId); _logger.LogInformation("Singleton: " + _singletonOperation.OperationId); await _next(context); }
向 AddSingleton 注册单一实例服务。 单一实例服务必须是线程安全的,并且通常在无状态服务中使用。
服务注册方法
上述任何服务注册方法都可用于注册同一服务类型的多个服务实例。
using ConsoleDI.IEnumerableExample; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using IHost host = CreateHostBuilder(args).Build(); _ = host.Services.GetService<ExampleService>(); await host.RunAsync(); static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => services.AddSingleton<IMessageWriter, ConsoleMessageWriter>() .AddSingleton<IMessageWriter, LoggingMessageWriter>() .AddSingleton<ExampleService>());
第二次对 AddSingleton
的调用在解析为 IMessageWriter
时替代上一次调用。 通过 IEnumerable<{SERVICE}>
解析服务时,服务按其注册顺序显示。
using System.Diagnostics; namespace ConsoleDI.IEnumerableExample; public sealed class ExampleService { public ExampleService( IMessageWriter messageWriter, IEnumerable<IMessageWriter> messageWriters) { Trace.Assert(messageWriter is LoggingMessageWriter); var dependencyArray = messageWriters.ToArray(); Trace.Assert(dependencyArray[0] is ConsoleMessageWriter); Trace.Assert(dependencyArray[1] is LoggingMessageWriter); } }
框架还提供 TryAdd{LIFETIME}
扩展方法,只有当尚未注册某个实现时,才注册该服务。
services.AddSingleton<IMessageWriter, ConsoleMessageWriter>(); services.TryAddSingleton<IMessageWriter, LoggingMessageWriter>(); //TryAddSingleton 的调用没有任何作用,因为 IMessageWriter 已有一个已注册的实现
TryAddEnumerable(ServiceDescriptor) 方法仅会在没有同一类型实现的情况下才注册该服务。 多个服务通过 IEnumerable<{SERVICE}>
解析。 注册服务时,如果还没有添加相同类型的实例,就添加一个实例。 库作者使用 TryAddEnumerable
来避免在容器中注册一个实现的多个副本。
public interface IMessageWriter1 { } public interface IMessageWriter2 { } public class MessageWriter : IMessageWriter1, IMessageWriter2 { } services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>()); services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter2, MessageWriter>()); services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());
IServiceScopeFactory 始终注册为单一实例,但 IServiceProvider 可能因包含类的生存期而异。 例如,如果从某个范围解析服务,而这些服务中的任意一种采用 IServiceProvider,该服务将是区分范围的实例。
若要在 IHostedService 的实现(例如 BackgroundService)中实现范围服务,请不要通过构造函数注入来注入服务依赖项。 请改为注入 IServiceScopeFactory,创建范围,然后从该范围解析依赖项以使用适当的服务生存期。
namespace WorkerScope.Example; public sealed class Worker : BackgroundService { private readonly ILogger<Worker> _logger; private readonly IServiceScopeFactory _serviceScopeFactory; public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory) => (_logger, _serviceScopeFactory) = (logger, serviceScopeFactory); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using (IServiceScope scope = _serviceScopeFactory.CreateScope()) { // } } } }
捕获依赖项
术语“捕获依赖项”由 Mark Seemann 提出,指的是服务生存期的配置不正确,其中具有较长生存期的服务捕获了具有较短生存期的服务。
应考虑通过将 validateScopes: true
传递到 BuildServiceProvider(IServiceCollection, Boolean) 来验证作用域。 验证作用域时,你将收到 InvalidOperationException
如果应用在 Development
环境中运行,并调用 CreateDefaultBuilder 以生成主机,默认服务提供程序会执行检查,以确认以下内容:
- 没有从根服务提供程序解析到范围内服务。
- 未将范围内服务注入单一实例。
调用 BuildServiceProvider 时创建根服务提供程序。
static void ScopedServiceBecomesSingleton() { var services = new ServiceCollection(); services.AddScoped<Bar>(); using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true); using (IServiceScope scope = serviceProvider.CreateScope()) { // Correctly scoped resolution Bar correct = scope.ServiceProvider.GetRequiredService<Bar>(); } // Not within a scope, becomes a singleton Bar avoid = serviceProvider.GetRequiredService<Bar>(); }
在前面的代码中,在 IServiceScope 中检索 Bar
,这是正确的。 反模式是作用域外的 Bar
检索,变量命名为 avoid
以显示不正确的示例检索。
避免在 ConfigureServices
中调用 BuildServiceProvider。正确方法是使用选项模式对 DI 的内置支持:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); services.AddOptions<CookieAuthenticationOptions>( CookieAuthenticationDefaults.AuthenticationScheme) .Configure<IMyService>((options, myService) => { options.LoginPath = myService.GetLoginPath(); }); services.AddRazorPages(); }