本文通过一个维修工与工具库的例子形象的描述一下为什么要用依赖注入、它的工作原理是什么样的, 然后根据这个类比一下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需要的服务,例如Authorization、RazorViewEngin、DataAnnotations等。
系统需要的添加好了, 剩下的就是我们把自己需要的用的添加进去了。 这里我们可以创建一个ServiceDescriptor然后把它添加到这个集合里, 系统①IServiceCollection也提供了AddSingleton、AddScoped和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是一个枚举, 上文说的AddSingleton、AddScoped和AddTransient就是对应这个枚举, 分别为:
Singleton: 单例, 例子中的卡车, 全单位只有一辆, 谁调用都是返回这个实例。
Scoped: 区域内单例, 例子中的傻瓜相机, 每小组一台, 小组内谁要都是同一台, 不同小组的相机不同。
Transient: 临时的 例子中的扳手和锤子, 谁要都给一把新的, 所有人的都不是同一把。
从这些属性的介绍来看, ServiceDescriptor规定了当有人需要ServiceType这个类型服务的时候, 提供给他一个ImplementationType类型的实例, 其他几个属性规定了提供的方法和生命周期.
③IServiceProvider
③IServiceProvider 服务提供者,由①IServiceCollection的扩展方法BuildServiceProvider创建, 当需要它提供某个服务的时候, 它会根据创建它的①IServiceCollection中的对应的②ServiceDescriptor提供相应的服务实例.。它提供了⑤GetService、GetRequiredService、GetServices、GetRequiredServices这样的几个用于提供服务实例的方法,就像库管老张一样, 告诉他你需要什么服务的实例, 他会根据清单规定给你对应的工具。
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获取 需要using Microsoft.Extensions.DependencyInjection
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--4b10-99e4-13b6b6926c16
cd0105f4-fa9d--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);
}
ASP.NET Core 2.0 : 六. 举个例子来聊聊它的依赖注入的更多相关文章
-
ASP.NET Core 中文文档 第三章 原理(10)依赖注入
原文:Dependency Injection 作者:Steve Smith 翻译:刘浩杨 校对:许登洋(Seay).高嵩 ASP.NET Core 的底层设计支持和使用依赖注入.ASP.NET Co ...
-
ASP.NET Core 2.0 : 系列目录
目录: ASP.NET Core 2.0 : 一. 概述 ASP.NET Core 2.0: 二. 开发环境 ASP.NET Core 2.0 : 三. 项目结构 ASP.NET Core 2.0 : ...
-
ASP.NET Core 1.0基础之依赖注入
来源https://docs.asp.net/en/latest/fundamentals/dependency-injection.html ASP.NET Core 1.0在设计上原生就支持和 ...
-
打造静态分析器(二)基于Asp.Net Core 3.0的AspectCore组件检测
上一篇,我们打造了一个简单的分析器,但是我们实际使用分析器就是为了对项目做分析检测,增加一些非语法的自检的 比如Asp.Net Core 3.0的替换依赖注入检测 设计分析 我们创建一个默认的Asp. ...
-
Asp.net core 2.0.1 Razor 的使用学习笔记(六)
Asp.net core 2.0.1 Razor 的使用学习笔记——基本页面的建立 VS这版(vs版本:15.5.6 .net版本:4.7.02558)的Razor页面自动生成就是坑爹货,它自动生成 ...
-
探索ASP.Net Core 3.0系列六:ASP.NET Core 3.0新特性启动信息中的结构化日志
前言:在本文中,我将聊聊在ASP.NET Core 3.0中细小的变化——启动时记录消息的方式进行小的更改. 现在,ASP.NET Core不再将消息直接记录到控制台,而是正确使用了logging 基 ...
-
ASP.NET Core 3.0 : 二十八. 在Docker中的部署以及docker-compose的使用
本文简要说一下ASP.NET Core 在Docker中部署以及docker-compose的使用 (ASP.NET Core 系列目录). 系统环境为CentOS 8 . 打个广告,求职中.. 一 ...
-
探索 ASP.Net Core 3.0系列三:ASP.Net Core 3.0中的Service provider validation
前言:在本文中,我将描述ASP.NET Core 3.0中新的“validate on build”功能. 这可以用来检测您的DI service provider是否配置错误. 具体而言,该功能可检 ...
-
初识ASP.NET Core 1.0
本文将对微软下一代ASP.NET框架做个概括性介绍,方便大家进一步熟悉该框架. 在介绍ASP.NET Core 1.0之前有必要澄清一些产品名称及版本号.ASP.NET Core1.0是微软下一代AS ...
随机推荐
-
Paxos算法细节详解(一)--通过现实世界描述算法
Paxos分析 最近研究paxos算法,看了许多相关的文章,概念还是很模糊,觉得还是没有掌握paxos算法的精髓,所以花了3天时间分析了libpaxos3的所有代码,此代码可以从https://bit ...
-
Cocos2d-x学习笔记(14)(更新函数scheduleUpdate、进度计时器CCProgressTo、滚动视图CCScrollView)
一.scheduleUpdate 1.scheduleUpdate:此函数是CCNode的函数,每一个CCNode仅仅要调用scheduleUpdate更新函数,那么这个CCNode就会响应当前类的u ...
-
Linux下TFTP的安装,配置和操作
注:转载他人,仅供自己研究学习使用 TFTP是用来下载远程文件的最简单网络协议,它其于UDP协议而实现.嵌入式linux的tftp开发环境包括两个方面:一是linux服务器端的tftp-server支 ...
-
Java递归方法遍历二叉树的代码
将内容过程中经常用的内容做个记录,如下内容内容是关于Java递归方法遍历二叉树的内容. package com.wzs; public class TestBinaryTree { public st ...
-
CentOS 7 常用命令
1.防火墙 yum install firewalld #安装firewalld 防火墙 systemctl start firewalld.service #开启防火墙 systemctl stop ...
-
django mysql数据库使用自己的User
由于我需要的User模型与django自带的User有所不同,所以需要定义自己的User Model,这里记录一下方法,适用于django 1.5+. 因为使用自己的后台,放弃django的管理后台, ...
-
记一次 Spring 事务配置踩坑记
记一次 Spring 事务配置踩坑记 问题描述:(SpringBoot + MyBatisPlus) 业务逻辑伪代码如下.理论上,插入数据 t1 后,xxService.getXxx() 方法的查询条 ...
-
MATLAB 制作GIF图像
前提要求:图像集保存在某个文件夹中,且每个图像以数字形式顺序命名,如001.jpg,002.jpg等. 代码1: 这个代码生成的效果有点问题,建议采用代码2. wm={'overwrite','app ...
-
bzoj4161: Shlw loves matrixI
Description 给定数列 {hn}前k项,其后每一项满足 hn = a1*h(n-1) + a2*h(n-2) + ... + ak*h(n-k) 其中 a1,a2...ak 为给定数列.请计 ...
-
Laravel 程序架构设计思路:使用动作类
当我们谈论到应用程序的架构的时候,经常会问到一个经典的问题,那就是"这段代码应该放在哪里比较好". 因为 Laravel 是一个相当灵活的框架,所以要回答这个问题其实没那么容易.我 ...