在這篇文章中,我們將深入研究.NET Core和ASP.NET Core MVC中的依賴注入,將介紹幾乎所有可能的選項,依賴注入是ASP.Net Core的核心,我將分享在ASP.Net Core應用中使用依賴注入的一些經驗和建議,並且將會討論這些原則背后的動機是什么:
(1)有效地設計服務及其依賴關系。
(2)防止多線程問題。
(3)防止內存泄漏。
(4)防止潛在的錯誤。
在討論該話題之前,了解什么是服務是生命周期至關重要,當組件通過依賴注入請求另一個組件時,它接收的實例是否對該組件實例是唯一的取決於生命周期。 因此,設置生存期決定了組件實例化的次數以及組件是否共享。
一、服務的生命周期
在ASP.Net Core 依賴注入有三種:
- Transient :每次請求時都會創建,並且永遠不會被共享。
- Scoped : 在同一個Scope內只初始化一個實例 ,可以理解為( 每一個request級別只創建一個實例,同一個http request會在一個 scope內)
- Singleton :只會創建一個實例。該實例在需要它的所有組件之間共享。因此總是使用相同的實例。
DI容器跟蹤所有已解析的組件, 組件在其生命周期結束時被釋放和處理:
- 如果組件具有依賴關系,則它們也會自動釋放和處理。
- 如果組件實現IDisposable接口,則在組件釋放時自動調用Dispose方法。
重要的是要理解,如果將組件A注冊為單例,則它不能依賴於使用Scoped或Transient生命周期注冊的組件。更一般地說:
服務不能依賴於生命周期小於其自身的服務。
通常你希望將應用范圍的配置注冊為單例,數據庫訪問類,比如Entity Framework上下文被推薦以Scoped方式注入,以便可以重用連接。如果要並行運行的話,請記住Entity Framework上下文不能由兩個線程共享,如果需要,最好將上下文注冊為Transient,然后每個服務都獲得自己的上下文實例,並且可以並行運行。
建議的做法:
盡可能將您的服務注冊為瞬態服務。 因為設計瞬態服務很簡單。 您通常不用關心多線程和內存泄漏,並且您知道該服務的壽命很短。
1、請謹慎使用Scoped,因為如果您創建子服務作用域或從非Web應用程序使用這些服務,則可能會非常棘手。
2、謹慎使用singleton ,因為您需要處理多線程和潛在的內存泄漏問題。
3、在singleton 服務中不要依賴transient 或者scoped 服務,因為如果當一個singleton 服務注入transient服務,這個 transient服務就會變成一個singleton服務,並且如果transient服務不是為支持這種情況而設計的,則可能導致問題。 在這種情況下,ASP.NET Core的默認DI容器已經拋出異常。
二、注冊服務:
注冊服務是ConfigureServices(IServiceCollection)
在您Startup
班級的方法中完成的。
以下是服務注冊的示例:
services.Add(new ServiceDescriptor(typeof(IDataService), typeof(DataService), ServiceLifetime.Transient));
該行代碼添加DataService
到服務集合中。服務類型設置為IDataService
如此,如果請求該類型的實例,則它們將獲得實例DataService
。生命周期也設置為Transient,因此每次都會創建一個新實例。
ASP.NET Core提供了各種擴展方法,方便服務的注冊,一下是最常用的方式,也是比較推薦的做法:
services.AddTransient<IDataService, DataService>();
簡單吧,對於不同的生命周期,有類似的擴展方法,你可以猜測它們的名稱。如果需要,你還可以注冊單一類型(實現類型=服務類型)
services.AddTransient<DataService>();
services.AddTransient<DataService, DataService>();
在某些特殊情況下,您可能希望接管某些服務的實例化過程。在這種情況下,您可以使用下面的方法。例子:
services.AddTransient<IDataService, DataService>((ctx) => { IOtherService svc = ctx.GetService<IOtherService>(); //IOtherService svc = ctx.GetRequiredService<IOtherService>(); return new DataService(svc); });
單例組件的注入,可以這樣做:
services.AddSingleton<IDataService>(new DataService());
有一個非常有意思的場景,DataService
實現兩個接口,如果我們這樣做:
驗證結果:
我們將會得到兩個實例,如果我們想共享一個實例,可以這樣做:
驗證結果:
如果組件具有依賴項,則可以從服務集合構建服務提供程序並從中獲取必要的依賴項:
IServiceProvider provider = services.BuildServiceProvider(); IOtherService otherService = provider.GetRequiredService<IOtherService>(); var dataService = new DataService(otherService); services.AddSingleton<IDataService>(dataService); services.AddSingleton<ISomeInterface>(dataService);
但我們一般不會這樣使用,也不建議這樣使用。
現在我們已經注冊了我們的組件,我們可以轉向實際使用它們,如下:
- 構造函數注入
構造函數注入用於在服務構造上聲明和獲取服務的依賴關系。 例如:
public class ProductService { private readonly IProductRepository _productRepository; public ProductService(IProductRepository productRepository) { _productRepository = productRepository; } public void Delete(int id) { _productRepository.Delete(id); } }
ProductService在其構造函數中將IProductRepository注入為依賴項,然后在Delete方法中使用它。
建議的做法:
- 在構造函數中顯示定義所需的依賴項。
- 將注入的依賴項分配給只讀【readonly】字段/屬性(防止在方法內意外地為其分配另外一個值),如果你的項目接入到sonar就會知道這是一個代碼規范。
- 服務定位器
服務定位器是另外一種獲取依賴項的模式,例如:
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。
解析構造函數中的服務時,將在釋放服務時釋放它們,所以,你不用關心釋放/處理在構造函數中解析的服務(就像構造函數和屬性注入一樣)。
建議的做法:
(1)盡可能不使用服務定位器模式,因為該模式存在隱含的依賴關系,這意味着在創建服務實例時無法輕松查看依賴關系,但是該模式對單元測試尤為重要。
(2)如果可能,解析服務構造函數中的依賴項。 解析服務方法會使您的應用程序更加復雜且容易出錯。 我將在下一節中介紹問題和解決方案。
再看一個綜合的例子:
public class LoggingMiddleware { private readonly RequestDelegate _next; public LoggingMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext ctx) { Debug.WriteLine("Request starting"); await _next(ctx); Debug.WriteLine("Request complete"); } }
在中間件中注入組件有三種不同的方法:
1、構造函數
2、調用參數
3、HttpContext.RequestServices
讓我們看看這三種方式注入的使用:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace WebAppPerformance { // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project public class LoggingMiddleware { private readonly RequestDelegate _next; private readonly IDataService _svc; public LoggingMiddleware(RequestDelegate next, IDataService svc) { _next = next; _svc = svc; } public async Task Invoke(HttpContext httpContext, IDataService svc2) { IDataService svc3 = httpContext.RequestServices.GetService<IDataService>(); Debug.WriteLine("Request starting"); await _next(httpContext); Debug.WriteLine("Request complete"); } } // Extension method used to add the middleware to the HTTP request pipeline. public static class LoggingMiddlewareExtensions { public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<LoggingMiddleware>(); } } }
中間件在應用程序生命周期中僅實例化一次,因此通過構造函數注入的組件對於所有通過的請求都是相同的。如果IDataService被注冊為singleton,我們會在所有這些實例中獲得相同的實例。
如果被注冊為scoped,svc2
並且svc3
將是同一個實例,但不同的請求會獲得不同的實例;如果在Transient 的情況下,它們都是不同的實例。
注意:我會盡量避免使用RequestServices
,只有在中間件中才使用它。
MVC過濾器中注入:
但是,我們不能像往常一樣在控制器上添加屬性,因為它必須在運行時獲得依賴關系。
我們有兩個選項可以在控制器或action級別添加它:
[TypeFilter(typeof(TestActionFilter))] public class HomeController : Controller { } // or [ServiceFilter(typeof(TestActionFilter))] public class HomeController : Controller { }
關鍵的區別在於,TypeFilterAttribute
將確定過濾器依賴性是什么,通過DI獲取它們,並創建過濾器。ServiceFilterAttribute
試圖從服務集合中找到過濾器!
為了[ServiceFilter(typeof(TestActionFilter))]
工作,我們需要更多配置:
public void ConfigureServices(IServiceCollection services) { services.AddTransient<TestActionFilter>(); }
現在ServiceFilterAttribute
可以找到過濾器了。
如果要全局添加過濾器:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(mvc => { mvc.Filters.Add(typeof(TestActionFilter)); }); }
這次不需要將過濾器添加到服務集合中,就像TypeFilterAttribute
在每個控制器上添加了一個過濾器一樣。
在方法體內解析服務
在某些情況下,您可能需要在方法中解析其他服務。在這種情況下,請確保在使用后釋放服務。確保這一點的最佳方法是創建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並將其分配給字段。然后,PriceCalculator在Calculate方法中使用它來創建子組件范圍。它使用scope.ServiceProvider來解析服務,而不是注入的_serviceProvider實例。因此,從范圍中解析的所有服務都將在using語句的末尾自動釋放/處理。
建議的做法:
- 如果要在方法體中解析服務,請始終創建子服務范圍以確保正確釋放已解析的服務。
- 如果一個方法把IServiceProvider 作為參數,那么可以直接從中解析出服務,不用關心服務的釋放/銷毀。創建/管理服務的scoped是調用你方法的代碼的責任,所以遵循該原則能是你的代碼更簡潔。
- 不要引用已經解析的服務,否則會導致內存泄漏,並且當你后面使用了對象的引用時,將很有機會訪問到已經銷毀的服務(除非被解析的服務是一個單例)
單例服務
單例服務通常用來保存應用程序的狀態,緩存是應用程序狀態的一個很好的例子,例如:
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 服務,因為transient 服務可能不是線程安全的,如果必須使用它們,那么在使用這些服務時請注意多線程。
- 內存泄漏通常是單例服務導致的,因為它們將駐留在內存中,直到應用程序結束。所以請確保在合適的時間釋放它們,可以參考在方法體內解析服務部分。
- 如果緩存數據(本示例中的文件內容),則應創建一種機制,以便在原始數據源更改時更新/使緩存的數據無效(當此示例中磁盤上的緩存文件發生更改時)。
域服務
Scoped生命周期首先似乎是存儲每個Web請求數據的良好候選者。 因為ASP.NET Core會為每個Web請求創建一個服務范圍【同一個http請求會在同一個域內】。 因此,如果您將服務注冊為Scoped,則可以在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]; } }
如果你以scoped注入RequestItemsService 並將其注入到兩個不同的服務中去,那么你可以從另外一個服務中獲取添加的項,因為它們將共享相同的RequestItemsService實例,這也是我們所期望看到的。但是事實並不是我們想象的那樣。如果你創建一個子域,並從子域中獲取RequestItemsService ,那么你將會獲取一個新的RequestItemsService 實例,並且這個新的實例並不會像你期望的那樣工作。所以,scoped服務並不總是表示每個Web請求的實例。你可能認為自己不會出現這樣的錯誤,但是,你並不能保證別人不會創建子域,並從中解析服務。
建議的做法:
- 一個scoped服務可以被認為是一個Web請求中太多服務被注入的優化。因此在相同的web請求期間,所有這些服務將會使用一個實例。
- scoped服務不需要設計為線程安全的,因為,它們通常應有單個web請求/線程使用。但是!你不應該在不同的線程之間共享scope服務!
- 如果您設計scoped服務以在Web請求中的其他服務之間共享數據,請務必小心!!!您可以將每個Web請求數據存儲在HttpContext中(注入IHttpContextAccessor以訪問它),這是更安全的方式。HttpContext的生命周期不是作用域。實際上,它根本沒有注冊到DI(這就是為什么你不注入它,而是注入IHttpContextAccessor)。HttpContextAccessor實現使用AsyncLocal在Web請求期間共享相同的HttpContext。
三、總結:
依賴注入起初看起來很簡單,但是如果你不遵循一些嚴格的原則,就會存在潛在的多線程和內存泄漏問題。如果有理解和翻譯不對的地方,還請指出來。到底服務以哪種方式注冊,還是要看具體的場景和業務需求,上面是一些建議,能遵守上面的建議,會避免一些不必要的問題。可能有些地方理解的還不是很深刻,只要在編碼時有這種意識就非常好了,這也是我寫這篇博客的原因。好了,就聊到這里,后面還會探討ASP.Net Core MVC配置相關的源碼,依賴注入是.Net Core中的核心,如果對依賴注入基礎知識還不太明白的話,可以參考老A和騰飛兩位大佬的博客:
https://www.cnblogs.com/artech/p/dependency-injection-in-asp-net-core.html
https://www.cnblogs.com/jesse2013/p/di-in-aspnetcore.html
參考文章:
https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96
https://joonasw.net/view/aspnet-core-di-deep-dive
作者:郭崢
出處:http://www.cnblogs.com/runningsmallguo/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。