理解ASP.NET Core 依賴注入(Dependency Injection)
依賴注入
什么是依賴注入
簡(jiǎn)單說(shuō),就是將對(duì)象的創(chuàng)建和銷毀工作交給DI容器來(lái)進(jìn)行,調(diào)用方只需要接收注入的對(duì)象實(shí)例即可。
依賴注入有什么好處
依賴注入在.NET中,可謂是“一等公民”,處處都離不開它,那么它有什么好處呢?
假設(shè)有一個(gè)日志類 FileLogger,用于將日志記錄到本地文件。
public class FileLogger { public void LogInfo(string message) { } }
日志很常用,幾乎所有服務(wù)都需要記錄日志。如果不使用依賴注入,那么我們就必須在每個(gè)服務(wù)中手動(dòng) new FileLogger 來(lái)創(chuàng)建一個(gè) FileLogger 實(shí)例。
public class MyService { private readonly FileLogger _logger = new FileLogger(); public void Get() { _logger.LogInfo("MyService.Get"); } }
如果某一天,想要替換掉 FileLogger,而是使用 ElkLogger,通過(guò)ELK來(lái)處理日志,那么我們就需要將所有服務(wù)中的代碼都要改成 new ElkLogger。
public class MyService { private readonly ElkLogger _logger = new ElkLogger(); public void Get() { _logger.LogInfo("MyService.Get"); } }
- 在一個(gè)大型項(xiàng)目中,這樣的代碼分散在項(xiàng)目各處,涉及到的服務(wù)均需要進(jìn)行修改,顯然一個(gè)一個(gè)去修改不現(xiàn)實(shí),且違反了“開閉原則”。
- 如果Logger中還需要其他一些依賴項(xiàng),那么用到Logger的服務(wù)也要為其提供依賴,如果依賴項(xiàng)修改了,其他服務(wù)也必須要進(jìn)行更改,更加增大了維護(hù)難度。
- 很難進(jìn)行單元測(cè)試,因?yàn)樗鼰o(wú)法進(jìn)行 mock
正因如此,所以依賴注入解決了這些棘手的問(wèn)題:
- 通過(guò)接口或基類(包含抽象方法或虛方法等)將依賴關(guān)系進(jìn)行抽象化
- 將依賴關(guān)系存放到服務(wù)容器中
- 由框架負(fù)責(zé)創(chuàng)建和釋放依賴關(guān)系的實(shí)例,并將實(shí)例注入到構(gòu)造函數(shù)、屬性或方法中
ASP.NET Core內(nèi)置的依賴注入
服務(wù)生存周期
Transient
瞬時(shí),即每次獲取,都是一個(gè)全新的服務(wù)實(shí)例
Scoped
范圍(或稱為作用域),即在某個(gè)范圍(或作用域內(nèi))內(nèi),獲取的始終是同一個(gè)服務(wù)實(shí)例,而不同范圍(或作用域)間獲取的是不同的服務(wù)實(shí)例。對(duì)于Web應(yīng)用,每個(gè)請(qǐng)求為一個(gè)范圍(或作用域)。
Singleton
單例,即在單個(gè)應(yīng)用中,獲取的始終是同一個(gè)服務(wù)實(shí)例。另外,為了保證程序正常運(yùn)行,要求單例服務(wù)必須是線程安全的。
服務(wù)釋放
若服務(wù)實(shí)現(xiàn)了IDisposable
接口,并且該服務(wù)是由DI容器創(chuàng)建的,那么你不應(yīng)該去Dispose
,DI容器會(huì)對(duì)服務(wù)自動(dòng)進(jìn)行釋放。
如,有Service1、Service2、Service3、Service4四個(gè)服務(wù),并且都實(shí)現(xiàn)了IDisposable
接口,如:
public class Service1 : IDisposable { public void Dispose() { Console.WriteLine("Service1.Dispose"); } } public class Service2 : IDisposable { public void Dispose() { Console.WriteLine("Service2.Dispose"); } } public class Service3 : IDisposable { public void Dispose() { Console.WriteLine("Service3.Dispose"); } } public class Service4 : IDisposable { public void Dispose() { Console.WriteLine("Service4.Dispose"); } }
并注冊(cè)為:
public void ConfigureServices(IServiceCollection services) { // 每次使用完(請(qǐng)求結(jié)束時(shí))即釋放 services.AddTransient<Service1>(); // 超出范圍(請(qǐng)求結(jié)束時(shí))則釋放 services.AddScoped<Service2>(); // 程序停止時(shí)釋放 services.AddSingleton<Service3>(); // 程序停止時(shí)釋放 services.AddSingleton(sp => new Service4()); }
構(gòu)造函數(shù)注入一下
public ValuesController( Service1 service1, Service2 service2, Service3 service3, Service4 service4) { }
請(qǐng)求一下,獲取輸出:
Service2.Dispose
Service1.Dispose
這些服務(wù)實(shí)例都是由DI容器創(chuàng)建的,所以DI容器也會(huì)負(fù)責(zé)服務(wù)實(shí)例的釋放和銷毀。注意,單例此時(shí)還沒(méi)到釋放的時(shí)候。
但如果注冊(cè)為:
public void ConfigureServices(IServiceCollection services) { // 注意與上面的區(qū)別,這個(gè)是直接 new 的,而上面是通過(guò) sp => new 的 services.AddSingleton(new Service1()); services.AddSingleton(new Service2()); services.AddSingleton(new Service3()); services.AddSingleton(new Service4()); }
此時(shí),實(shí)例都是咱們自己創(chuàng)建的,DI容器就不會(huì)負(fù)責(zé)去釋放和銷毀了,這些工作都需要我們開發(fā)人員自己去做。
更多注冊(cè)方式,請(qǐng)參考官方文檔-Service registration methods
TryAdd{Lifetime}擴(kuò)展方法
當(dāng)你將同樣的服務(wù)注冊(cè)了多次時(shí),如:
services.AddSingleton<IMyService, MyService>(); services.AddSingleton<IMyService, MyService>();
那么當(dāng)使用IEnumerable<{Service}>
(下面會(huì)講到)解析服務(wù)時(shí),就會(huì)產(chǎn)生多個(gè)MyService
實(shí)例的副本。
為此,框架提供了TryAdd{Lifetime}
擴(kuò)展方法,位于命名空間Microsoft.Extensions.DependencyInjection.Extensions
下。當(dāng)DI容器中已存在指定類型的服務(wù)時(shí),則不進(jìn)行任何操作;反之,則將該服務(wù)注入到DI容器中。
services.AddTransient<IMyService, MyService1>(); // 由于上面已經(jīng)注冊(cè)了服務(wù)類型 IMyService,所以下面的代碼不不會(huì)執(zhí)行任何操作(與生命周期無(wú)關(guān)) services.TryAddTransient<IMyService, MyService1>(); services.TryAddTransient<IMyService, MyService2>();
- TryAdd:通過(guò)參數(shù)
ServiceDescriptor
將服務(wù)類型、實(shí)現(xiàn)類型、生命周期等信息傳入進(jìn)去 - TryAddTransient:對(duì)應(yīng)AddTransient
- TryAddScoped:對(duì)應(yīng)AddScoped
- TryAddSingleton:對(duì)應(yīng)AddSingleton
- TryAddEnumerable:這個(gè)和
TryAdd
的區(qū)別是,TryAdd
僅根據(jù)服務(wù)類型來(lái)判斷是否要進(jìn)行注冊(cè),而TryAddEnumerable
則是根據(jù)服務(wù)類型和實(shí)現(xiàn)類型一同進(jìn)行判斷是否要進(jìn)行注冊(cè),常常用于注冊(cè)同一服務(wù)類型的多個(gè)不同實(shí)現(xiàn)。舉個(gè)例子吧:
// 注冊(cè)了 IMyService - MyService1 services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>()); // 注冊(cè)了 IMyService - MyService2 services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService2>()); // 未進(jìn)行任何操作,因?yàn)?IMyService - MyService1 在上面已經(jīng)注冊(cè)了 services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());
解析同一服務(wù)的多個(gè)不同實(shí)現(xiàn)
默認(rèn)情況下,如果注入了同一個(gè)服務(wù)的多個(gè)不同實(shí)現(xiàn),那么當(dāng)進(jìn)行服務(wù)解析時(shí),會(huì)以最后一個(gè)注入的為準(zhǔn)。
如果想要解析出同一服務(wù)類型的所有服務(wù)實(shí)例,那么可以通過(guò)IEnumerable<{Service}>
來(lái)解析(順序同注冊(cè)順序一致):
public interface IAnimalService { } public class DogService : IAnimalService { } public class PigService : IAnimalService { } public class CatService : IAnimalService { } public void ConfigureServices(IServiceCollection services) { // 生命周期沒(méi)有限制 services.AddTransient<IAnimalService, DogService>(); services.AddScoped<IAnimalService, PigService>(); services.AddSingleton<IAnimalService, CatService>(); } public ValuesController( // CatService IAnimalService animalService, // DogService、PigService、CatService IEnumerable<IAnimalService> animalServices) { }
Replace && Remove 擴(kuò)展方法
上面我們所提到的,都是注冊(cè)新的服務(wù)到DI容器中,但是有時(shí)我們想要替換或是移除某些服務(wù),這時(shí)就需要使用Replace
和Remove
了
// 將 IMyService 的實(shí)現(xiàn)替換為 MyService1 services.Replace(ServiceDescriptor.Singleton<IMyService, MyService>()); // 移除 IMyService 注冊(cè)的實(shí)現(xiàn) MyService services.Remove(ServiceDescriptor.Singleton<IMyService, MyService>()); // 移除 IMyService 的所有注冊(cè) services.RemoveAll<IMyService>(); // 清除所有服務(wù)注冊(cè) services.Clear();
Autofac
Autofac 是一個(gè)老牌DI組件了,接下來(lái)我們使用Autofac替換ASP.NET Core自帶的DI容器。
1.安裝nuget包:
Install-Package Autofac Install-Package Autofac.Extensions.DependencyInjection
2.替換服務(wù)提供器工廠
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) // 通過(guò)此處將默認(rèn)服務(wù)提供器工廠替換為 autofac .UseServiceProviderFactory(new AutofacServiceProviderFactory());
3.在 Startup 類中添加 ConfigureContainer 方法
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public ILifetimeScope AutofacContainer { get; private set; } public void ConfigureServices(IServiceCollection services) { // 1. 不要 build 或返回任何 IServiceProvider,否則會(huì)導(dǎo)致 ConfigureContainer 方法不被調(diào)用。 // 2. 不要?jiǎng)?chuàng)建 ContainerBuilder,也不要調(diào)用 builder.Populate(),AutofacServiceProviderFactory 已經(jīng)做了這些工作了 // 3. 你仍然可以在此處通過(guò)微軟默認(rèn)的方式進(jìn)行服務(wù)注冊(cè) services.AddOptions(); services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication.Ex", Version = "v1" }); }); } // 1. ConfigureContainer 用于使用 Autofac 進(jìn)行服務(wù)注冊(cè) // 2. 該方法在 ConfigureServices 之后運(yùn)行,所以這里的注冊(cè)會(huì)覆蓋之前的注冊(cè) // 3. 不要 build 容器,不要調(diào)用 builder.Populate(),AutofacServiceProviderFactory 已經(jīng)做了這些工作了 public void ConfigureContainer(ContainerBuilder builder) { // 將服務(wù)注冊(cè)劃分為模塊,進(jìn)行注冊(cè) builder.RegisterModule(new AutofacModule()); } public class AutofacModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { // 在此處進(jìn)行服務(wù)注冊(cè) builder.RegisterType<UserService>().As<IUserService>(); } } public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { // 通過(guò)此方法獲取 autofac 的 DI容器 AutofacContainer = app.ApplicationServices.GetAutofacRoot(); } }
服務(wù)解析和注入
上面我們主要講了服務(wù)的注入方式,接下來(lái)看看服務(wù)的解析方式。解析方式有兩種:
- 用于創(chuàng)建未在DI容器中注冊(cè)的服務(wù)實(shí)例
- 用于某些框架級(jí)別的功能
構(gòu)造函數(shù)注入
上面我們舉得很多例子都是使用了構(gòu)造函數(shù)注入——通過(guò)構(gòu)造函數(shù)接收參數(shù)。構(gòu)造函數(shù)注入是非常常見(jiàn)的服務(wù)注入方式,也是首選方式,這要求:
- 構(gòu)造函數(shù)可以接收非依賴注入的參數(shù),但必須提供默認(rèn)值
- 當(dāng)服務(wù)通過(guò)
IServiceProvider
解析時(shí),要求構(gòu)造函數(shù)必須是public - 當(dāng)服務(wù)通過(guò)
ActivatorUtilities
解析時(shí),要求構(gòu)造函數(shù)必須是public,雖然支持構(gòu)造函數(shù)重載,但必須只能有一個(gè)是有效的,即參數(shù)能夠全部通過(guò)依賴注入得到值
方法注入
顧名思義,方法注入就是通過(guò)方法參數(shù)來(lái)接收服務(wù)實(shí)例。
[HttpGet] public string Get([FromServices]IMyService myService) { return "Ok"; }
屬性注入
ASP.NET Core內(nèi)置的依賴注入是不支持屬性注入的。但是Autofac支持,用法如下:
老規(guī)矩,先定義服務(wù)和實(shí)現(xiàn)
public interface IUserService { string Get(); } public class UserService : IUserService { public string Get() { return "User"; } }
然后注冊(cè)服務(wù)
- 默認(rèn)情況下,控制器的構(gòu)造函數(shù)參數(shù)由DI容器來(lái)管理嗎,而控制器實(shí)例本身卻是由ASP.NET Core框架來(lái)管理,所以這樣“屬性注入”是無(wú)法生效的
- 通過(guò)
AddControllersAsServices
方法,將控制器交給 autofac 容器來(lái)處理,這樣就可以使“屬性注入”生效了
public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddControllersAsServices(); } public void ConfigureContainer(ContainerBuilder builder) { builder.RegisterModule<AutofacModule>(); } public class AutofacModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<UserService>().As<IUserService>(); var controllerTypes = Assembly.GetExecutingAssembly().GetExportedTypes() .Where(type => typeof(ControllerBase).IsAssignableFrom(type)) .ToArray(); // 配置所有控制器均支持屬性注入 builder.RegisterTypes(controllerTypes).PropertiesAutowired(); } }
最后,我們?cè)诳刂破髦型ㄟ^(guò)屬性來(lái)接收服務(wù)實(shí)例
public class ValuesController : ControllerBase { public IUserService UserService { get; set; } [HttpGet] public string Get() { return UserService.Get(); } }
通過(guò)調(diào)用Get
接口,我們就可以得到IUserService
的實(shí)例,從而得到響應(yīng)
User
一些注意事項(xiàng)
- 避免使用服務(wù)定位模式。盡量避免使用
GetService
來(lái)獲取服務(wù)實(shí)例,而應(yīng)該使用DI。
using Microsoft.Extensions.DependencyInjection; public class ValuesController : ControllerBase { private readonly IServiceProvider _serviceProvider; // 應(yīng)通過(guò)依賴注入的方式獲取服務(wù)實(shí)例 public ValuesController(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } [HttpGet] public string Get() { // 盡量避免通過(guò) GetService 方法獲取服務(wù)實(shí)例 var myService = _serviceProvider.GetService<IMyService>(); return "Ok"; } }
- 避免在
ConfigureServices
中調(diào)用BuildServiceProvider
。因?yàn)檫@會(huì)導(dǎo)致創(chuàng)建第二個(gè)DI容器的副本,從而導(dǎo)致注冊(cè)的單例服務(wù)出現(xiàn)多個(gè)副本。
public void ConfigureServices(IServiceCollection services) { // 不要在該方法中調(diào)用該方法 var serviceProvider = services.BuildServiceProvider(); }
- 一定要注意服務(wù)解析范圍,不要在 Singleton 中解析 Transient 或 Scoped 服務(wù),這可能導(dǎo)致服務(wù)狀態(tài)錯(cuò)誤(如導(dǎo)致服務(wù)實(shí)例生命周期提升為單例)。允許的方式有:
1.在 Scoped 或 Transient 服務(wù)中解析 Singleton 服務(wù)
2.在 Scoped 或 Transient 服務(wù)中解析 Scoped 服務(wù)(不能和前面的Scoped服務(wù)相同)
- 當(dāng)在
Development
環(huán)境中運(yùn)行、并通過(guò)CreateDefaultBuilder
生成主機(jī)時(shí),默認(rèn)的服務(wù)提供程序會(huì)進(jìn)行如下檢查:
1.不能在根服務(wù)提供程序解析 Scoped 服務(wù),這會(huì)導(dǎo)致 Scoped 服務(wù)的生命周期提升為 Singleton,因?yàn)楦萜髟趹?yīng)用關(guān)閉時(shí)才會(huì)釋放。
2.不能將 Scoped 服務(wù)注入到 Singleton 服務(wù)中
- 隨著業(yè)務(wù)增長(zhǎng),需要依賴注入的服務(wù)也越來(lái)越多,建議使用擴(kuò)展方法,封裝服務(wù)注入,命名為
Add{Group_Name}
,如將所有 AppService 的服務(wù)注冊(cè)封裝起來(lái)
namespace Microsoft.Extensions.DependencyInjection { public static class ApplicationServiceCollectionExtensions { public static IServiceCollection AddApplicationService(this IServiceCollection services) { services.AddTransient<Service1>(); services.AddScoped<Service2>(); services.AddSingleton<Service3>(); services.AddSingleton(sp => new Service4()); return services; } } }
然后在ConfigureServices
中調(diào)用即可
public void ConfigureServices(IServiceCollection services) { services.AddApplicationService(); }
框架默認(rèn)提供的服務(wù)
以下列出一些常用的框架已經(jīng)默認(rèn)注冊(cè)的服務(wù):
服務(wù)類型 | 生命周期 |
---|---|
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Transient |
IHostApplicationLifetime | Singleton |
IHostLifetime | Singleton |
IWebHostEnvironment | Singleton |
IHostEnvironment | Singleton |
Microsoft.AspNetCore.Hosting.IStartup | Singleton |
Microsoft.AspNetCore.Hosting.IStartupFilter | Transient |
Microsoft.AspNetCore.Hosting.Server.IServer | Singleton |
Microsoft.AspNetCore.Http.IHttpContextFactory | Transient |
Microsoft.Extensions.Logging.ILogger |
Singleton |
Microsoft.Extensions.Logging.ILoggerFactory | Singleton |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Singleton |
Microsoft.Extensions.Options.IConfigureOptions |
Transient |
Microsoft.Extensions.Options.IOptions |
Singleton |
System.Diagnostics.DiagnosticSource | Singleton |
System.Diagnostics.DiagnosticListener | Singleton |
到此這篇關(guān)于理解ASP.NET Core 依賴注入(Dependency Injection)的文章就介紹到這了,更多相關(guān)ASP.NET Core 依賴注入內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
.net 單點(diǎn)登錄的設(shè)計(jì)與實(shí)踐
本篇文章主要介紹了解析.net 單點(diǎn)登錄實(shí)踐,具有一定的參考價(jià)值,有需要的可以了解一下。2016-11-11使用FreeHost SQL2000網(wǎng)頁(yè)管理器出錯(cuò)解決辦法
在您登陸FreeHost SQL2000網(wǎng)頁(yè)管理器時(shí),如果提示以下信息: 發(fā)生類型為 System.Web.HttpUnhandledException 的異常2012-01-01使用DataTable更新數(shù)據(jù)庫(kù)(增,刪,改)
使用DataTable更新數(shù)據(jù)庫(kù)(增,刪,改),需要的朋友可以參考一下2013-03-03ASP.NET?Core基于現(xiàn)有數(shù)據(jù)庫(kù)創(chuàng)建EF模型
這篇文章介紹了ASP.NET?Core基于現(xiàn)有數(shù)據(jù)庫(kù)創(chuàng)建EF模型的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04.NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)數(shù)據(jù)塑形
在查詢的場(chǎng)景中,還有一類需求不是很常見(jiàn),就是在前端請(qǐng)求中指定返回的字段。所以這篇文章主要介紹了.NET 6如何實(shí)現(xiàn)數(shù)據(jù)塑形,需要的可以參考一下2022-01-01asp.net下用服務(wù)器端代碼解決瀏覽器兼容性問(wèn)題
在你不厭其煩的搞瀏覽器兼容性問(wèn)題的時(shí)候不妨試下這個(gè)方法 任何一種網(wǎng)頁(yè)編程語(yǔ)言都能實(shí)現(xiàn),基于獲取用戶請(qǐng)求信息的判斷瀏覽器類型2010-01-01Asp.net防重復(fù)提交機(jī)制實(shí)現(xiàn)方法
在Button或其他控件加上下面兩個(gè)屬性:UseSubmitBehavior="false"及OnClientClick設(shè)置控件為不可用即可,感興趣的朋友可以參考下哈2013-04-04