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

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

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

這些原則背後的目的是:c#

  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容器中).ide

建議作法

  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

相關文章
相關標籤/搜索