ASP.NET Core依赖注入最佳实践,提示技巧
分享翻譯一篇Abp框架作者(Halil ?brahim Kalkan)關(guān)于ASP.NET Core依賴注入的博文.
在本文中,我將分享我在ASP.NET Core應(yīng)用程序中使用依賴注入的經(jīng)驗(yàn)和建議.
這些原則背后的目的是:
有效地設(shè)計(jì)服務(wù)及其依賴關(guān)系
防止多線程問題
防止內(nèi)存泄漏
防止?jié)撛诘腻e(cuò)誤
本文假設(shè)你已經(jīng)熟悉基本的ASP.NET Core以及依賴注入. 如果沒有的話,請首先閱讀ASP.NET核心依賴注入文檔.
ASP.NET Core 依賴注入文檔
構(gòu)造函數(shù)注入
構(gòu)造函數(shù)注入用在服務(wù)的構(gòu)造函數(shù)上聲明和獲取依賴服務(wù).
例如:
ProductService在構(gòu)造函數(shù)中將IProductRepository注入為依賴項(xiàng),然后在Delete方法中使用它.
屬性注入
ASP.NET Core的標(biāo)準(zhǔn)依賴注入容器不支持屬性注入,但是你可以使用其它支持屬性注入的IOC容器.
例如:
ProductService具有公開的Logger屬性. 依賴注入容器可以自動設(shè)置Logger(前提是ILogger之前注冊到DI容器中).
建議做法
僅對可選依賴項(xiàng)使用屬性注入。這意味著你的服務(wù)可以脫離這些依賴能正常工作.
盡可能得使用Null對象模式(如本例所示Logger = NullLogger<ProductService>.Instance;), 不然就需要在使用依賴項(xiàng)時(shí)始終做空引用的檢查.
服務(wù)定位器
服務(wù)定位器模式是獲取依賴服務(wù)的另一種方式.
例如:
ProductService服務(wù)注入IServiceProvider并使用它來解析其依賴,如果欲解析的依賴未注冊GetRequiredService會拋出異常,GetService只返回NULL.
在構(gòu)造函數(shù)中解析的依賴,它們將會在服務(wù)被釋放的時(shí)候釋放,因此你不需要關(guān)心在構(gòu)造函數(shù)中解析的服務(wù)釋放/處置(release/dispose),這點(diǎn)同樣適用于構(gòu)造函數(shù)注入和屬性注入.
建議做法
如果在開發(fā)過程中已知依賴的服務(wù)盡可能不使用服務(wù)定位器模式, 因?yàn)樗挂蕾囮P(guān)系含糊不清,這意味著在創(chuàng)建服務(wù)實(shí)例時(shí)無法獲得依賴關(guān)系,特別是在單元測試中需要模擬服務(wù)的依賴性尤為重要.
盡可能在構(gòu)造函數(shù)中解析所有的依賴服務(wù),在服務(wù)的方法中解析服務(wù)會使你的應(yīng)用程序更加的復(fù)雜且容易出錯(cuò).我將在下一節(jié)中介紹在服務(wù)方法中解析依賴服務(wù)
服務(wù)生命周期
ASP.NET Core下依賴注入中有三種服務(wù)生命周期:
Transient,每次注入或請求時(shí)都會創(chuàng)建轉(zhuǎn)瞬即逝的服務(wù).
Scoped,是按范圍創(chuàng)建的,在Web應(yīng)用程序中,每個(gè)Web請求都會創(chuàng)建一個(gè)新的獨(dú)立服務(wù)范圍.這意味著服務(wù)根據(jù)每個(gè)Web請求創(chuàng)建.
Singleton,每個(gè)DI容器創(chuàng)建一個(gè)單例服務(wù),這通常意味著它們在每個(gè)應(yīng)用程序只創(chuàng)建一次,然后用于整個(gè)應(yīng)用程序生命周期.
DI容器自動跟蹤所有已解析的服務(wù),服務(wù)在其生命周期結(jié)束時(shí)被釋放/處置(release/dispose)
如果服務(wù)具有依賴關(guān)系,則它們的依賴的服務(wù)也會自動釋放/處置(release/dispose)
如果服務(wù)實(shí)現(xiàn)IDisposable接口,則在服務(wù)被釋放時(shí)自動調(diào)用Dispose方法.
建議做法
盡可能將你的服務(wù)生命周期注冊為Transient,因?yàn)樵O(shè)計(jì)Transient服務(wù)很簡單,你通常不關(guān)心多線程和內(nèi)存泄漏,該服務(wù)的壽命很短.
請謹(jǐn)慎使用Scoped生命周期的服務(wù),因?yàn)槿绻銊?chuàng)建子服務(wù)作用域或從非Web應(yīng)用程序使用這些服務(wù),則可能會非常棘手.
小心使用Singleton生命周期的服務(wù),這種情況你需要處理多線程和潛在的內(nèi)存泄漏問題.
不要在Singleton生命周期的服務(wù)中依賴Transient或Scoped生命周期的服務(wù).因?yàn)?strong>Transient生命周期的服務(wù)注入到Singleton生命周期的服務(wù)時(shí)變?yōu)閱卫龑?shí)例,如果Transient生命周期的服務(wù)沒有對此種情況特意設(shè)計(jì)過,則可能導(dǎo)致問題. ASP.NET Core默認(rèn)DI容器會對這種情況拋出異常.
在服務(wù)方法中解析依賴服務(wù)
在某些情況下你可能需要在服務(wù)方法中解析其他服務(wù).在這種情況下,請確保在使用后及時(shí)釋放解析得服務(wù),確保這一點(diǎn)的最佳方法是創(chuàng)建Scoped服務(wù).
例如:
PriceCalculator在構(gòu)造函數(shù)中注入IServiceProvider服務(wù),并賦值給_serviceProvider屬性. 然后在PriceCalculator的Calculate方法中使用它來創(chuàng)建子服務(wù)范圍。 它使用scope.ServiceProvider來解析服務(wù),而不是注入的_serviceProvider實(shí)例。 因此從范圍中解析的所有服務(wù)都將在using語句的末尾自動釋放/處置(release/dispose)
建議做法
如果要在方法體中解析服務(wù),請始終創(chuàng)建子服務(wù)范圍以確保正確的釋放已解析的服務(wù).
如果將IServiceProvider作為方法的參數(shù),那么你可以直接從中解析服務(wù)而無需關(guān)心釋放/處置(release/dispose). 創(chuàng)建/管理服務(wù)范圍是調(diào)用方法的代碼的責(zé)任. 遵循這一原則使你的代碼更清晰.
不要引用解析到的服務(wù),不然它可能會導(dǎo)致內(nèi)存泄漏或者在你以后使用對象引用時(shí)可能訪問已處置的(dispose)服務(wù)(除非服務(wù)是單例)
單例服務(wù)(Singleton Services)
單例服務(wù)通常用于保持應(yīng)用程序狀態(tài). 緩存服務(wù)是應(yīng)用程序狀態(tài)的一個(gè)很好的例子.
例如:
FileService緩存文件內(nèi)容以減少磁盤讀取. 此服務(wù)應(yīng)注冊為Singleton,否則緩存將無法按預(yù)期工作.
建議做法
如果服務(wù)需要保持狀態(tài),則應(yīng)以線程安全的方式訪問該狀態(tài).因?yàn)樗姓埱笸瑫r(shí)使用相同的服務(wù)實(shí)例.我使用ConcurrentDictionary而不是Dictionary來確保線程安全.
不要在單例服務(wù)中使用Scoped生命周期或Transient生命周期的服務(wù).因?yàn)榕R時(shí)服務(wù)可能不是設(shè)計(jì)為線程安全.如果必須使用它們那么在使用這些服務(wù)時(shí)請注意多線程問題(例如使用鎖).
內(nèi)存泄漏通常由單例服務(wù)引起.它們在應(yīng)用程序結(jié)束前不會被釋放/處置(release/dispose). 因此如果他們實(shí)例化的類(或注入)但不釋放/處置(release/dispose).它們,它們也將留在內(nèi)存中直到應(yīng)用程序結(jié)束. 確保在正確的時(shí)間釋放/處置(released/disposed)它們。 請參閱上面的在方法中的解析服務(wù)內(nèi)容.
如果緩存數(shù)據(jù)(本示例中的文件內(nèi)容),則應(yīng)創(chuàng)建一種機(jī)制,以便在原始數(shù)據(jù)源更改時(shí)更新/使緩存的數(shù)據(jù)無效(當(dāng)上面示例中磁盤上的緩存文件發(fā)生更改時(shí)).
范圍服務(wù)(Scoped Services)
Scoped生命周期的服務(wù)乍一看似乎是存儲每個(gè)Web請求數(shù)據(jù)的良好候選者.因?yàn)锳SP.NET Core會為每個(gè)Web請求創(chuàng)建一個(gè)服務(wù)范圍. 因此,如果你將服務(wù)注冊為作用域則可以在Web請求期間共享該服務(wù).
例如:
如果將RequestItemsService注冊為Scoped并將其注入兩個(gè)不同的服務(wù),則可以獲取從另一個(gè)服務(wù)添加的項(xiàng),因?yàn)樗鼈儗⒐蚕硐嗤腞equestItemsService實(shí)例.這就是我們對Scoped生命周期服務(wù)的期望.
但是...事實(shí)可能并不總是那樣. 如果你創(chuàng)建子服務(wù)范圍并從子范圍解析RequestItemsService,那么你將獲得RequestItemsService的新實(shí)例,它將無法按預(yù)期工作.因此,作用域服務(wù)并不總是表示每個(gè)Web請求的實(shí)例。
你可能認(rèn)為你沒有犯這樣一個(gè)明顯的錯(cuò)誤(在子范圍內(nèi)解析服務(wù)). 情況可能不那么簡單. 如果你的服務(wù)之間存在大的依賴關(guān)系,則無法知道是否有人創(chuàng)建了子范圍并解析了注入另一個(gè)服務(wù)的服務(wù).最終注入了作用域服務(wù).
建議做法
Scoped生命周期的服務(wù)可以被認(rèn)為是在Web請求中由太多服務(wù)注入的優(yōu)化.因此,所有這些服務(wù)將在同一Web請求期間使用該服務(wù)的單個(gè)實(shí)例.
Scoped生命周期的服務(wù)不需要設(shè)計(jì)為線程安全的. 因?yàn)樗鼈兺ǔ?yīng)由單個(gè)Web請求/線程使用.但是...在這種情況下,你不應(yīng)該在不同的線程之間共享Scoped生命周期服務(wù)!
如果你設(shè)計(jì)Scoped生命周期服務(wù)以在Web請求中的其他服務(wù)之間共享數(shù)據(jù),請務(wù)必小心(如上所述). 你可以將每個(gè)Web請求數(shù)據(jù)存儲在HttpContext中(注入IHttpContextAccessor以訪問它),這是更安全的方式. HttpContext的生命周期不是作用域. 實(shí)際上它根本沒有注冊到DI(這就是為什么你不注入它,而是注入IHttpContextAccessor). HttpContextAccessor使用AsyncLocal實(shí)現(xiàn)在Web請求期間共享相同的HttpContext.
結(jié)論
依賴注入起初看起來很簡單,但是如果你不遵循一些嚴(yán)格的原則,就會存在潛在的多線程和內(nèi)存泄漏問題. 我根據(jù)自己在ASP.NET Boilerplate框架開發(fā)過程中的經(jīng)驗(yàn)分享了一些很好的原則.
相關(guān)文章:
C#中字段、屬性、只讀、構(gòu)造函數(shù)賦值、反射賦值的相關(guān)
原文地址:https://www.cnblogs.com/realmaliming/p/9467601.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core依赖注入最佳实践,提示技巧的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NetCore SkyWalking
- 下一篇: MEF 插件式开发 - DotNetCo