本文通过一个维修工与工具库的例子形象的描述一下为什么要用依赖注入它的工作原理是什么样的, 然后根据这个类比一下 ASP.NET Core 中的依赖注入, 从而深刻了解它的使用方法注意事项以及回收机制等.
本文主要内容:
1. 为什么要用依赖注入(DI)
2. 容器的构建和规则
3.ASP.NET Core 2.0 中的依赖注入
4. 使用方法及需要注意的问题
5. 服务的 Dispose
6. 我想换个容器
1. 为什么要用依赖注入(DI)
什么是依赖注入就不说了, 为什么要使用呢?
软件设计原则中有一个依赖倒置原则 (DIP) 讲的是要依赖于抽象, 不要依赖于具体, 高层模块不应该依赖于低层模块, 二者应该依赖于抽象简单的说就是为了更好的解耦而控制反转 (Ioc) 就是这样的原则的其中一个实现思路, 这个思路的其中一种实现方式就是依赖注入(DI)
感觉有点绕, 举个栗子: 老李是一个维修工, 现在要出任务去维修, 得先去申领个扳手
图一
老李: "请给我一把可以可以拧 7mm 大小的六角螺丝的扳手.", 然后库管老张就从仓库里拿了一把这样的 大力牌扳手给老李
在这个例子中, 维修工老李只要告诉库管我要一个 "可以拧 7mm 大小的六角螺丝" 的扳手即可, 他不用关心扳手的品牌和样式, 也不用采购扳手, 更不用关心这个扳手是怎么来的. 而对于库管, 他只需提供满足这样规则的一个扳手即可, 不用去关心老李拿着这个扳手之后去干什么所以老李和老张都只是关心 "可以拧 7mm 大小的六角螺丝的" 这个规则即可, 也就是说, 如果后期仓库里不再提供 大力牌扳手, 而是提供了 这样的大牛牌扳手, 无论换了什么牌子和样式, 只要仍满足这个规则, 老李仍然可以正常工作. 它们定义了一个规则 (比如接口 IWrench7mm), 二者都依赖于这个规则, 然后仓库无论提供大力牌(WrenchDaLi : IWrench7mm) 还是大牛牌(WrenchDaNiu : IWrench7mm), 都不影响正常工作.
这就是依赖倒置原则 (DIP), 不依赖于具体(牌子), 高层模块(老李) 不应该依赖于低层模块 (大力牌扳手), 二者应该依赖于抽象(IWrench7mm: 可以拧 7mm 大小的六角螺丝) 如果直接由老李去获取 (new) 大力牌扳手, 那么当业务改变要求采用大牛牌的时候, 我们就要去修改老李的代码. 为了解耦, 在本例中我们只要在配置中让仓库由原来的提供大力牌改为提供大牛牌即可老李要使用的时候, 可以通过注入 (构造器属性方法) 的方式, 将仓库提供的扳手实例提供给老李使用
2. 容器的构建和规则
继续上面的例子, 库管老张为什么会提供给老李大力牌而不是大牛牌的扳手呢? 那是因为领导给了他一份构建仓库的物品购置及发放清单:
A. 当有人要 7mm 的六角扳手的时候, 给他一个大力牌的扳手, 当再有人来要的时候就再给另一把
B. 但对于相机, 每个小组只能给一台, 小组内所有人共用这一台
C. 卡车更是全单位只有一辆, 谁申请都是同一辆
图二
3.ASP.NET Core 2.0 中的依赖注入
首先看一下下面的图三
图三
这就是 ASP.NET Core 中默认的依赖注入方式, 对比一下图二是不是很像?
上篇文章说要将 Startup 放大介绍一下, 那么打开 Startup 这个文件, 看一下里面的 ConfigureServices 方法顾名思义, 这个方法是用来配置服务,
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc();
- }
此方法接收一个 IServiceCollection 类型的参数, 查看它的定义, 被定义在 Microsoft.Extensions.DependencyInjection 这个 NuGet 包中, 功能就是依赖注入, 在 ASP.NET Core 中被广泛使用.
IServiceCollection
它正是图三中的 IServiceCollection, 它是一个 IList<ServiceDescriptor > 类型的集合也就是上门的维修工的例子中领导制定的清单, 而 Startup 中的 ConfigureServices 这个方法的作用就是让我们作为 "领导" 来配置这个清单方法中默认调用的 services.AddMvc(), 是 IServiceCollection 的一个扩展方法 public static IMvcBuilder AddMvc(this IServiceCollection services); , 作用就是向这个清单中添加了一些 MVC 需要的服务, 例如 AuthorizationRazorViewEnginDataAnnotations 等
系统需要的添加好了, 剩下的就是我们把自己需要的用的添加进去了 这里我们可以创建一个 ServiceDescriptor 然后把它添加到这个集合里, 系统 IServiceCollection 也提供了 AddSingletonAddScoped 和 AddTransient 这样的方法, 三种方法定义了所添加服务的生命周期, 具体见 ServiceDescriptor.
当然我们可以在 ConfigureServices 中通过一堆 AddXXX 将服务添加到 IServiceCollection, 但这样好多堆在一起不易于修改和阅读, 特别还有一些功能会包含好几个服务的添加, 所以推荐像系统默认的 AddMvc() 这样封装到一个扩展方法中去
现在来看一下清单中的内容
ServiceDescriptor
既然 IServiceCollection 是一个 IList<ServiceDescriptor>, 那么 ServiceDescriptor 也就是这个集合中的内容了, 也就是仓库中物品的描述. 对照图三中的 ServiceDescriptor 看一下它的各个属性
A. Type ServiceType: 服务的类型 --7mm 六角扳手
B. Type ImplementationType: 实现的类型 -- 大力牌扳手
C. ServiceLifetime Lifetime: 服务的生命周期 -- 若干(谁要都给一把新的)
D. object ImplementationInstance: 实现服务的实例
E: Func<IServiceProvider, object> ImplementationFactory: 创建服务实例的工厂
ServiceLifetime 是一个枚举, 上文说的 AddSingletonAddScoped 和 AddTransient 就是对应这个枚举, 分别为:
Singleton: 单例, 例子中的卡车, 全单位只有一辆, 谁调用都是返回这个实例
Scoped: 区域内单例, 例子中的傻瓜相机, 每小组一台, 小组内谁要都是同一台, 不同小组的相机不同
Transient: 临时的 例子中的扳手和锤子, 谁要都给一把新的, 所有人的都不是同一把
从这些属性的介绍来看, ServiceDescriptor 规定了当有人需要 ServiceType 这个类型服务的时候, 提供给他一个 ImplementationType 类型的实例, 其他几个属性规定了提供的方法和生命周期.
IServiceProvider
IServiceProvider 服务提供者, 由 IServiceCollection 的扩展方法 BuildServiceProvider 创建, 当需要它提供某个服务的时候, 它会根据创建它的 IServiceCollection 中的对应的 ServiceDescriptor 提供相应的服务实例. 它提供了 GetServiceGetRequiredServiceGetServicesGetRequiredServices 这样的几个用于提供服务实例的方法, 就像库管老张一样, 告诉他你需要什么服务的实例, 他会根据清单规定给你对应的工具
GetService 和 GetRequiredService 的区别:
维修工老李: "老张, 给我一架空客 A380." -- GetService<IA380>();
老张: "这个没有." -- return null;
维修工老李: "老张, 必须给我一架空客 A380!" -- GetRequiredService<IA380>();
老张: "这个真 TMD 没有." -- System.InvalidOperationException:No service for type 'IA380' has been registered.;
GetServices 和 GetRequiredServices 这两个加了 "s" 的方法返回对应的集合
IServiceScope
上文中的 ServiceDescriptor 的 Lifetime 属性为 Scoped 的时候, IServiceProvider 会为其创建一个新的区域 IServiceScope,
- public interface IServiceScope : IDisposable
- {
- IServiceProvider ServiceProvider { get; }
- }
从上面的代码可以看出它只是对 IServiceProvider 进行了一个简单的封装, 原始的 IServiceProvider 通过 CreateScope()创建了一个 IServiceScope, 而这个 IServiceScope 的 ServiceProvider 属性将负责这个区域内的服务提供, 而 Lifetime 为 Scoped 的 ServiceDescriptor 创建的实例在本区域内是以 "单例" 的形式存在的.
在 ASP.NET Core 中, Lifetime 为 Scoped 的实例在每次请求中只创建一次.
4. 使用方法及需要注意的问题
对于上面的维修工的例子, ASP.NET Core 的依赖注入还是有一些不一样的地方, 比如用卡车 (全单位只有一辆, 谁借都是这一辆) 来类比单例, 只有一个确实没问题, 但对于卡车, A 把它借走了, B 只有等他被还回来才能去借 同样标记为 Scoped 的傻瓜相机即使在小组内也是需要轮换使用的 没错, 就是并发问题, 对于 ASP.NET Core 的依赖注入提供的 Singleton 和 Scoped 的实例来说, 它是很有可能同时被多个地方获取并调用的通过下面的例子看一下这个问题, 顺便巩固一下上面的内容
- public interface ITest
- {
- Guid Guid { get; }
- string Name { get; set; }
- }
- public class Test : ITest
- {
- public Test()
- {
- this.Guid = Guid.NewGuid();
- }
- public Guid Guid { get; }
- public string Name { get; set; }
- }
一个 Test 类继承自 ITest, 为了方便比较是不是同一个实例, 在构造方法里对它的 Guid 属性赋一个新值, 然后将其注册一下
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc();
- services.AddTransient<ITest,Test>();
- }
现在通过三种方法来获取这个 Test, Controller 中如下
- public class HomeController : Controller
- {
- private ITest _test;
- public HomeController( ITest test)
- {
- this._test = test;
- }
- public IActionResult Index()
- {
- ViewBag.Test = this._test; // 构造方法获取
- ViewBag.TestFromContext = HttpContext.RequestServices.GetService<ITest>(); // 通过 HttpContext 获取
- return View();
- }
- }
View 中通过 @inject ITest viewITest 的方式获取, 然后把他们的 Guid 值显示出来:
- @inject ITest viewITest
- <ul>
- <li>@ViewBag.Test.Guid</li>
- <li>@ViewBag.TestFromContext.Guid</li>
- <li>@viewITest.Guid</li>
- </ul>
结果如下
- ad79690e-1ee2-41bd-82f1-062de4c124b2
- 92cd97fc-7083-4b10-99e4-13b6b6926c16
- cd0105f4-fa9d-4221-b395-af06798d96a2
说明三种方式获取了三个不同的实例, 刷新一下页面, 又变成了另外三个不同的值.
现在在 startup 文件中将原来的 services.AddTransient<ITest,Test>() 改为 services.AddSingleton<ITest,Test>() , 其他不变, 重新运行一下, 结果如下
- dd4c952e-b64c-4dc8-af01-2b9d667cf190
- dd4c952e-b64c-4dc8-af01-2b9d667cf190
- dd4c952e-b64c-4dc8-af01-2b9d667cf190
发现三组值是一样的, 说明获得的是同一个实例, 在刷新一下页面, 仍然是这三组值, 说明多次请求获得的结果也是同一个实例.
再将 services.AddSingleton<ITest,Test>() 改为 services.AddScoped<ITest,Test>() , 重新运行, 这次结果是
- ad5a600b-75fb-43c0-aee9-e90231fd510c
- ad5a600b-75fb-43c0-aee9-e90231fd510c
- ad5a600b-75fb-43c0-aee9-e90231fd510c
三组数字相同, 刷新一下, 又变成了另外三组一样的值, 这说明在同一次请求里, 获取的实例是同一个
因为无论在 Singleton 还是 Scoped 的情况下, 可能在应用的多个地方同时使用同一个实例, 所以在程序设置的时候就要注意了, 如果存在像在上面的 Test 有个 Name 属性提供了 { get; set; }的时候, 多个引用者处理它的值, 会造成一些不可预料的错误
5. 服务的 Dispose
对于每次请求, 我们最初配置的根 IServiceProvider 通过 CreateScope()创建了一个新的 IServiceScope, 而这个 IServiceScope 的 ServiceProvider 属性将负责本次该次请求的服务提供, 当请求结束, 这个 ServiceProvider 的 dispose 会被调用, 同时它负责由它创建的各个服务
在 1.0 版中, ServiceProvider 将对所有 IDisposable 对象调用 dispose, 包括那些并非由它创建的对象
而在 2.0 中, ServiceProvider 只调用由它创建的 IDisposable 类型的 Dispose 如果将一个实例添加到容器, 它将不会被释放
例如: services.AddSingleton<ITest>(new Test());
6. 我想换个容器
可以将默认的容器改为其他的容器, 比如
Autofac, 这需要将 ConfigureServices 方法由返回 void 改为 IServiceProvider
- public IServiceProvider ConfigureServices(IServiceCollection services)
- {
- services.AddMvc();
- // Add other framework services
- // Add Autofac
- var containerBuilder = new ContainerBuilder();
- containerBuilder.RegisterModule<DefaultModule>();
- containerBuilder.Populate(services);
- var container = containerBuilder.Build();
- return new AutofacServiceProvider(container);
- }
来源: https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_6.html