依赖注入

服务生存期

不要从单一实例解析范围内服务。 当处理后续请求时,它可能会导致服务处于不正确的状态。 可以:

  • 从范围内或暂时性服务解析单一实例服务。
  • 从其他范围内或暂时性服务解析范围内服务。

默认情况下在开发环境中,从具有较长生存期的其他服务解析服务将引发异常。

要在中间件中使用范围内服务,请使用以下方法之一:

  • 将服务注入中间件的 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>());

IServiceCollection 是 ServiceDescriptor 对象的集合。

 

范围场景

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();
}

 

posted @ 2020-08-28 14:33  yetsen  阅读(201)  评论(0编辑  收藏  举报