简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可。
微软官方文档-DI
依赖注入在.NET中,可谓是“一等公民”,处处都离不开它,那么它有什么好处呢?
假设有一个日志类FileLogger,用于将日志记录到本地文件。
typescript
publicclassFileLogger{publicvoidLogInfo(stringmessage){}}
日志很常用,几乎所有服务都需要记录日志。如果不使用依赖注入,那么我们就必须在每个服务中手动newFileLogger来创建一个FileLogger实例。
typescript
publicclassMyService{privatereadonlyFileLogger_logger=newFileLogger();publicvoidGet(){_("");}}
如果某一天,想要替换掉FileLogger,而是使用ElkLogger,通过ELK来处理日志,那么我们就需要将所有服务中的代码都要改成newElkLogger。
typescript
publicclassMyService{privatereadonlyElkLogger_logger=newElkLogger();publicvoidGet(){_("");}}
在一个大型项目中,这样的代码分散在项目各处,涉及到的服务均需要进行修改,显然一个一个去修改不现实,且违反了“开闭原则”。
如果Logger中还需要其他一些依赖项,那么用到Logger的服务也要为其提供依赖,如果依赖项修改了,其他服务也必须要进行更改,更加增大了维护难度。
很难进行单元测试,因为它无法进行mock
正因如此,所以依赖注入解决了这些棘手的问题:
通过接口或基类(包含抽象方法或虚方法等)将依赖关系进行抽象化
将依赖关系存放到服务容器中
由框架负责创建和释放依赖关系的实例,并将实例注入到构造函数、属性或方法中
Transient
瞬时,即每次获取,都是一个全新的服务实例
Scoped
范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域)。
Singleton
单例,即在单个应用中,获取的始终是同一个服务实例。另外,为了保证程序正常运行,要求单例服务必须是线程安全的。
若服务实现了IDisposable接口,并且该服务是由DI容器创建的,那么你不应该去Dispose,DI容器会对服务自动进行释放。
如,有Service1、Service2、Service3、Service4四个服务,并且都实现了IDisposable接口,如:
arduino
publicclassService1:IDisposable{publicvoidDispose(){("");}}publicclassService2:IDisposable{publicvoidDispose(){("");}}publicclassService3:IDisposable{publicvoidDispose(){("");}}publicclassService4:IDisposable{publicvoidDispose(){("");}}
并注册为:
typescript
publicvoidConfigureServices(IServiceCollectionservices){//每次使用完(请求结束时)即释放();//超出范围(请求结束时)则释放();//程序停止时释放();//程序停止时释放(sp=newService4());}
构造函数注入一下
typescript
publicValuesController(Service1service1,Service2service2,Service3service3,Service4service4){}
请求一下,获取输出:
mipsasm
这些服务实例都是由DI容器创建的,所以DI容器也会负责服务实例的释放和销毁。注意,单例此时还没到释放的时候。
但如果注册为:
reasonml
publicvoidConfigureServices(IServiceCollectionservices){//注意与上面的区别,这个是直接new的,而上面是通过sp=new的(newService1());(newService2());(newService3());(newService4());}
此时,实例都是咱们自己创建的,DI容器就不会负责去释放和销毁了,这些工作都需要我们开发人员自己去做。
更多注册方式,请参考官方文档-Serviceregistrationmethods
当你将同样的服务注册了多次时,如:
bnf
,MyService();,MyService();
那么当使用IEnumerable{Service}(下面会讲到)解析服务时,就会产生多个MyService实例的副本。
为此,框架提供了TryAdd{Lifetime}扩展方法,位于命名空间下。当DI容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到DI容器中。
dts
,MyService1();//由于上面已经注册了服务类型IMyService,所以下面的代码不不会执行任何操作(与生命周期无关),MyService1();,MyService2();
TryAdd:通过参数ServiceDescriptor将服务类型、实现类型、生命周期等信息传入进去
TryAddTransient:对应AddTransient
TryAddScoped:对应AddScoped
TryAddSingleton:对应AddSingleton
TryAddEnumerable:这个和TryAdd的区别是,TryAdd仅根据服务类型来判断是否要进行注册,而TryAddEnumerable则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。举个例子吧:
awk
//注册了(,MyService1());//注册了(,MyService2());//未进行任何操作,因为IMyService-MyService1在上面已经注册了(,MyService1());解析同一服务的多个不同实现
默认情况下,如果注入了同一个服务的多个不同实现,那么当进行服务解析时,会以最后一个注入的为准。
如果想要解析出同一服务类型的所有服务实例,那么可以通过IEnumerable{Service}来解析(顺序同注册顺序一致):
angelscript
publicinterfaceIAnimalService{}publicclassDogService:IAnimalService{}publicclassPigService:IAnimalService{}publicclassCatService:IAnimalService{}publicvoidConfigureServices(IServiceCollectionservices){//生命周期没有限制,DogService();,PigService();,CatService();}publicValuesController(//CatServiceIAnimalServiceanimalService,//DogService、PigService、CatServiceIEnumerableIAnimalServiceanimalServices){}
ReplaceRemove扩展方法上面我们所提到的,都是注册新的服务到DI容器中,但是有时我们想要替换或是移除某些服务,这时就需要使用Replace和Remove了
awk
//将IMyService的实现替换为(,MyService());//移除IMyService注册的实现(,MyService());//移除IMyService的所有注册();//清除所有服务注册();Autofac
Autofac是一个老牌DI组件了,接下来我们使用Autofac替换自带的DI容器。
安装nuget包:
ada
替换服务提供器工厂
zephir
publicstaticIHostBuilderCreateHostBuilder(string[]args)=(args).ConfigureWebHostDefaults(webBuilder={();})//通过此处将默认服务提供器工厂替换为(newAutofacServiceProviderFactory());
在Startup类中添加ConfigureContainer方法
arduino
publicclassStartup{publicStartup(IConfigurationconfiguration){Configuration=configuration;}publicIConfigurationConfiguration{get;}publicILifetimeScopeAutofacContainer{get;privateset;}publicvoidConfigureServices(IServiceCollectionservices){//1.不要build或返回任何IServiceProvider,否则会导致ConfigureContainer方法不被调用。//2.不要创建ContainerBuilder,也不要调用(),AutofacServiceProviderFactory已经做了这些工作了//3.你仍然可以在此处通过微软默认的方式进行服务注册();();(c={("v1",newOpenApiInfo{Title="",Version="v1"});});}//1.ConfigureContainer用于使用Autofac进行服务注册//2.该方法在ConfigureServices之后运行,所以这里的注册会覆盖之前的注册//3.不要build容器,不要调用(),AutofacServiceProviderFactory已经做了这些工作了publicvoidConfigureContainer(ContainerBuilderbuilder){//将服务注册划分为模块,进行注册(newAutofacModule());}publicclassAutofacModule:{protectedoverridevoidLoad(ContainerBuilderbuilder){//在此处进行服务注册().AsIUserService();}}publicvoidConfigure(IApplicationBuilderapp,ILoggerFactoryloggerFactory){//通过此方法获取autofac的DI容器AutofacContainer=();}}
服务解析和注入上面我们主要讲了服务的注入方式,接下来看看服务的解析方式。解析方式有两种:
IServiceProvider
ActivatorUtilities
用于创建未在DI容器中注册的服务实例
用于某些框架级别的功能
上面我们举得很多例子都是使用了构造函数注入——通过构造函数接收参数。构造函数注入是非常常见的服务注入方式,也是首选方式,这要求:
构造函数可以接收非依赖注入的参数,但必须提供默认值
当服务通过IServiceProvider解析时,要求构造函数必须是public
当服务通过ActivatorUtilities解析时,要求构造函数必须是public,虽然支持构造函数重载,但必须只能有一个是有效的,即参数能够全部通过依赖注入得到值
顾名思义,方法注入就是通过方法参数来接收服务实例。
csharp
[HttpGet]publicstringGet([FromServices]IMyServicemyService){return"Ok";}
属性注入内置的依赖注入是不支持属性注入的。但是Autofac支持,用法如下:
老规矩,先定义服务和实现
angelscript
publicinterfaceIUserService{stringGet();}publicclassUserService:IUserService{publicstringGet(){return"User";}}
然后注册服务
默认情况下,控制器的构造函数参数由DI容器来管理吗,而控制器实例本身却是由框架来管理,所以这样“属性注入”是无法生效的
通过AddControllersAsServices方法,将控制器交给autofac容器来处理,这样就可以使“属性注入”生效了
reasonml
publicvoidConfigureServices(IServiceCollectionservices){().AddControllersAsServices();}publicvoidConfigureContainer(ContainerBuilderbuilder){();}publicclassAutofacModule:{protectedoverridevoidLoad(ContainerBuilderbuilder){().AsIUserService();varcontrollerTypes=().GetExportedTypes().Where(type=typeof(ControllerBase).IsAssignableFrom(type)).ToArray();//配置所有控制器均支持属性注入(controllerTypes).PropertiesAutowired();}}
最后,我们在控制器中通过属性来接收服务实例
csharp
publicclassValuesController:ControllerBase{publicIUserServiceUserService{get;set;}[HttpGet]publicstringGet(){();}}
通过调用Get接口,我们就可以得到IUserService的实例,从而得到响应
crmsh
User一些注意事项
避免使用服务定位模式。尽量避免使用GetService来获取服务实例,而应该使用DI。
csharp
;publicclassValuesController:ControllerBase{privatereadonlyIServiceProvider_serviceProvider;//应通过依赖注入的方式获取服务实例publicValuesController(IServiceProviderserviceProvider){_serviceProvider=serviceProvider;}[HttpGet]publicstringGet(){//尽量避免通过GetService方法获取服务实例varmyService=_();return"Ok";}}
避免在ConfigureServices中调用BuildServiceProvider。因为这会导致创建第二个DI容器的副本,从而导致注册的单例服务出现多个副本。
typescript
publicvoidConfigureServices(IServiceCollectionservices){//不要在该方法中调用该方法varserviceProvider=();}
一定要注意服务解析范围,不要在Singleton中解析Transient或Scoped服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例)。允许的方式有:在Scoped或Transient服务中解析Singleton服务在Scoped或Transient服务中解析Scoped服务(不能和前面的Scoped服务相同)
当在Development环境中运行、并通过CreateDefaultBuilder生成主机时,默认的服务提供程序会进行如下检查:不能在根服务提供程序解析Scoped服务,这会导致Scoped服务的生命周期提升为Singleton,因为根容器在应用关闭时才会释放。不能将Scoped服务注入到Singleton服务中
随着业务增长,需要依赖注入的服务也越来越多,建议使用扩展方法,封装服务注入,命名为Add{Group_Name},如将所有AppService的服务注册封装起来
typescript
{publicstaticclassApplicationServiceCollectionExtensions{publicstaticIServiceCollectionAddApplicationService(thisIServiceCollectionservices){();();();(sp=newService4());returnservices;}}}
然后在ConfigureServices中调用即可
typescript
publicvoidConfigureServices(IServiceCollectionservices){();}
框架默认提供的服务以下列出一些常用的框架已经默认注册的服务:
服务类型
生命周期
Transient
IHostApplicationLifetime
Singleton
IHostLifetime
Singleton
IWebHostEnvironment
Singleton
IHostEnvironment
Singleton
Singleton
Transient
Singleton
Transient
Singleton
Singleton
Singleton
Transient
Singleton
Singleton
Singleton
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。