1 DI概念

1.1 基本定义

依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IOC)思想的实现方式
简化模块的组装过程,降低模块之间的耦合度。

1.2 理解及实现方式

代码控制反转的目的:“怎样创建xxx对象”—>”我要xxx对象”
两种实现方式:1)服务定位器(ServiceLocator) 2)依赖注入(Dependency Injection,DI),省略GetService步骤

1.3 几个关键概念

服务(service):要获得的对象
注册服务:将对象注册进去
服务容器:负责管理注册的服务
查询服务:创建对象及关联对象
对象生命周期(销毁的时间):Transient(瞬态)每次获取都获得一个新的对象 Scoped(范围) Singleton(单例)同一个对象

1.4 .NET中的DI

Interface ITest
TestInterfaceImplementA:ITest
TestInterfaceImplementB:ITest

  • 根据类型来获取和注册服务:可以分别指定服务类型(service type)和实现类型(implementattion type)。服务类型尽可能的选择是接口,面向接口编程。
  • .NET控制反转组件取名为DependencyInjection,但它包含ServiceLocator功能。

使用DI 2:

  1. 1.Install-Package Microsoft.Extensions.DependencyInjection
  2. 2.using Microsoft.Extensions.DependencyInjection
  3. 3.ServiceCollection 用来构造容器对象IServiceProvider 调用ServiceCollectionBuildServiceProvider()创建的ServiceProvider,可以用来获取BuildServiceProvider()之前ServiceCollection中的对象。

2 服务的生命周期

AddSingleton拿到的是同一个对象
Scope创建范围,创建一个CreateScope() 就是一个范围,通常using的范围就是Scope范围
当一个类没有成员变量,没有属性的类,建议声明为Singleton。如果类有状态且有Scope范围,建议为Scoped。因为通常这种Scope控制下的代码都是运行在同一线程中的,没有并发修改的问题;在使用Transient瞬态的时候要谨慎。

image.png

  1. using System;
  2. using Microsoft.Extensions.DependencyInjection;
  3. namespace DIAndIOC
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. //创建一个容器
  10. var serviceCollection = new ServiceCollection();
  11. //将对象注入到瞬态中
  12. serviceCollection.AddTransient<TestServiceImpA>();
  13. //将对象创建成实例,服务定位器
  14. using (var sp = serviceCollection.BuildServiceProvider())
  15. {
  16. //我要容器里这个对象TestServiceImpA
  17. var testServiceImpA = sp.GetService<TestServiceImpA>();
  18. testServiceImpA.Name = "Albert";
  19. testServiceImpA.ShowName();
  20. }
  21. //创建一个容器
  22. var serviceCollection2 = new ServiceCollection();
  23. //将对象注入到范围中,在范围内拿到的是同一个对象
  24. serviceCollection2.AddScoped<TestServiceImpA>();
  25. using (var sp = serviceCollection2.BuildServiceProvider())
  26. {
  27. using(var scope = sp.CreateScope())
  28. {
  29. var testServiceImpA = scope.ServiceProvider.GetService<TestServiceImpA>();
  30. testServiceImpA.Name = "Albert";
  31. testServiceImpA.ShowName();
  32. }
  33. }
  34. Console.ReadLine();
  35. }
  36. }
  37. interface ITestService
  38. {
  39. public string Name { get; set; }
  40. public void ShowName();
  41. }
  42. public class TestServiceImpA : ITestService
  43. {
  44. public string Name { get; set; }
  45. public void ShowName()
  46. {
  47. Console.WriteLine(this.Name);
  48. }
  49. }
  50. }

3 其他注册方法

  • 服务类型和实现类型不一致的注册(尽量注册一个接口,服务类型为接口)
  • 简单看看其他Add*方法:
  • T GetService() 如果找不到会返回Null object GetService(typeof(T))
  • GetRequiredService() 如果找不到直接异常
  • IEnumerable GetServices()适用于可能有多个满足条件的服务

*T是根据注册服务类型来的,上面是ITestService,这边必须是ITestService,找不到服务返回null**

  1. static void Main(string[] args)
  2. {
  3. //new collection--addscope声明服务和对象--BuilderServicerProvider()创建服务提供者
  4. //--GetService<T>获取服务
  5. ServiceCollection serviceCollection = new ServiceCollection();
  6. //服务类型和实现类型
  7. serviceCollection.AddScoped<ITestService,TestServiceImpA>();
  8. using (var sp = serviceCollection.BuildServiceProvider())
  9. {
  10. //此处不关心你实现类是什么
  11. //GetService<T>() T是根据注册服务类型来的,上面是ITestService,这边必须是ITestService,找不到服务返回null
  12. var ts = sp.GetService<ITestService>();
  13. Console.WriteLine(ts.GetType());
  14. Console.ReadLine();
  15. }
  16. }

4 依赖注入DI显威力

  • 依赖注入具有“传染性”,如果一个类的对象是通过DI创建,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但是如果一个对象是成员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值。(服务相互依赖,多层依赖关系,自动创建其他依赖服务,根服务是自己创建的)
  • .NET的DI默认是构造函数注入(Java中的Spring是属性注入)
  • 例子:编写一个类,连接数据库做插入操作,并且记录日志(模拟的输出),把Dao、日志都放入单独的服务类,connstr见备注。
  • 总结:依赖注入用于多个接口(服务)相互依赖的情景下,接口A依赖接口B,要在A中通过构造函数给接口B的属性(接口A)进行赋值,在使用BuilderServiceProvider之前要将所有的服务注册进去,服务和实现类一起注入其中。Controller必须是容器创建出来的,然后DI将服务传进去。 ```csharp using Microsoft.Extensions.DependencyInjection; using System;

namespace _210720_Demon02_InfectDI { class Program { static void Main(string[] args) { ServiceCollection services = new ServiceCollection(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();

  1. using (var sp = services.BuildServiceProvider()) {
  2. var controller = sp.GetRequiredService<Controller>();
  3. controller.Test();
  4. }
  5. Console.ReadLine();
  6. }
  7. }
  8. class Controller {
  9. private readonly ILog log;
  10. private readonly ICloud cloud;
  11. public Controller(ILog log, ICloud cloud) {
  12. this.log = log;
  13. this.cloud = cloud;
  14. }
  15. public void Test() {
  16. this.log.Log("开始上传");
  17. this.cloud.Save(".NET第一行代码", "albert.txt");
  18. this.log.Log("上传完毕");
  19. }
  20. }
  21. interface ILog {
  22. public void Log(string msg);
  23. }
  24. interface IConfig {
  25. public string GetValue(string name);
  26. }
  27. interface ICloud {
  28. public void Save(string content, string name);
  29. }
  30. class LogImp : ILog {
  31. public void Log(string msg) {
  32. Console.WriteLine($"日志:{msg}");
  33. }
  34. }
  35. class ConfigImp : IConfig {
  36. public string GetValue(string server) {
  37. return $"返回服务名为{server}";
  38. }
  39. }
  40. class CloudImp : ICloud {
  41. private readonly IConfig config;
  42. public CloudImp(IConfig config) {
  43. this.config = config;
  44. }
  45. public void Save(string content, string name) {
  46. string server = config.GetValue("albert_server");
  47. Console.WriteLine($"向服务{server}中上传的内容为{content},内容保存在{name}文件下");
  48. }
  49. }

}

```

5 DI综合案例1

需求说明:
1.目的:演示DI的能力
2.有配置服务、日志服务,然后再开发一个邮件发送器服务(MailK)。可以通过配置服务来从文件、环境变量、数据库等地方读取配置,可以通过日志服务来将程序运行过程中的日志信息写入文件、控制台、数据库等。
3.说明:案例中开发了自己的日志、配置等接口,这只是在揭示原理,.NET有现成的。
第一个根上对象只能用服务定位器的方式,如果需要属性赋值在构造函数中则需写成:service.AddScoped(typeof(IT),s=>new TImp(){xxx=xxx})
image.png
image.png
代码:https://gitee.com/hongyongzhao/dot-net
项目克隆:https://gitee.com/hongyongzhao/dot-net.git

6 DI综合案例2

实现一个“可覆盖的配置读取器”(集群:多台服务器—从单本地配置文件web.config从中心化配置服务器中一键修改所有的配置文件)
image.png
image.png

7 总结

image.png