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


分享翻譯一篇Abp框架作者(Halil İbrahim Kalkan)關於ASP.NET Core依賴注入的博文.

在本文中,我將分享我在ASP.NET Core應用程序中使用依賴注入的經驗和建議.

這些原則背后的目的是:

  1. 有效地設計服務及其依賴關系
  2. 防止多線程問題
  3. 防止內存泄漏
  4. 防止潛在的錯誤

本文假設你已經熟悉基本的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容器中).

建議做法

  1. 僅對可選依賴項使用屬性注入。這意味着你的服務可以脫離這些依賴能正常工作.
  2. 盡可能得使用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),這點同樣適用於構造函數注入和屬性注入.

建議做法

  1. 如果在開發過程中已知依賴的服務盡可能不使用服務定位器模式, 因為它使依賴關系含糊不清,這意味着在創建服務實例時無法獲得依賴關系,特別是在單元測試中需要模擬服務的依賴性尤為重要.
  2. 盡可能在構造函數中解析所有的依賴服務,在服務的方法中解析服務會使你的應用程序更加的復雜且容易出錯.我將在下一節中介紹在服務方法中解析依賴服務

服務生命周期

ASP.NET Core下依賴注入中有三種服務生命周期:

  1. Transient,每次注入或請求時都會創建轉瞬即逝的服務.
  2. Scoped,是按范圍創建的,在Web應用程序中,每個Web請求都會創建一個新的獨立服務范圍.這意味着服務根據每個Web請求創建.
  3. Singleton,每個DI容器創建一個單例服務,這通常意味着它們在每個應用程序只創建一次,然后用於整個應用程序生命周期.

DI容器自動跟蹤所有已解析的服務,服務在其生命周期結束時被釋放/處置(release/dispose)

  1. 如果服務具有依賴關系,則它們的依賴的服務也會自動釋放/處置(release/dispose)
  2. 如果服務實現IDisposable接口,則在服務被釋放時自動調用Dispose方法.

建議做法

  1. 盡可能將你的服務生命周期注冊為Transient,因為設計Transient服務很簡單,你通常不關心多線程和內存泄漏,該服務的壽命很短.
  2. 請謹慎使用Scoped生命周期的服務,因為如果你創建子服務作用域或從非Web應用程序使用這些服務,則可能會非常棘手.
  3. 小心使用Singleton生命周期的服務,這種情況你需要處理多線程和潛在的內存泄漏問題.
  4. 不要在Singleton生命周期的服務中依賴TransientScoped生命周期的服務.因為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)

建議做法

  1. 如果要在方法體中解析服務,請始終創建子服務范圍以確保正確的釋放已解析的服務.
  2. 如果將IServiceProvider作為方法的參數,那么你可以直接從中解析服務而無需關心釋放/處置(release/dispose). 創建/管理服務范圍是調用方法的代碼的責任. 遵循這一原則使你的代碼更清晰.
  3. 不要引用解析到的服務,不然它可能會導致內存泄漏或者在你以后使用對象引用時可能訪問已處置的(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,否則緩存將無法按預期工作.

建議做法

  1. 如果服務需要保持狀態,則應以線程安全的方式訪問該狀態.因為所有請求同時使用相同的服務實例.我使用ConcurrentDictionary而不是Dictionary來確保線程安全.
  2. 不要在單例服務中使用Scoped生命周期或Transient生命周期的服務.因為臨時服務可能不是設計為線程安全.如果必須使用它們那么在使用這些服務時請注意多線程問題(例如使用鎖).
  3. 內存泄漏通常由單例服務引起.它們在應用程序結束前不會被釋放/處置(release/dispose). 因此如果他們實例化的類(或注入)但不釋放/處置(release/dispose).它們,它們也將留在內存中直到應用程序結束. 確保在正確的時間釋放/處置(released/disposed)它們。 請參閱上面的在方法中的解析服務內容.
  4. 如果緩存數據(本示例中的文件內容),則應創建一種機制,以便在原始數據源更改時更新/使緩存的數據無效(當上面示例中磁盤上的緩存文件發生更改時).

范圍服務(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請求的實例。

你可能認為你沒有犯這樣一個明顯的錯誤(在子范圍內解析服務). 情況可能不那么簡單. 如果你的服務之間存在大的依賴關系,則無法知道是否有人創建了子范圍並解析了注入另一個服務的服務.最終注入了作用域服務.

建議做法

  1. Scoped生命周期的服務可以被認為是在Web請求中由太多服務注入的優化.因此,所有這些服務將在同一Web請求期間使用該服務的單個實例.
  2. Scoped生命周期的服務不需要設計為線程安全的. 因為它們通常應由單個Web請求/線程使用.但是...在這種情況下,你不應該在不同的線程之間共享Scoped生命周期服務!
  3. 如果你設計Scoped生命周期服務以在Web請求中的其他服務之間共享數據,請務必小心(如上所述). 你可以將每個Web請求數據存儲在HttpContext中(注入IHttpContextAccessor以訪問它),這是更安全的方式. HttpContext的生命周期不是作用域. 實際上它根本沒有注冊到DI(這就是為什么你不注入它,而是注入IHttpContextAccessor). HttpContextAccessor使用AsyncLocal實現在Web請求期間共享相同的HttpContext.

結論

依賴注入起初看起來很簡單,但是如果你不遵循一些嚴格的原則,就會存在潛在的多線程和內存泄漏問題. 我根據自己在ASP.NET Boilerplate框架開發過程中的經驗分享了一些很好的原則.

原文地址:ASP.NET Core Dependency Injection Best Practices, Tips & Tricks


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM