.NET中的依赖注入

原文链接

依赖注入是什么

.NET支持依赖注入(DI)软件设计模式,这是一种用于在类和它们的依赖项之间控制反转(Ioc)的技术。
在.NET中,依赖注入,配置项(configuration),日志(logging)还有选项模式(options pattern)是第一类对象(first-class citizen)

依赖项是另一个对象所依赖的对象。思考下面的MessageWriter类,它有一个Write方法,并且有另外一个类依赖它。

public class MessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

别的类通过实例化MessageWriter类以调用它的Write方法。在下面的例子中,MessageWriter类是Worker类的一个依赖项。

public class Worker : BackgroundService
{
    private readonly MessageWriter _messageWriter = new MessageWriter();

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1000, stoppingToken);
        }
    }
}

该类创建的时候直接依赖于 MessageWriter类。像例子中这样,硬编码的依赖项是有问题的,应该避免这样使用,理由如下:

  • 如果要使用不同的实现替换MessageWriter 类,Worker类就必须修改。
  • 如果MessageWriter类有依赖项,则也必须由Workers类配置。在有多个类依赖于MessageWriter大型项目中,配置将会散布在整个App中。
  • 要实现单元测试会很困难。App应该使用模拟的或者存根(stub)MessageWriter类,以这种方式是不可能实现的。

依赖注入解决了这些问题通过:

  • 使用接口或基类来抽象依赖关系实现。
  • 在服务容器中注册依赖项。在.NET中,提供了一个内置的服务容器, IServiceProvider。服务通常在App启动时注册,并添加到IServiceCollection中。当添加完所有的依赖项后,需要使用BuildServiceProvider创建服务容器。
  • 将服务注入到使用到的类的构造函数中。.NET框架负责创建服务的实例并在不需要它们的时候销毁它们。

举个例子,IMessageWriter接口定义了Write方法:

namespace DependencyInjection.Example
{
    public interface IMessageWriter
    {
        void Write(string message);
    }
}

这个接口被一个实体类MessageWriter实现:

using System;

namespace DependencyInjection.Example
{
    public class MessageWriter : IMessageWriter
    {
        public void Write(string message)
        {
            Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
        }
    }
}

以下示例代码使用实体类MessageWriter类注册了IMessageWriter服务。AddScoped方法注册的服务生命周期为Scoped,也就是单个请求(single request)的生命周周期。服务的生命周期将在本文稍后介绍。

using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DependencyInjection.Example
{
    class Program
    {
        static Task Main(string[] args) =>
            CreateHostBuilder(args).Build().RunAsync();

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddHostedService<Worker>()
                            .AddScoped<IMessageWriter, MessageWriter>());
    }
}

以下示例App中,使用IMessageWriter服务调用Write方法。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

namespace DependencyInjection.Example
{
    public class Worker : BackgroundService
    {
        private readonly IMessageWriter _messageWriter;

        public Worker(IMessageWriter messageWriter) =>
            _messageWriter = messageWriter;

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

通过使用依赖注入的方式,worker服务:

  • 不需要使用实体类MessageWriter,仅使用IMessageWriter接口实现它。这将使更换控制器使用的实现更容易,而不需要更改控制器。
  • 由依赖注入创建MessageWriter的实例而不需要自己创建。

IMessageWriter接口的实现可以通过使用内置的日志API改进:

using Microsoft.Extensions.Logging;

namespace DependencyInjection.Example
{
    public class LoggingMessageWriter : IMessageWriter
    {
        private readonly ILogger<LoggingMessageWriter> _logger;

        public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
            _logger = logger;

        public void Write(string message) =>
            _logger.LogInformation(message);
    }
}

更新的ConfigureServices方法注册了IMessageWriter的新实现:

static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((_, services) =>
            services.AddHostedService<Worker>()
                    .AddScoped<IMessageWriter, LoggingMessageWriter>());

LoggingMessageWriter类依赖于ILogger,在构造函数中被需要。ILogger<TCategoryName> 类是.NET框架内置的服务(framework-provided service)。

以链式方式使用依赖注入也很常见。每个被依赖项又依赖于它们自己的依赖项。容器解析图中的依赖关系并返回完全解析的依赖服务。必须被解析的依赖关系的集合通常也被称为“依赖关系树”,“赖关系图”或“对象图”。

容器通过利用“(generic)开放类型”(open types)解析ILogger<TCategoryName>,从而无需注册每个(generic)构造类型(constructed type)。

在依赖注入的术语中,服务(service):

  • 通常是一个给其它对象提供服务的对象,比如IMessageWriter服务。
    Is typically an object that provides a service to other objects, such as the IMessageWriter service.
  • 和Web服务无关,尽管它可能使用Web服务。
    Is not related to a web service, although the service may use a web service.

内置Log

.NET框架提供了一个可靠的日志系统。
在前边示例中展示的IMessageWriter的实现是为了演示基本的DI编写的,而不是为了实现日志。大多数的App都不需要编写日志记录器。下面的代码展示了如何使用默认的日志纪记录器,只需要将Worker注册到ConfigureServices中作为一个托管服务(AddHosetedService):

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger) =>
        _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

使用上述代码不需要更新ConfigureServices,因为日志是由.NET 框架提供的。

使用拓展方法注册服务组(Register groups of services with extension methods)

Microsoft Extensions 使用一种约定注册一组相关的服务。
该约定使用.NET 框架的一个特性 Add{Group_Name} 拓展方法来注册所有需要的服务。例如,AddOptions拓展方法注册使用选项(options)所需的所有服务。

.NET框架提供的服务(Framework-provided services)

ConfigureServices方法注册App使用的所有服务,包括平台特性(platform features)。
最初,提供给ConfigureServicesIServiceCollection具有框架定义的服务,取决于主机是如何配置的(how the host was configured)。对于基于.NET模板的APP,框架注册了数百个服务。
下表列出了框架注册的服务的一小部分:

服务类型生命周期
IHostApplicationLifetimeSingleton
Microsoft.Extensions.Logging.ILogger<TCategoryName>Singleton
Microsoft.Extensions.Logging.ILoggerFactorySingleton
Microsoft.Extensions.ObjectPool.ObjectPoolProviderSingleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions>Singleton
Microsoft.Extensions.Options.IOptions<TOptions>Singleton
System.Diagnostics.DiagnosticListenerSingleton
System.Diagnostics.DiagnosticSourceSingleton

服务的生命周期(Service lifetimes)

可以使用以下生命周期注册服务:

  • Transient (译: 瞬时的)
  • Scoped (译: 作用域的)
  • Singleton (译: 单例的)

要为每个服务选择合适的生命周期。

Transient

生命周期为Transient 的服务,在每次被请求时从服务容器创建。
这种生命周期适合轻量的、无状态的服务。
使用 AddTransient注册 Transient类型的服务。

Scoped

对于Web应用,Soped 生命周期指明 在每次客户端请请求(连接)时创建服务。
使用AddSoped注册 Scoped类型的服务。
在处理请求的“App中,Scoped类型的服务在请求结束时被销毁(Dispose)。

当使用Entity Framework Core时,AddDbContext拓展方法默认使用Scoped注册DbContext类型。

备注:

不要直接或间接地从 singleton类型的服务解析(resolve) scoped类型的服务,比如通过 transient 类型的服务。
这可能导致在处理后续请求时服务处于不正确的状态。
这样做是可以的:

从Scoped 或 Transient服务解析Singleton类型的服务。
从另一个Scoped类型或者Transient类型的服务解析Scoped类型的服务。

默认情况下,在开发环境中,从一个具有更长的生命周期的服务解析另一个服务将会引发异常。更详细的信息,参考 Scope validation

Singleton

以下情况下创建 Singleton 生命周期的服务:

  • 当服务第一次被请求时
  • 在直接向容器提供实现的实例时由开发人员创建。

后续每一个请求的来自依赖注入容器的服务实现都使用同一个实例。
如果App需要Singleton行为模式,则允许服务容器管理服务的生命周期。

不要实现单例设计模式或提供释放单例服务的代码。

永远不应该通过代码释放从容器解析的单例服务。
如果一种类型或工厂注册为单例,容器会自动释放该单例。

使用 AddSingleton注册 Singleton类型的服务。
单例服务必须是线程安全的,并且通常在无状态的服务中使用。

服务注册方法

.NET 框架提供了适用于特定场景下注册服务的拓展方法:

MethodAutomatic object disposalMultiple implementationsPass argsExample
Add{Lifetime}<{Service}, {Implementation}>YYNservices.AddSingleton<IMyDep, MyDep>
Add{Lifetime}<{Service}>(sp => new {Implementation})YYYservices.AddSingleton<IMyDep>(sp => new MyDep(99) )
Add{Lifetime}<{Implementation}>()YNNservices.AddSingleton<MyDep()>
AddSingleton<{Service}>(new {Implementation})NYYservices.AddSingleton<IMyDep>(new MyDep(99) )
AddSingleton(new {Implementation})NNYservices.AddSingleton(new MyDep(99)

传递参数指 向实现的构造函数传递参数

更多关于可释放类型(type disposal)的信息,请参阅 Disposal of services 部分。

仅使用实现类型注册服务等效于 使用相同的实现类型和服务注册服务。因此不能使用没有显式声明服务类型的方法注册服务的多重实现。这些方法可以给服务注册多个实例,但是所有实例都使用相同的实现类型。

上述的任何一种服务注册方法都能用来注册同一个服务的多个服务实例。下面的例子中以IMessage为服务类型调用了AddSingleton两次。
第二次对AddSingleton的调用在解析为IMessageWriter时覆盖了前一次调用,并在通过IEnumerable<IMessageWriter> 解析多个服务时添加到上一次调用。
通过IEnumerable<{SERVICE}> 解析服务时,服务按其注册顺序呈现。

using System.Threading.Tasks;
using ConsoleDI.IEnumerableExample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ConsoleDI.Example
{
    class Program
    {
        static Task Main(string[] args)
        {
            using IHost host = CreateHostBuilder(args).Build();

            _ = host.Services.GetService<ExampleService>();

            return host.RunAsync();
        }

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddSingleton<IMessageWriter, ConsoleMessageWriter>()
                            .AddSingleton<IMessageWriter, LoggingMessageWriter>()
                            .AddSingleton<ExampleService>());
    }
}

上述示例的源代码为IMessageWriter注册了两种实现。

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace ConsoleDI.IEnumerableExample
{
    public 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);
        }
    }
}

ExampleService定义了两个构造函数参数,一个是IMessageWriter单例,另一个是IEnumerable<IMessageWriter>
IMessageWriter单例是已经注册的最后一个实现,而**IEnumerable<IMessageWriter>是所有已经注册的实现。

TryAdd{LIFETIME}

.NET框架还提供了TryAdd{LIFETIME} 拓展方法,仅在还没有服务的实现被注册时才注册该服务。
在下面的例子中,对AddSingleton的调用注册了ConsoleMessageWriter作为服务IMessageWriter的实现。随后对TryAddSingleton的调用没有造成任何影响,因为IMessageWriter服务已经有了注册了的实现。

services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
services.TryAddSingleton<IMessageWriter, LoggingMessageWriter>();

TryAddSingleton不起作用,因为该服务已经被添加并且“try”将会失败。**ExampleService **将断言以下内容:

public class ExampleService
{
    public ExampleService(
        IMessageWriter messageWriter,
        IEnumerable<IMessageWriter> messageWriters)
    {
        Trace.Assert(messageWriter is ConsoleMessageWriter);
        Trace.Assert(messageWriters.Single() is ConsoleMessageWriter);
    }
}

更多详情信息请参考:

TryAddEnumerable(ServiceDescriptor)

TryAddEnumerable(ServiceDescriptor)方法仅在没有注册同一类型的实现 时注册该服务。多个服务通过IEnumerable<{SERVICE}> 实现。注册服务时,如果还没有添加同类型的实例,就添加该实例。库开发者通过使用TryAddEnumerable避免在容器中注册一个实现的多个副本。

在下述例子中,第一次调用TryAddEnumerable注册了MessageWriter作为IMessageWriter1的一个实现。第二次调用TryAddEnumerable注册MessageWrite作为IMessageWriter2的实现。而第三次调用则没有影响,因为已经注册了MessageWriter作为IMessageWriter1的实现。

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

服务注册的顺序通常是无关的,除非注册一个服务的多重实现。
IServiceCollectionServiceDescriptor对象的集合。下面的例子展示了如何通过创建和添加一个ServiceDescription来注册服务:

string secretKey = Configuration["SecretKey"];
var descriptor = new ServiceDescriptor(
    typeof(IMessageWriter),
    _ => new DefaultMessageWriter(secretKey),
    ServiceLifetime.Transient);

services.Add(descriptor);

内置的**Add{LIFETIME}**方法使用同一种方式。示例参考AddScoped source code

构造函数注入行为(Constructor injection behavior)

服务可以通过以下方式解析:

构造函数可以接受不是由依赖注入提供的参数,但是该参数必须要有默认值。

当使用IServiceProviderActivatorUtilities解析服务时,构造函数注入需要public类型的构造函数。

当使用ActivatorUtilities解析服务时,构造函数注入要求只存在一个可用的构造函数。支持构造函数重载,但其参数可以全部通过依赖注入来实现的重载只能存在一个。

作用域验证(Scope validation)

当App是在开发环境(Development) 下运行并且调用CreateDefaultBuilder 方法创建主机(build the host),默认服务提供程序会执行检查,确认以下内容:

  • 没有从根服务提供程序解析到范围内服务
    Scoped services aren’t resolved from the root service provider
  • 未将范围内服务注入单一实例。
    Scoped services aren’t injected into singletons.

调用BuildServiceProvider时会创建根服务提供程序(root service provider)。根服务提供程序 的生命周期在它被创建时绑定到App的生命周期上,并在应用关闭时释放。

有作用域的服务(Scoped services)由创建它们的容器销毁。如果一个有作用域的服务由根容器创建,这个服务的生命周期将提升到单例,因为它只有在App关闭时才由根容器销毁。

范围场景(Scope scenarios)

IServiceScopeFactory 总是注册为单例,但是IServiceProvider可以根据包含的类的生命周期而有所不同。比如,从作用域(scope)解析服务,并且这些服务中的任何服务都使用IServiceProvider,那它将是一个作用域实例。

为了在IHostedService的实现中实现作用域服务,比如BackgroundService,不要通过构造函数注入的方式注入服务。应该注入IServiceScopeFactory,创建作用域,然后从该作用域中解析依赖以使用恰当的服务生命周期。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace WorkerScope.Example
{
    public 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()) //注意
                {
                    try
                    {
                        _logger.LogInformation(
                            "Starting scoped work, provider hash: {hash}.",
                            scope.ServiceProvider.GetHashCode());

                        var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
                        var next = await store.GetNextAsync();
                        _logger.LogInformation("{next}", next);

                        var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
                        await processor.ProcessAsync(next);
                        _logger.LogInformation("Processing {name}.", next.Name);

                        var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
                        await relay.RelayAsync(next);
                        _logger.LogInformation("Processed results have been relayed.");

                        var marked = await store.MarkAsync(next);
                        _logger.LogInformation("Marked as processed: {next}", marked);
                    }
                    finally
                    {
                        _logger.LogInformation(
                            "Finished scoped work, provider hash: {hash}.{nl}",
                            scope.ServiceProvider.GetHashCode(), Environment.NewLine);
                    }
                }
            }
        }
    }
}

上述代码中,当App运行时,后台服务程序:

  • 依赖IServiceScopeFactory
    (Depends on the IServiceScopeFactory)
  • 创建了一个IServiceScope来解析其它服务
    (Creates an IServiceScope for resolving additional services)
  • 解析作用域服务以供消费
    (Resolves scoped services for consumption)
  • 处理对象,然后转发它们,最后将它们标记为已处理。
    (Works on processing objects and then relaying them, and finally marks them as processed)

从示例源代码中,你可以看到IHostedService的实现是如何从作用域服务生命周期中获益的。
(From the sample source code, you can see how implementations of IHostedService can benefit from scoped service lifetimes)

查看更多

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值