分享翻譯一篇 "Abp" 框架作者(Halil İbrahim Kalkan)關於ASP.NET Core依賴註入的博文. 在本文中,我將分享我在ASP.NET Core應用程式中使用依賴註入的經驗和建議. 這些原則背後的目的是: 1. 有效地設計服務及其依賴關係 2. 防止多線程問題 3. 防止內 ...
分享翻譯一篇Abp框架作者(Halil İbrahim Kalkan)關於ASP.NET Core依賴註入的博文.
在本文中,我將分享我在ASP.NET Core應用程式中使用依賴註入的經驗和建議.
這些原則背後的目的是:
- 有效地設計服務及其依賴關係
- 防止多線程問題
- 防止記憶體泄漏
- 防止潛在的錯誤
本文假設你已經熟悉基本的ASP.NET Core以及依賴註入. 如果沒有的話,請首先閱讀ASP.NET核心依賴註入文檔.
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方法中使用它.
屬性註入
ASP.NET Core的標準依賴註入容器不支持屬性註入,但是你可以使用其它支持屬性註入的IOC容器.
例如:
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具有公開的Logger屬性. 依賴註入容器可以自動設置Logger(前提是ILogger之前註冊到DI容器中).
建議做法
- 僅對可選依賴項使用屬性註入。這意味著你的服務可以脫離這些依賴能正常工作.
- 儘可能得使用Null對象模式(如本例所示
Logger = NullLogger<ProductService>.Instance;
), 不然就需要在使用依賴項時始終做空引用的檢查.
服務定位器
服務定位器模式是獲取依賴服務的另一種方式.
例如:
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),這點同樣適用於構造函數註入和屬性註入.
建議做法
- 如果在開發過程中已知依賴的服務儘可能不使用服務定位器模式, 因為它使依賴關係含糊不清,這意味著在創建服務實例時無法獲得依賴關係,特別是在單元測試中需要模擬服務的依賴性尤為重要.
- 儘可能在構造函數中解析所有的依賴服務,在服務的方法中解析服務會使你的應用程式更加的複雜且容易出錯.我將在下一節中介紹在服務方法中解析依賴服務
服務生命周期
ASP.NET Core下依賴註入中有三種服務生命周期:
- Transient,每次註入或請求時都會創建轉瞬即逝的服務.
- Scoped,是按範圍創建的,在Web應用程式中,每個Web請求都會創建一個新的獨立服務範圍.這意味著服務根據每個Web請求創建.
- Singleton,每個DI容器創建一個單例服務,這通常意味著它們在每個應用程式只創建一次,然後用於整個應用程式生命周期.
DI容器自動跟蹤所有已解析的服務,服務在其生命周期結束時被釋放/處置(release/dispose)
- 如果服務具有依賴關係,則它們的依賴的服務也會自動釋放/處置(release/dispose)
- 如果服務實現IDisposable介面,則在服務被釋放時自動調用Dispose方法.
建議做法
- 儘可能將你的服務生命周期註冊為Transient,因為設計Transient服務很簡單,你通常不關心多線程和記憶體泄漏,該服務的壽命很短.
- 請謹慎使用Scoped生命周期的服務,因為如果你創建子服務作用域或從非Web應用程式使用這些服務,則可能會非常棘手.
- 小心使用Singleton生命周期的服務,這種情況你需要處理多線程和潛在的記憶體泄漏問題.
- 不要在Singleton生命周期的服務中依賴Transient或Scoped生命周期的服務.因為Transient生命周期的服務註入到Singleton生命周期的服務時變為單例實例,如果Transient生命周期的服務沒有對此種情況特意設計過,則可能導致問題. ASP.NET Core預設DI容器會對這種情況拋出異常.
在服務方法中解析依賴服務
在某些情況下你可能需要在服務方法中解析其他服務.在這種情況下,請確保在使用後及時釋放解析得服務,確保這一點的最佳方法是創建Scoped服務.
例如:
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服務,並賦值給_serviceProvider屬性. 然後在PriceCalculator的Calculate方法中使用它來創建子服務範圍。 它使用scope.ServiceProvider來解析服務,而不是註入的_serviceProvider實例。 因此從範圍中解析的所有服務都將在using語句的末尾自動釋放/處置(release/dispose)
建議做法
- 如果要在方法體中解析服務,請始終創建子服務範圍以確保正確的釋放已解析的服務.
- 如果將IServiceProvider作為方法的參數,那麼你可以直接從中解析服務而無需關心釋放/處置(release/dispose). 創建/管理服務範圍是調用方法的代碼的責任. 遵循這一原則使你的代碼更清晰.
- 不要引用解析到的服務,不然它可能會導致記憶體泄漏或者在你以後使用對象引用時可能訪問已處置的(dispose)服務(除非服務是單例)
單例服務(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緩存文件內容以減少磁碟讀取. 此服務應註冊為Singleton,否則緩存將無法按預期工作.
建議做法
- 如果服務需要保持狀態,則應以線程安全的方式訪問該狀態.因為所有請求同時使用相同的服務實例.我使用ConcurrentDictionary而不是Dictionary來確保線程安全.
- 不要在單例服務中使用Scoped生命周期或Transient生命周期的服務.因為臨時服務可能不是設計為線程安全.如果必須使用它們那麼在使用這些服務時請註意多線程問題(例如使用鎖).
- 記憶體泄漏通常由單例服務引起.它們在應用程式結束前不會被釋放/處置(release/dispose). 因此如果他們實例化的類(或註入)但不釋放/處置(release/dispose).它們,它們也將留在記憶體中直到應用程式結束. 確保在正確的時間釋放/處置(released/disposed)它們。 請參閱上面的在方法中的解析服務內容.
- 如果緩存數據(本示例中的文件內容),則應創建一種機制,以便在原始數據源更改時更新/使緩存的數據無效(當上面示例中磁碟上的緩存文件發生更改時).
範圍服務(Scoped Services)
Scoped生命周期的服務乍一看似乎是存儲每個Web請求數據的良好候選者.因為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註冊為Scoped並將其註入兩個不同的服務,則可以獲取從另一個服務添加的項,因為它們將共用相同的RequestItemsService實例.這就是我們對Scoped生命周期服務的期望.
但是...事實可能並不總是那樣. 如果你創建子服務範圍並從子範圍解析RequestItemsService,那麼你將獲得RequestItemsService的新實例,它將無法按預期工作.因此,作用域服務並不總是表示每個Web請求的實例。
你可能認為你沒有犯這樣一個明顯的錯誤(在子範圍內解析服務). 情況可能不那麼簡單. 如果你的服務之間存在大的依賴關係,則無法知道是否有人創建了子範圍並解析了註入另一個服務的服務.最終註入了作用域服務.
建議做法
- Scoped生命周期的服務可以被認為是在Web請求中由太多服務註入的優化.因此,所有這些服務將在同一Web請求期間使用該服務的單個實例.
- Scoped生命周期的服務不需要設計為線程安全的. 因為它們通常應由單個Web請求/線程使用.但是...在這種情況下,你不應該在不同的線程之間共用Scoped生命周期服務!
- 如果你設計Scoped生命周期服務以在Web請求中的其他服務之間共用數據,請務必小心(如上所述). 你可以將每個Web請求數據存儲在HttpContext中(註入IHttpContextAccessor以訪問它),這是更安全的方式. HttpContext的生命周期不是作用域. 實際上它根本沒有註冊到DI(這就是為什麼你不註入它,而是註入IHttpContextAccessor). HttpContextAccessor使用AsyncLocal實現在Web請求期間共用相同的HttpContext.
結論
依賴註入起初看起來很簡單,但是如果你不遵循一些嚴格的原則,就會存在潛在的多線程和記憶體泄漏問題. 我根據自己在ASP.NET Boilerplate框架開發過程中的經驗分享了一些很好的原則.
原文地址:ASP.NET Core Dependency Injection Best Practices, Tips & Tricks