本文經過一個維修工與工具庫的例子形象的描述一下爲何要用依賴注入、它的工做原理是什麼樣的, 而後根據這個類比一下ASP.NET Core 中的依賴注入, 從而深入瞭解它的使用方法、注意事項以及回收機制等.html
本文主要內容:github
1.爲何要用依賴注入(DI)併發
2.容器的構建和規則ide
3.ASP.NET Core 2.0中的依賴注入工具
4.使用方法及須要注意的問題ui
5.服務的Disposethis
6.我想換個容器spa
什麼是依賴注入就不說了, 爲何要使用呢?設計
軟件設計原則中有一個依賴倒置原則(DIP)講的是要依賴於抽象,不要依賴於具體,高層模塊不該該依賴於低層模塊, 兩者應該依賴於抽象。簡單的說就是爲了更好的解耦。而控制反轉(Ioc)就是這樣的原則的其中一個實現思路, 這個思路的其中一種實現方式就是依賴注入(DI)。
感受有點繞, 舉個栗子:老李是一個維修工, 如今要出任務去維修, 得先去申領個扳手。
圖一
老李: "請給我一把能夠能夠擰7mm大小的六角螺絲的扳手.", 而後庫管老張就從倉庫裏拿了一把這樣的大力牌扳手給老李。
在這個例子中, 維修工老李只要告訴庫管我要一個 "能夠擰7mm大小的六角螺絲"的扳手便可, 他不用關心扳手的品牌和樣式, 也不用採購扳手,更不用關心這個扳手是怎麼來的.而對於庫管, 他只需提供知足這樣規則的一個扳手便可, 不用去關心老李拿着這個扳手以後去幹什麼。因此老李和老張都只是關心"能夠擰7mm大小的六角螺絲的"這個規則便可, 也就是說, 若是後期倉庫裏再也不提供大力牌扳手, 而是提供了這樣的大牛牌扳手, 不管換了什麼牌子和樣式, 只要仍知足這個規則, 老李仍然能夠正常工做.它們定義了一個規則(好比接口IWrench7mm), 兩者都依賴於這個規則, 而後倉庫不管提供大力牌(WrenchDaLi : IWrench7mm)仍是大牛牌(WrenchDaNiu : IWrench7mm), 都不影響正常工做.
這就是依賴倒置原則(DIP), 不依賴於具體(牌子), 高層模塊(老李)不該該依賴於低層模塊(大力牌扳手), 兩者應該依賴於抽象(IWrench7mm:能夠擰7mm大小的六角螺絲)。若是直接由老李去獲取(new)大力牌扳手, 那麼當業務改變要求採用大牛牌的時候, 咱們就要去修改老李的代碼.爲了解耦, 在本例中咱們只要在配置中讓倉庫由原來的提供大力牌改成提供大牛牌便可。老李要使用的時候, 能夠經過注入(構造器、屬性、方法)的方式, 將倉庫提供的扳手實例提供給老李使用。
繼續上面的例子, 庫管老張爲何會提供給老李大力牌而不是大牛牌的扳手呢? 那是由於領導給了他一份構建倉庫的物品購置及發放清單:
A. 當有人要7mm的六角扳手的時候,給他一個大力牌的扳手, 當再有人來要的時候就再給另外一把。
B. 但對於相機, 每一個小組只能給一臺, 小組內全部人共用這一臺。
C. 卡車更是全單位只有一輛, 誰申請都是同一輛。
圖二
首先看一下下面的圖三
圖三
這就是ASP.NET Core 中默認的依賴注入方式, 對比一下圖二是否是很像?
上篇文章說要將Startup放大介紹一下, 那麼打開Startup這個文件, 看一下里面的ConfigureServices方法。顧名思義, 這個方法是用來配置服務,
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); }
此方法接收一個IServiceCollection類型的參數, 查看它的定義, 被定義在Microsoft.Extensions.DependencyInjection這個NuGet包中, 功能就是依賴注入, 在ASP.NET Core中被普遍使用.
它正是圖三中的①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() 這樣封裝到一個擴展方法中去。
如今來看一下清單中的內容。
既然①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 服務提供者,由①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"的方法返回對應的集合。
上文中的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的實例在每次請求中只建立一次.
對於上面的維修工的例子, 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-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; }的時候,多個引用者處理它的值, 會形成一些不可預料的錯誤。
對於每次請求, 咱們最初配置的根IServiceProvider經過CreateScope()建立了一個新的IServiceScope, 而這個IServiceScope的ServiceProvider屬性將負責本次該次請求的服務提供, 當請求結束, 這個ServiceProvider的dispose會被調用, 同時它負責由它建立的各個服務。
在 1.0 版中,ServiceProvider將對全部 IDisposable
對象調用 dispose,包括那些並不是由它建立的對象。
而在2.0中, ServiceProvider只調用由它建立的 IDisposable
類型的 Dispose
。 若是將一個實例添加到容器,它將不會被釋放。
例如: services.AddSingleton<ITest>(new Test());
能夠將默認的容器改成其餘的容器, 好比Autofac,這須要將ConfigureServices方法由返回void改成IServiceProvider。
1 public IServiceProvider ConfigureServices(IServiceCollection services) 2 { 3 services.AddMvc(); 4 // Add other framework services 5 6 // Add Autofac 7 var containerBuilder = new ContainerBuilder(); 8 containerBuilder.RegisterModule<DefaultModule>(); 9 containerBuilder.Populate(services); 10 var container = containerBuilder.Build(); 11 return new AutofacServiceProvider(container); 12 }