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

来源:https://www.cnblogs.com/realmaliming/archive/2018/08/13/9467601.html
-Advertisement-
Play Games

分享翻譯一篇 "Abp" 框架作者(Halil İbrahim Kalkan)關於ASP.NET Core依賴註入的博文. 在本文中,我將分享我在ASP.NET Core應用程式中使用依賴註入的經驗和建議. 這些原則背後的目的是: 1. 有效地設計服務及其依賴關係 2. 防止多線程問題 3. 防止內 ...


分享翻譯一篇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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • NET Core的跨平臺大家已經有目共睹,而在MAC平臺上做開發已經成為目前的主流,無論哪種語言。 在一次微服務移植的過程中,客戶端需要發送Http自定義混合驗證,在MonoNET上沒有任何問題,而移植到NET Core 2.0並運行,就出現了錯誤:The handler does not supp ...
  • 使用Task,await,async,非同步執行事件(event),不阻塞UI線程和不跨線程執行UI更新 使用Task,await,async 的非同步模式 去執行事件(event) 解決不阻塞UI線程和不誇跨線程執行UI更新報錯的最佳實踐,附加幾種其他方式比較 由於是Winform代碼和其他原因,本文 ...
  • 1、 問題描述 最近使用ABP .Net Core框架做一個微信開發,同時採用了一個微信開發框架集成到ABP,在微信用戶關註的推送事件里調用了一個async 方法,由於沒有返回值,也沒做任何處理,本地調試也OK,但一發佈到線上就有問題,微信公眾號關註成功,也有推送消息過來,但微信用戶一直保存不上,查 ...
  • TransactionScope只要一個操作失敗,它會自動回滾,Complete表示事務完成 實事上,一個錯誤的理解就是Complete()方法是提交事務的,這是錯誤的,事實上,它的作用的表示本事務完成,它一般放在try{}的結尾處,不用判斷前臺操作是否成功,如果不成功,它會自己回滾。 在.net ...
  • 概述 UniformGrid 控制項是一個響應式的佈局控制項,允許把 items 排列在一組均勻分佈的行或列中,以填充整體的可用顯示空間,形成均勻的多個網格。預設情況下,網格中的每個單元格大小相同。 這是一個非常實用的控制項,比如相冊應用中多行多列均勻排列圖片,比如新聞類應用中排列新聞,再比如我們在來畫視 ...
  • 單元測試怎麼寫?包含什麼東西? 說下我自己的心得。 按照官方文檔創建對應的單元測試項目,然後分析介面會出現哪幾種結果或者錯誤,然後給出相應的輸入參數。 單元測試方法有通用的3A模型(Arrange,Action,Assert)。 下麵是我自己寫的其中一個介面的單元測試,如果有問題大家一起討論。 我要 ...
  • /// /// 判斷經緯度是否在範圍類 /// /// 經度 /// 緯度 /// /// public static bool IsInRegion(double longitudeCur, double latitudeCur, IList pathList... ...
  • 本人也是英文盲,翻譯不對的地方請諒解。由於翻譯內容較多,會慢慢更新 orleans簡稱ol,一些專用詞不做翻譯。先決條件,讀這表文章之前需要瞭解:actor,es,cqrs 參考鏈接: https://www.cnblogs.com/netfocus/p/4150084.html http://ww ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...