ASP.NET Core 依賴注入最佳實踐——提示與技巧

在這篇文章,我將分享一些在ASP.NET Core程序中使用依賴注入的我的經驗和建議。這些原則背後的動機以下:git

  • 高效地設計服務和它們的依賴。
  • 預防多線程問題。
  • 預防內存泄漏。
  • 預防潛在的BUG。

這篇文章假設你已經基本熟悉依賴注入和ASP.NET Core。若是不是,則先閱讀文章: 在ASP.NET Core中使用依賴注入github

基礎

構造函數注入

構造函數注入經常使用於在服務構建上定義和獲取服務依賴。例如:緩存

 1 public class ProductService  2 {  3 private readonly IProductRepository _productRepository;  4 public ProductService(IProductRepository productRepository)  5  {  6 _productRepository = productRepository;  7  }  8 public void Delete(int id)  9  { 10  _productRepository.Delete(id); 11  } 12 }

ProductService 將 IProductRepository做爲依賴注入到它的構造函數,而後在 Delete 方法內部使用這個依賴。安全

實踐指南:

  • 在服務構造函數中明確地定義必需的依賴。所以該服務在沒有這些依賴時沒法被構造。
  • 將注入的依賴賦值給只讀(readonly)的字段或屬性(爲了防止在內部方法中意外地賦予其餘值)。

屬性注入

ASP.NET Core 的標準依賴注入容器不支持屬性注入。可是你可使用其餘容器支持屬性注入。例如:多線程

 

 1 using Microsoft.Extensions.Logging;  2 using Microsoft.Extensions.Logging.Abstractions;  3 namespace MyApp  4 {  5 public class ProductService  6  {  7  public ILogger<ProductService> Logger { get; set; }
8 private readonly IProductRepository _productRepository;
9 public ProductService(IProductRepository productRepository) 10 { 11 _productRepository = productRepository;
12 Logger = NullLogger<ProductService>.Instance; 13 } 14 public void Delete(int id) 15 { 16 _productRepository.Delete(id);
17 Logger.LogInformation( 18 $"Deleted a product with id = {id}"); 19 } 20 } 21 }

 

 

ProductService 定義了一個帶公開setter的Logger 屬性。併發

依賴注入容器能夠設置 Logger屬性,若是它可用(已經註冊到DI容器)。框架

實踐指南:

  • 僅對可選依賴使用屬性注入。這意味着你的服務能夠在沒有提供這些依賴時正常地工做。
  • 若是可能,使用空對象模式(就像這個例子中這樣)。不然,在使用這個依賴時始終檢查是否爲null

服務定位器

服務定位器模式是獲取依賴關係的另一種方式。例如:ide

 

 1 public class ProductService  2 {  3 private readonly IProductRepository _productRepository;
4 private readonly ILogger<ProductService> _logger;
5 public ProductService(IServiceProvider serviceProvider) 6 { 7 _productRepository = serviceProvider 8 .GetRequiredService<IProductRepository>();
9 _logger = serviceProvider 10 .GetService<ILogger<ProductService>>() ?? 11 NullLogger<ProductService>.Instance; 12 } 13 public void Delete(int id) 14 { 15 _productRepository.Delete(id); 16 _logger.LogInformation($"Deleted a product with id = {id}"); 17 } 18 }

 

 

ProductService 注入了 IServiceProvider 來解析並使用依賴。 若是請求的依賴以前沒有被註冊,那麼GetRequiredService將會拋出異常。換句話說, 這種狀況下,GetService只會返回null。函數

當你在構造函數內部解析服務時,它們會隨着服務的釋放而釋放。所以,你沒必要關心構造函數內部已解析服務的釋放問題(就像構造函數注入和屬性注入)。單元測試

實踐指南

  • 儘量不要使用服務定位模式(除非服務類型在開發時就已經知道)。由於它讓依賴不明確。這意味着在建立服務實例期間不可能容易地看出依賴關係。這對單元測試來講尤其重要,由於你可能想要模擬一些依賴。
  • 若是可能,在服務構造函數中解析依賴。在服務方法中解析會使你的程序更加難懂、更加容易出錯。我將在下一個章節討論問題和解決方案。

服務生命週期

 下面是服務在ASP.NET Core依賴注入中的生命週期:

  1. Transient 類型的服務在每次注入或請求的時候被建立。
  2. Scoped 類型的服務按照做用域被建立。在Web程序中,每一個Web請求都會建立新的隔離的服務做用域。這意味着Scoped類型的服務一般會根據Web請求建立。
  3. Singleton 類型的服務由DI容器建立。這一般意味着它們根據應用程序僅僅被建立一次,而後用於應用程序的整個生命週期。

 

DI容器會持續跟蹤全部已經被解析的服務。當服務的生命週期終止時,它們會被釋放並銷燬:

  • 若是服務還有依賴,它們一樣會被自動釋放並銷燬。
  • 若是服務實現了 IDisposable 接口,Dispose 方法會在服務釋放時自動被調用。  

實踐指南:

  • 儘量地將你的服務註冊爲 Transient 類型。由於設計Transient服務是簡單的。你一般不用關心多線程問題內存泄漏問題,而且你知道這類服務只有很短的生存期。
  • 謹慎使用 Scoped 類型服務生命週期,由於若是你建立了子服務做用域或者由非Web程序使用這些服務,那麼它會變得詭異複雜。
  • 謹慎使用Singleton 類型的生命週期,由於你須要處理多線程問題和潛在的內存泄漏問題
  • 不要在Singleton服務上依賴 Transient類型或者 Scoped類型的服務。由於當單例服務注入的時候,Transient服務也會變成單例實例。而且若是Transient服務不是設計用於支持這樣的場景的話則可能會致使一些問題。ASP.NET Core的默認DI容器在這種狀況下會拋出異常

在方法體中解析服務

在某些狀況下,你可能須要在你的服務的某個方法中解析另外一個服務。 這種狀況下,請確保在使用後釋放該服務。保障這個的最好方法是建立一個服務做用域。例如:

 1 public class PriceCalculator  2 {  3     private readonly IServiceProvider _serviceProvider;
4 public PriceCalculator(IServiceProvider serviceProvider) 5 { 6 _serviceProvider = serviceProvider; 7 }
8 public float Calculate(Product product, int count, 9 Type taxStrategyServiceType) 10 { 11 using (var scope = _serviceProvider.CreateScope()) 12 { 13 var taxStrategy = (ITaxStrategy)scope.ServiceProvider 14 .GetRequiredService(taxStrategyServiceType);
15 var price = product.Price * count;
16 return price + taxStrategy.CalculateTax(price); 17 } 18 } 19 }

 

PriceCalculator 在構造函數中注入了 IServiceProvider,並賦值給了一個字段。而後,PriceCalculator使用它在 Calculate方法內部建立了一個子服務做用域。該做用域使用 scope.ServiceProvider來解析服務,替代了注入的 _serviceProvider 實例。所以,在using語句結束後,全部從該做用域解析的服務都會自動釋放並銷燬。

實踐指南:

  • 若是你在某個方法體內解析服務,始終建立一個子服務做用域來確保解析出的服務被正確地釋放。
  • 若是某個方法使用 IServiceProvider做爲參數,你能夠直接從它解析服務,而且沒必要關心服務的釋放和銷燬。建立和管理服務做用域是調用你方法的代碼的職責。遵循這個原則可使你的代碼更加整潔。
  • 不要讓解析到的服務持有引用!不然,它可能致使內存泄漏。而且當你後面在使用對象引用時,你可能訪問到一個已經銷燬的服務。(除非解析到的服務是單例)

Singleton服務

單例服務一般設計用於保持應用程序狀態。緩存是一個應用程序狀態的好例子。例如:

 1 public class FileService  2 {  3 private readonly ConcurrentDictionary<string, byte[]> _cache;
4 public FileService() 5 { 6 _cache = new ConcurrentDictionary<string, byte[]>(); 7 } 8 public byte[] GetFileContent(string filePath) 9 { 10 return _cache.GetOrAdd(filePath, _ => 11 { 12 return File.ReadAllBytes(filePath); 13 }); 14 } 15 }

FileService簡單地緩存了文件內容以減小磁盤讀取。這個服務應該被註冊爲一個單例,不然,緩存將沒法按照預期工做。

實踐指南:

  • 若是服務持有狀態,那它應該以線程安全的方式來訪問這個狀態。由於全部請求會併發地使用該服務的同一個實例。我使用 ConcurrentDictionary 替代 Dictionary 來確保線程安全。
  • 不要在單例服務中使用Transient或Scoped服務。由於Transient服務可能不是設計爲線程安全的。若是你使用了它們,在使用這些服務期間須要處理多線程問題(對實例使用lock語句)
  • 內存泄漏一般由單例服務致使。在應用程序結束前單例服務不會被釋放/銷燬。所以,若是這些單例服務實例化了類(或注入)可是沒有釋放/銷燬,這些類會一直保留在內存中,直到應用程序結束。確保適時地釋放/銷燬這些類。見上面「在方法體中解析服務」的章節。
  • 若是你緩存數據(本例中的文件內容),當原始數據源發生變化時,你應該建立一個機制來更新/失效緩存的數據。

Scoped 服務

Scoped 生命週期的服務看起來是一個不錯的存儲每一個Web請求數據的好方法。由於ASP.NET Core爲每一個Web請求建立一個服務做用域。所以,若是你把一個服務註冊爲Scoped,那麼它能夠在一個Web請求期間被共享。例如:

 1 public class RequestItemsService  2 {  3     private readonly Dictionary<string, object> _items;  4 
 5     public RequestItemsService()  6  {  7         _items = new Dictionary<string, object>();  8  }  9 
10     public void Set(string name, object value) 11  { 12         _items[name] = value; 13  } 14 
15     public object Get(string name) 16  { 17         return _items[name]; 18  } 19 }

若是你將RequestItemsService註冊爲Scoped,並注入到兩個不一樣的服務,而後你能夠獲得一個從另一個服務添加的項。由於它們會共享同一個RequestItemsService的實例。這就是咱們對 Scoped服務的預期

 

可是!!!事實並不老是如此。 若是你建立了一個子服務做用域並從子做用域解析RequestItemsService,而後你會獲得一個RequestItemsService的新實例,而且不會按照你的預期工做。所以,Scoped服務並不老是意味着每一個Web請求一個實例。

你可能認爲你不會犯如此明顯的錯誤(在子做用域內部解析另外一個做用域)。可是,這並非一個錯誤(一個很常規的用法)而且狀況可能不會如此簡單。若是你的服務之間有一個大的依賴關係,你不知道是否有人建立了子做用域並在其餘注入的服務中解析了服務……最終注入了一個Scoped服務。

實踐指南:

  • Scoped服務能夠認爲是在Web請求中注入太多服務的一種優化。所以,在相同的Web請求期間,全部這些服務都將使用該服務的單個實例。
  • Scoped服務無需設計爲線程安全的。由於,它們應該正常地被單個Web請求或線程使用。可是,這這種狀況下,你不該該在不一樣的線程之間共享服務做用域
  • 在Web請求中,若是你設計一個Scoped服務在其餘服務之間共享數據,請當心(上面解釋過)。你能夠在HttpContext中存儲每一個Web請求的數據(注入IHttpContextAccessor 來訪問它),這是共享數據的更安全的方式。 HttpContext的生命週期不是Scoped類型的,事實上,它根本不會被註冊到DI(這也是爲何不注入它,而是注入 IHttpContextAccessor來代替)。HttpContextAccessor 的實現採用 AsyncLocal 在Web請求期間共享同一個 HttpContext.

結論:

依賴注入剛開始看起來很容易使用,可是若是你不遵循一些嚴格的原則,則會有潛在的多線程問題和內存泄漏問題。我分享的這些實踐指南基於我在開發ABP框架期間的我的經驗。

相關文章
相關標籤/搜索