.NET 依赖注入

  • 依赖注入是什么
  • 内置Log
  • 使用拓展方法注册服务组(Register groups of services with extension methods)
  • .NET框架提供的服务(Framework-provided services)
  • 服务的生命周期(Service lifetimes)
    • Transient
    • Scoped
      • 备注:
    • Singleton
  • 服务注册方法
  • TryAdd{LIFETIME}
  • TryAddEnumerable(ServiceDescriptor)
  • 构造函数注入行为(Constructor injection behavior)
  • 作用域验证(Scope validation)
  • 范围场景(Scope scenarios)
  • 查看更多

原文链接

依赖注入是什么

.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,框架注册了数百个服务。
下表列出了框架注册的服务的一小部分:

服务类型 生命周期
IHostApplicationLifetime Singleton
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Singleton
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticListener Singleton
System.Diagnostics.DiagnosticSource Singleton

服务的生命周期(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 框架提供了适用于特定场景下注册服务的拓展方法:

Method Automatic object disposal Multiple implementations Pass args Example
Add{Lifetime}<{Service}, {Implementation}> Y Y N services.AddSingleton<IMyDep, MyDep>
Add{Lifetime}<{Service}>(sp => new {Implementation}) Y Y Y services.AddSingleton<IMyDep>(sp => new MyDep(99) )
Add{Lifetime}<{Implementation}>() Y N N services.AddSingleton<MyDep()>
AddSingleton<{Service}>(new {Implementation}) N Y Y services.AddSingleton<IMyDep>(new MyDep(99) )
AddSingleton(new {Implementation}) N N Y services.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);}
}

更多详情信息请参考:

  • TryAdd
  • TryAddTransient
  • TryAddScoped
  • TryAddSingleton

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

服务注册的顺序通常是无关的,除非注册一个服务的多重实现。
IServiceCollection是ServiceDescriptor对象的集合。下面的例子展示了如何通过创建和添加一个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)

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

  • IServiceProvider
  • ActivatorUtilities:
    • 创建没有在容器中注册的对象
    • 使用框架的一些特性

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

当使用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)

查看更多

  • Use dependency injection in .NET
  • Dependency injection guidelines
  • Dependency injection in ASP.NET Core
  • NDC Conference Patterns for DI app development
  • Explicit dependencies principle
  • Inversion of control containers and the dependency injection pattern (Martin Fowler)
  • DI bugs should be created in the github.com/dotnet/extensions repo

.NET中的依赖注入相关推荐

  1. 转: 理解AngularJS中的依赖注入

    理解AngularJS中的依赖注入 AngularJS中的依赖注入非常的有用,它同时也是我们能够轻松对组件进行测试的关键所在.在本文中我们将会解释AngularJS依赖注入系统是如何运行的. Prov ...

  2. JavaEE开发之Spring中的依赖注入与AOP编程

    上篇博客我们系统的聊了<JavaEE开发之基于Eclipse的环境搭建以及Maven Web App的创建>,并在之前的博客中我们聊了依赖注入的相关东西,并且使用Objective-C的R ...

  3. 理解AngularJS中的依赖注入

    作者 CraftsCoder 冷月无声 - 博客频道 - CSDN.NET http://blog.csdn.net/jaytalent/article/details/50986402 本文结合一些 ...

  4. spring中的依赖注入——构造函数注入、set方法注入( 更常用的方式)、复杂类型的注入/集合类型的注入

    spring中的依赖注入 依赖注入: Dependency Injection IOC的作用:降低程序间的耦合(依赖关系) 依赖关系的管理:以后都交给spring来维护.在当前类需要用到其他类的对象, ...

  5. Angular 中的依赖注入link

    Angular 中的依赖注入link 依赖注入(DI)是一种重要的应用设计模式. Angular 有自己的 DI 框架,在设计应用时常会用到它,以提升它们的开发效率和模块化程度. 依赖,是当类需要执行 ...

  6. ASP.NET CORE MVC 2.0 如何在Filter中使用依赖注入来读取AppSettings

    问: ASP.NET CORE MVC 如何在Filter中使用依赖注入来读取AppSettings 答: Dependency injection is possible in filters as ...

  7. SAP Spartacus 中的依赖注入 Dependency Injection 介绍

    先了解 Angular 中的依赖注入 依赖项是指某个类执行其功能所需的服务或对象.依赖项注入(DI)是一种设计模式,在这种设计模式中,类会从外部源请求依赖项而不是让类自己来创建它们. Angular ...

  8. ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理

    ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationI ...

  9. ASP.NET Core - 在ActionFilter中使用依赖注入

    上次ActionFilter引发的一个EF异常,本质上是对Core版本的ActionFilter的知识掌握不够牢固造成的,所以花了点时间仔细阅读了微软的官方文档.发现除了IActionFilter.I ...

  10. 如何在 Web Forms 中引入依赖注入机制

    依赖注入技术就是将一个对象注入到一个需要它的对象中,同时它也是控制反转的一种实现,显而易见,这样可以实现对象之间的解耦并且更方便测试和维护,依赖注入的原则早已经指出了,应用程序的高层模块不依赖于低层模 ...

最新文章

  1. linux设置NO_PROXY绕过代理
  2. Sentinel(十九)之主流框架的适配
  3. 命题公式的主合取范式C语言,命题公式主范式的自动生成与形式输出.pdf
  4. 带你了解敏捷和DevOps的发布策略
  5. 【数论学习笔记】同余
  6. 在 Android* 平台上设置原生 OpenGL ES*
  7. 【C】动态申请二维数组
  8. Flume+Kafka+storm的连接整合
  9. 软考高级《信息系统项目管理师》(简称高项)考证经验(满满的干货)
  10. linux自动切换网,linux使用shell自动切换网关
  11. 短视频解析 MD5修改 ,为什么要修改MD5
  12. html中<img src=““ alt=““>标签里面alt的作用
  13. 计算机配置单性价比高,i5电脑主机配置单,性价比高
  14. android 录屏工具,android实现录屏小功能
  15. web页面-JS/DOM/BOM/窗口滚动/修改内容/上传文件
  16. web_socket 协同文档编辑
  17. linux克隆步骤,CentOS克隆机器步骤,图文教程
  18. 开通阿里云的对象存储服务OSS
  19. 【CSS】字体、行高、文本对齐
  20. 众多场景已经全面普及智能取餐柜

热门文章

  1. Python读取复杂电子表格(CSV)数据小技巧一则
  2. uniapp使用七牛云上传
  3. 社会化招聘,又出新高度!
  4. Codeforces_723_D
  5. 数据库管理员-DBA-高级
  6. 江门附近计算机软件培训构,江门cad培训cad制图培训班
  7. 惠普企业旗下软件业务与Mirco Focus合并
  8. CSS3咖啡制作全过程动画
  9. php+只能继承一次,php继承相关的一个问题
  10. icloud电脑设置_如何在Android上设置iCloud电子邮件访问