everythingtoolbar.dll”或它的一个依赖项。_ASP.NET Core依赖注入最佳实践、提示和技巧...
譯者前言
本文譯自ABP框架的開發博客《ASP.NET Core Dependency Injection Best Practices, Tips & Tricks》一文(原作者是Halil ?brahim Kalkan),僅在知乎平臺發布。轉載請注明原文鏈接、本文鏈接和譯者(知乎用戶 @葉影 )。
原文前言
在本文中,筆者將對http://ASP.NET Core應用中使用依賴注入的話題分享經驗和建議。這些原則背后的動機是:
- 有效地設計服務及其依賴性
- 防止多線程問題
- 防止內存泄漏
- 防止潛在的錯誤
本文假定讀者已經基本熟悉了依賴注入和http://ASP.NET Core。如果沒有,請首先閱讀微軟ASP.NET Core依賴注入文檔。
基礎
構造器注入用于聲明和獲取服務對服務構造的依賴關系。例如:
public class ProductService {private readonly IProductRepository _productRepository;public ProductService(IProductRepository productRepository){_productRepository = productRepository;}public void Delete(int id){_productRepository.Delete(id);} }ProductService將IProductRepository作為依賴項注入其構造函數中,然后在Delete方法中使用它。
優秀實踐:
- 在服務的構造函數中顯式定義所需的依賴項。這樣,沒有依賴項就無法構建服務。
- 將注入的依賴項分配給只讀字段/屬性(以防止在方法內部意外為其分配另一個值)。
2.屬性注入
http://ASP.NET Core的標準依賴注入容器不支持屬性注入。但是您可以使用支持屬性注入的容器。例如:
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace MyApp {public class ProductService{public ILogger<ProductService> Logger { get; set; }private readonly IProductRepository _productRepository;public ProductService(IProductRepository productRepository){_productRepository = productRepository;Logger = NullLogger<ProductService>.Instance;}public void Delete(int id){_productRepository.Delete(id);Logger.LogInformation($"Deleted a product with id = {id}");}} }ProductService使用public setter聲明Logger屬性。只要ILogger的實例可用(之前已注冊到DI容器),依賴注入容器就可以設置Logger對象。
優秀實踐:
- 僅將屬性注入用于可選的依賴項。這意味著您的服務可以在不提供這些依賴項的情況下正常運行。
- 如果可能,請使用空對象模式(Null Object Pattern,如本例所示)。否則,請在使用依賴項時始終進行Null檢查。
3. 服務定位
服務定位模式(Service Locator Pattern)是獲取依賴項的另一種方式。例如:
public class ProductService {private readonly IProductRepository _productRepository;private readonly ILogger<ProductService> _logger;public ProductService(IServiceProvider serviceProvider){_productRepository = serviceProvider.GetRequiredService<IProductRepository>();_logger = serviceProvider.GetService<ILogger<ProductService>>() ??NullLogger<ProductService>.Instance;}public void Delete(int id){_productRepository.Delete(id);_logger.LogInformation($"Deleted a product with id = {id}");} }ProductService注入了IServiceProvider并使用它來解決依賴關系。如果之前未注冊請求的依賴項,則GetRequiredService會拋出異常。另一方面,在這種情況下,GetService僅返回Null。
當您在構造函數內部解析服務時,這些依賴服務將隨著服務釋放而一起釋放。因此,您不必關心釋放(release)/處理(dispose)在構造函數內部解析的依賴服務(就像構造函數和屬性注入一樣)。
優秀實踐:
- 不管在什么地方都盡量別使用服務定位器模式(如果服務的類型在開發階段已知)。因為它使依賴關系變成了隱式的(implicit)。那意味著在創建一個服務實例時不能輕易地看出它的依賴項。這對于單元測試尤其重要,在單元測試中,您可能希望模擬(mock)服務的某些依賴關系。
- 盡量在服務的構造方法中解析依賴關系。在服務的普通方法中解析依賴會使您的應用程序更加復雜和容易出錯。筆者將在下一部分中介紹問題和解決方案。
4. 服務生命周期
在http://ASP.NET Core的依賴注入中,有三種服務生命周期。
- Transient類型的服務。每次注入或請求都會創建一個實例。
- Scoped類型的服務。按域(scope)的概念創建實例。在一個web應用中,每個web請求都會創建一個新的單獨的服務域。那意味著scope類型的服務一般在每個web請求中都會創建一次。
- Singleton類型的服務。在每個DI容器里創建。通常,這意味著每個應用程序只能創建一次,然后在整個應用程序生命周期中使用它們。
DI容器會持續跟蹤所有解析過的服務。所有服務都會在生命周期結束時被釋放和處理:
- 如果服務具有依賴項,則它們也將自動釋放和處置。
- 如果該服務實現IDisposable接口,則在服務釋放時Dispose方法會被自動調用。
優秀實踐:
- 盡可能將您的服務注冊為Transient服務。因為設計Transient服務很簡單。您通常不用關心多線程和內存泄漏,并且知道該服務的生命周期很短。
- 慎用Scoped服務,因為如果您創建了子服務域或者從一個非Web應用程序使用這些服務,情況可能會很棘手。
- 慎用Singleton服務,因為使用后你需要處理多線程和潛在的內存泄漏問題。
- 不要依賴Singleton服務中的Transient或Scope服務。因為,隨著Singleton服務的注入,其中的Transient服務也變成了一個單例,而如果Transient服務在設計上并不支持這樣的一個場景,則可能會導致問題。在這種情況下,http://ASP.NET Core的默認DI容器會拋出異常。
在方法體中解析服務
在某些情況下,您可能需要在您的服務中某個方法里解析另一項服務。在這種情況下,請確保在使用后釋放服務。確保這一點的最佳方法是創建服務域。例如:
public class PriceCalculator {private readonly IServiceProvider _serviceProvider;public PriceCalculator(IServiceProvider serviceProvider){_serviceProvider = serviceProvider;}public float Calculate(Product product, int count,Type taxStrategyServiceType){using (var scope = _serviceProvider.CreateScope()){var taxStrategy = (ITaxStrategy)scope.ServiceProvider.GetRequiredService(taxStrategyServiceType);var price = product.Price * count;return price + taxStrategy.CalculateTax(price);}} }PriceCalculator將IServiceProvider注入其構造函數中,并將其分配給字段。然后,PriceCalculator在Calculate方法中使用它來創建子服務域。它使用scope.ServiceProvider解析服務,而不是注入的_serviceProvider實例。因此,從域解析的所有服務都會在using語句的末尾自動釋放/處理。
優秀實踐:
- 如果要在方法體中解析服務,請始終創建子服務域以確保正確釋放解析出的服務。
- 如果方法以IServiceProvider作為參數,則方法內可以直接從中解析服務,而無需考慮釋放(release)/處理(dispose)。創建/管理服務域由調用您方法的代碼負責。遵循此原則可使您的代碼更整潔。
- 不要保留對解析出的服務的引用!否則,這可能會導致內存泄漏,以及當之后使用該對象引用時,您將訪問到一個資源已釋放的服務(disposed service)(除非解析的服務是單例)。
單例服務(Singleton Services)
單例服務通常被設計用來保持一個應用程序狀態。緩存是應用程序狀態中一個很好的例子。例如:
public class FileService {private readonly ConcurrentDictionary<string, byte[]> _cache;public FileService(){_cache = new ConcurrentDictionary<string, byte[]>();}public byte[] GetFileContent(string filePath){return _cache.GetOrAdd(filePath, _ =>{return File.ReadAllBytes(filePath);});} }FileService只是緩存文件內容以減少磁盤讀取。該服務應注冊為單例。否則,緩存將無法按預期工作。
優秀實踐:
- 如果服務保留一個狀態,則應以線程安全的方式訪問該狀態。因為所有請求可以同時使用服務的相同實例。筆者使用ConcurrentDictionary而不是Dictionary來確保線程安全。
- 不要使用單例服務中的Scoped或Transient服務。因為,Transient服務可能沒有被設計為線程安全的。如果您不得不使用它們,則在使用這些服務時要注意多線程(例如使用鎖)。
- 內存泄漏通常是由單例服務引起的。在應用程序結束之前,它們不會被釋放/處理。因此,如果單例服務實例化了類(或注入)但不釋放/處理它們,這些對象也將保留在內存中,直到應用程序結束。確保在適當的時候釋放/處理它們。請參閱上文“在方法體中解析服務”。
- 如果您緩存了數據(如本例中的文件內容),則應創建一種機制,當初始數據源發生變化時(本例中就是對已緩存的文件內容,實際硬盤上的文件發生了變動),由該機制更新/作廢緩存的數據。
域服務(Scoped Services)
作用域類型服務的生命周期首先似乎是存儲每個Web請求數據的理想候選。因為http://ASP.NET Core會為每個Web請求創建一個服務域。因此,如果您將服務注冊為域服務,則可以在Web請求期間共享該服務。例如:
public class RequestItemsService {private readonly Dictionary<string, object> _items;public RequestItemsService(){_items = new Dictionary<string, object>();}public void Set(string name, object value){_items[name] = value;}public object Get(string name){return _items[name];} }如果將RequestItemsService注冊為域服務并將其注入到兩個不同的服務中,則可以在其中一個服務里獲得從另一個服務添加的項目,因為它們將共享相同的RequestItemsService實例。這就是我們對域服務的期望。
但……事實并不總是那樣。如果創建了一個子服務域并從子域解析RequestItemsService,則將獲得RequestItemsService的新實例,它將無法按預期工作。所以,域服務并不總等同于每個Web請求里的實例。
您可能會覺得您不會犯這么明顯的錯誤(在子作用域內解析一個域服務)。然而,這不是一個錯誤(而是一個常規用法),而且實際情況可能并非如此簡單。如果您的服務之間存在很大的依賴關系圖,您將無法得知是否有人創建了一個子作用域并在其中解析出了一個服務,又將解析出的服務注入進了另一個服務……最終就是注入了一個域服務。
優秀實踐:
- 域服務可以被認為是一種優化,一個Web請求里有非常多的服務注入它。因此,所有這些服務將在同一Web請求期間使用該服務的單例。
- 域服務不需要設計為線程安全的。因為,它們通常應由單個Web請求/線程使用。然而……如果不是的話,您不應該在不同線程之間共享服務域!
- 如果您要設計一個域服務,用來在單個Web請求的其他服務之間共享數據,請謹慎。您可以將每個Web請求的數據存儲在HttpContext內部(注入IHttpContextAccessor進行訪問),這是更安全的方法。HttpContext的生存期不受作用域限制。實際上,它壓根就沒有注冊到DI(這就是為什么您不注入它,而是注入IHttpContextAccessor的原因)。HttpContextAccessor的實現使用了AsyncLocal使得在Web請求期間共享相同的HttpContext。
結論
依賴注入最開始似乎很容易使用,但若您不遵循某些嚴格的原則,則可能存在潛在的多線程和內存泄漏問題。筆者基于自己在ASP.NET Boilerplate框架的開發過程中總結的經驗,分享了一些好的原則。
總結
以上是生活随笔為你收集整理的everythingtoolbar.dll”或它的一个依赖项。_ASP.NET Core依赖注入最佳实践、提示和技巧...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: json字符串中的大括号转义传到后台_j
- 下一篇: c6011取消对null指针的引用_C+