一、控制反转(IOC,Inversion of Control)和依赖注入的作用
1、控制系统Controller需要使用业务逻辑层(BLL)的功能
1>三层框架:表示层(Controller)、业务逻辑层BLL、数据访问层DAL
控制系统Controller是表示层的一个组件
2>方法1:通过new
关键字来创建一个BLL的实例
1》主动寻求服务:这个过程就像顾客自己去餐厅找服务员一样,是主动寻求服务的行为。
2》出现问题:高耦合度和低灵活性
如果BLL的实现发生变化(比如需要添加额外的依赖项或更改构造函数的参数),那么控制系统也需要相应地更新,这增加了代码的维护成本。
3>方法2【解决方法1出现问题】:采用控制反转原则,特别是依赖注入技术
1》在依赖注入的框架下,外部容器(比如Autofac等IOC容器)负责创建BLL的实例
BLL的实例不是由控制系统直接创建的,而是由外部容器(比如Autofac等IOC容器)负责创建和管理。
3》被动接收服务:这个过程就像服务员主动来到顾客的桌前提供服务一样,是被动接收服务的行为。
4》好处:低耦合度和高灵活性
1)控制系统Controller只需声明它对BLL的依赖关系,而不需要关心BLL的具体实现和创建过程。
2)当BLL的实现发生变化时,控制系统不需要进行任何修改。
因为IOC容器会自动处理这些变化,并将正确的BLL实例注入到控制系统中。
2、什么叫控制反转?【结合上述内容一句话说明】
本身是需要去new这个服务,去请求它。但是现在反过来了,我就坐着等你自动把服务给我注入进来。这个过程叫做控制反转。
二、依赖注入(DI,Dependency Injection)=使用服务:实现控制反转的一种方式
1、依赖注入:把 控制器Controller所依赖的服务Service 给 注入进IOC容器 里
1>什么是IOC容器?——servicecollection(服务收集器),一个存放服务的容器罐子而已。【IOC原生容器它只能够做简单的构造函数注入,其它注入见六、】
这里所谓的"服务"就是指在开发中需要使用的各种类的统称。
2、依赖注入要理清以下四点
1>谁依赖于谁:应用程序(controller)依赖于IOC容器
2>为什么需要依赖:应用程序需要IOC容器来提供对象需要的外部资源
3>谁注入谁:很明显是IOC容器注入应用程序某个对象(即应用程序所依赖的对象)
4>注入了什么:注入某个对象所需要的外部资源(包括对象、资源、常量数据)
3、如何进行依赖注入=如何使用服务?
1>如何使用服务——打开服务门,把服务放进去,关上服务门
服务注册一共有三个生命周期,一个是瞬时,一个是单例,一个是作用域。【详情见五??】
2>以瞬时服务周期AddTransient为例进行服务注册
1》新建Services文件夹,新建UserService服务
2》在Program.cs中进行一个服务的注册,即把服务放进IOC容器里
3》在Controller里写上构造函数,并调用UserService服务里面的方法
快捷键,选属性或字段都可
调用UserService服务里面的方法
4》运行代码
4、理解依赖注入原理,结合二、3、的代码
1>系统在创建Controller时,首先使用构造函数。
2>看这个构造函数里面有一个参数UserService,便到我的容器里面去找一找,发现我的容器里面有这个UserService这么一个注册的一个类。
3>既然有,那我就把这个类给创建一个 实例userService,去放到这个构造函数里面去。
4>最后,把拿到的 实例userService 传给 Controller里的属性UserService。
5、在Minimal API中注册和使用服务
三、依赖倒置=>依赖于抽象
1、依赖倒置原则:通过接口(Interface)或抽象类来定义模块之间的交互,而不是直接依赖于具体的实现。
1>高层模块不应该依赖于低层模块,二者都应该依赖于其抽象。
“高层模块”:调用或使用其他模块功能的模块。
“低层模块”:被调用或提供具体实现的模块。
2>抽象不应该依赖于细节,细节应该依赖于抽象。
2、使用接口 来实现依赖倒置原则(三层架构中):controller需要通过接口IBLL层,才能去调用BLL层(BLL, Business Logic Layer)
1>Controller(控制器)通常负责处理用户请求,并调用业务逻辑层(BLL)来执行具体的业务操作。
2>为了避免Controller直接依赖于BLL的具体实现,我们可以定义一个接口(IBLL层),让BLL的实现类实现这个接口,而Controller则依赖于这个接口
3、依赖倒置理解示例
1>假设有一个日志记录系统,其中有一个Logger
类负责记录日志信息。
2>定义一个ILogger
接口来描述日志记录的行为,并让Logger
类实现这个接口。
3>高层模块(如业务逻辑层)可以依赖于ILogger
接口,而不是直接依赖于Logger
类。
4>如果我们想更换日志记录的实现(例如,将日志信息写入数据库),我们只需要实现一个新的ILogger
接口,并将其注入到高层模块中即可,而不需要去改变Logger
类里的信息。
4、具体理解 服务类(UserService) 和 接口(lUserService) 的关系
1>lUserService 接口只定义了一组方法的签名(以及可能的属性、事件和索引器的签名),UserService 类必须提供 lUserService 接口中方法的具体实现。
注意:UserService 类没有继承 lUserService 接口(因为接口不是类),而是实现了它。
新建Services文件夹、IServices文件夹。
新建UserService服务,lUserService接口。
在UserService中“继承”lUserService。
2>总结下述实现方法1
1》没有用到在容器中取,所以不用进行如下图的服务注册,也不用在TestController里写上构造函数
2》具体调用时先用 lUserService接口定义一个变量 = 构造函数中定义的UserService变量实例化这个接口
注意:接口 IUserService 是不能直接实例化的,如下图会报错。
所以应该用UserService去实现它,同时UserService是“继承”于IUserService的。
3》然后调用 lUserService接口定义的变量.方法 进行访问
2>总结下述在实现方法2【推荐】
1》把IUserService放进IOC容器里,所以在Controller中构造函数是IUserService
2》具体调用时直接使用构造函数中定义的变量UserService.方法
5、实现方法1:lUserService 接口的变量 = new UserService()实例化
1>在TestController中调用IUserService接口里面的方法【类型是IUserService】
1》没有用到在容器中取,所以不用进行如下图的服务注册,也不用在TestController里写上构造函数
2》具体调用时先用 lUserService接口定义一个变量 = 构造函数中定义的UserService变量实例化这个接口
3》然后调用 lUserService接口定义的变量.方法 进行访问
区别上述二、3、2>3》中依赖注入的写法,
6、实现方法2:把IUserService放进IOC容器里 ,当作服务类型
1>AddTransient<服务的类型,该服务类型的具体实现类型>
AddTransient有两个参数,TService:一个服务的类型;TImplementation:该服务类型的具体实现类型。
4>在TestController里写上构造函数【注意实现类型是IUserService】,并调用IUserService服务里面的方法
由于是把IUserService放到了容器里,所以Controller中对应的构造函数的参数不能是UserService,应该是IUserService。这样才能在容器中找到对应的服务。
7、依赖注入和依赖倒置,两种可以同时都使用
四、依赖注入的三种服务生命周期+各自使用场景
1、服务生命周期+各自使用场景
1> Transient(临/瞬时生命周期)
-
特点:每次请求服务时,都会创建一个新的服务实例。这意味着即使在同一请求中多次请求同一服务,也会得到不同的服务实例。
-
适用场景:适用于无状态、轻量级的服务,如日志记录、验证服务等。这些服务不需要保持状态,因此每次都可以创建新的实例来处理请求。
2>Singleton(单例生命周期)
-
特点:整个应用程序生命周期内,只创建一个服务实例,并且该实例在应用程序的所有部分中共享。
-
适用场景:适用于全局状态管理、配置信息读取等场景。这些服务在整个应用程序中只需要一个实例来维护状态或提供数据。
3> Scoped(作用域生命周期)
-
特点:每次请求(或特定作用域)都会创建一个新的服务实例,并且在该请求(或作用域)中共享该实例。不同的请求(或作用域)会得到不同的服务实例。
-
适用场景:适用于需要在请求范围内共享状态的服务,如数据库上下文(DbContext)等。这些服务需要在请求开始时创建,并在请求结束时销毁。
-
与数据库连接的DbContext,默认是使用的Scoped,也建议使用Scoped。如果使用瞬时,会连接很多次数据库;如果使用单例,就会抢线程,因为我一次请求还没结束,我另一次请求如果还用这个上下文就会出错。【通过.NET5第8讲、第12讲理解】
2、具体理解分析1、中的结论
1>准备工作
1》创建三个服务,以及创建三个服务相对应的接口
把它拉进lService
同理,创建另外两个服务响应的接口。(此处省略)
2》再创建两个IOC服务,用于集成这三个生命周期的服务
3》在Program.cs里进行注册(还有其它注册方法,见四、)
4》在controller里进行使用
2>分析代码运行过程——每个生命周期是怎么调度的?
1》在所有的服务(包括IOC)里都打个断点。运行代码,再在swagger里进行Execute
2》先按照controller的构造函数里的 IIocService1里的顺序 执行各个构造函数
1)先进入单例Singletonservice的构造函数里,说明单例这个类型被构造被实例化了
2)然后是ScopedService被构造
3)然后是TransientService被构造
4)最后是IocService1被构造。说明在调用IocService1的时候,三个服务都被创建了一遍
3》再按照controller的构造函数里的 IIocService2里的顺序 执行各个构造函数
1)继续上述4)后面的下一步,就是TransientService。前面的SingletonService和scopedService都没再次运行
2)然后直接进入IocService2
3)并且我们之前创建的SingletonService和scopedService都还是有
4)说明SingletonService和scopedService只创建一次。
4》再次点击“Execute”,再请求一次:SingletonService没有进入,被跳过了
1》进入的是ScopedService。说明SingletonService没有进入,被跳过了。
3>总结
1》说明SingletonService,即单例的生命周期去注册的服务,只要一旦被创建以后,就不会再创建了。
2》ScopedService在一次请求中,第一次被创建,后面就不会去创建。但是第二次请求来的时候,又是一个新的线程,所以它又被创建了。
3》TransientService,瞬时的生命周期去注册的服务,只要用到都会被创建一次
所以,结合过程理解其使用场景
五、四、中的三种服务的生命周期,对应了两种服务注册的方式
1、一种注册方式是用上述的泛型注册方式。
2、另一种注册方式是用typeof
注册方式。(为什么要用typeof?有什么好处?见六、)
3、注意:上图中,Singleton多了最下面那种注册方式,为什么?
单例的情况下,只要我第一次使用时被创建,以后都不会创建,它会把我的这个实现的对象保存起来。我下次再用的时候不再需要new了,我直接把这个保存的对象扔给你就可以了
这样做意义何在?——给构造函数上面赋初值【单例特有的特性】
六、理解五、中为什么要有typeof?
1、从功能上理解:泛型的服务注册——使用typeof
1>泛型在Program.cs中如何注册?——使用typeof(...<>)
<>里面不能再套<>,如下图写法会报错
正确写法应该使用typeof(...<>)
2>在控制器里泛型<>中传入对象WeatherForecast
2、从框架上理解为什么要有typeof?——在开发时,如果上面有一个泛型,那么下面一定会有一个同名的方法接收Type类型的参数【成对出现】
1> typeof(T)
的结果是一个Type
对象,它代表了泛型类型参数T
在运行时的实际类型。
typeof
关键字用于在编译时获取一个类型的 Type
对象。
这个 Type
对象包含了关于该类型的元数据,比如它的名称、属性、方法、事件等。
通过 typeof
获取的 Type
对象,可以进一步使用反射来查询和操作该类型的实例或成员。
例如,typeof(int)
在编译时就被解析为 int
类型的 Type
对象,并且我们可以访问这个对象的 Name
属性来获取类型的名称。
2>反射的作用
在C#中,你可以使用反射(Reflection)来动态地检查和处理泛型类型。通过反射,你可以获取泛型类型的类型参数(T),然后根据这些类型参数的类型信息来决定执行什么操作。
3>泛型在Program.cs中注册
新建文件夹和文件
4>在开发时,需要传入一个泛型——typeof()+反射
开发者是不知道这个泛型是什么东西的。要对这个泛型进行操作,得先用到一个方法的反射(即typeof)。
通过反射这个泛型T,再通过一系列算法去判断这个泛型是什么类型的对象,做一个什么样的操作。【=知道这个泛型是什么东西】
1》
2》既然做到上面这步,那么可以直接搞一个方法,就接收Type类型的参数
3》可以看到typeof(T)的类型就是Type,所以,可以不用两个写重复的
4》所以,下图就是在开发时,如果上面有一个泛型,那么下面一定会有一个同名的方法接收Type类型的参数【成对出现】
七、增强容器Autofac——是什么?怎么把服务放进这个容器里?
1、上述的IOC原生容器:只能够做简单的构造函数注入
2、增强容器Autofac:构造函数注入+属性注入+方法注入【极少】+AOP
1>Autofac它还有一个功能叫做AOP面向切面编程
在WebAPI里面,其实AOP的使用场景比较少,因为它框架自带AOP功能。
WebAPI中起到AOP功能的是五大filter。
2>Autofac性能远低于IOC原生容器。但是它的属性注入是比较有优势的。
打开AOP,它默认是全局打开的。是说我全局里面任何的一个方法,任何一个类型在被调用之前,它都会在前面插入一些可用的是业务逻辑。这样很显然对我们的性能消耗是极大的。
3、怎么使用Autofac容器,怎么把服务放进这个容器里?
1>创建如下的UserService和IUserService
2>导入Autofac.Extensions.DependencyInjection
新建文件夹AutofacDemo
Autofac.Extensions.DependencyInjection依赖于Autofac,下载它就会自动下载Autofac
两种方法点击Restore
3>替换掉原生容器:UseServiceProviderFactory
1》.NET5 中写法如下:在<>这个方法里面去写上我们当前替代它这个原生容器的东西。
2》.NET6中写法如下:在UseServiceProviderFactory()括号里面写上我们当前替代它这个原生容器的东西。
1)下图的IServiceCollection是.NET自动的容器,现在需要把它替换掉
2)把这个工厂实例化好之后,把它丢到UseServiceProviderFactory里去。
此时我们的容器就被替换了。此时系统里运行的全是Autofac容器了。
4>把服务放进Autofac容器里,即对服务进行注册
1》Configurecontainer<ContainerBuilder>(builder => {...})
1)上图中,ContainerBuilder是Autofac.dll程序集下的一个密封类(不能被继承), ContainerBuilder是解开这把锁的钥匙。
2)有了ContainerBuilder后,里面有个参数叫builder,这个builder就是ContainerBuilder
在.NET5中,需要在startup.cs中新建一个方法,里面的参数类型是ContainerBuilder
2》通过Autofac容器对服务进行注册【区别上面的原生容器注入】
前面是实现类型注册的RegisterType<UserService> 。把这个类型用As给IUserService。
注意前面和后面是不一样的。正好跟原生容器反过来
4、 Autofac增强容器的泛型注入:加上typeof(...<>)就行了
5、Autofac容器增强中,不写默认是瞬态生命周期
单例和作用域生命周期使用如下:
6、理解:加个Autofac增强容器,只是把IOC原生容器倒进去了
如果把Autofac的容器注释掉,原生的容器依然可以使用,如下图。
因为原生的容器是一直存在的,加个Autofac的容器,只是把原生的容器倒进去了。
八、增强容器Autofac——如何做构造函数注入?如何做属性注入?
新建IocService服务和IIocService接口
1、做构造函数注入(结合上述七、3、)
2> 注意:做构造函数注入,如果想在IocService里访问userService,只能把userService方法放到IocService里
2、做属性注入:希望IocService里可以通过属性注入注入userService
1>PropertiesAutowired():开启属性注入功能。允许IocService这个类中使用属性注入
2>在IocService中可以通过属性注入的方式,注入之前注册过的服务
3>怎么使用userService中的方法?
1》把IocService里的属性先复制粘贴进IIocService接口
2》下面就可以通过IIocService接口去调用UserService中的方法(也是通过IUserService接口调用)
下图红框中的UserService,是上上图的IIocService接口中定义的名字
下图红框中的GetUserName,是上面左图IUserService接口中定义的名字
3、Autofac容器增强中,不写默认是瞬态生命周期
单例和作用域生命周期使用如下:
九、如果Autofac的所有的注册都放在Program.cs中,那么它太重了。解决方案
1、解决方案:用扩展方法进行方法的扩展和抽取
添加文件夹Common和文件AutofacExtensions.cs
为了在Program.cs中保持一致,在方法名前加个Add
在Program.cs中把方法写入
运行报错
2、泛型注入踩坑,解决上述1、的报错
为什么报错?
如果两个(TestService和ITestService)都是开放性泛型(<>里面没写东西),就认为它们两个不一样
加上AssignableTo看它两个是否不一样
AssignableTo:有一个定义在里面,如果TestService和ITestService两个都是开放性泛型(<>里面没写东西),就认为它们两个不一样
解决方法1:把RegisterType改为RegisterGeneric
解决方法2:在<>里面写上具体的类型