在這篇文章中,咱們將深刻研究.NET Core和ASP.NET Core MVC中的依賴注入,將介紹幾乎全部可能的選項,依賴注入是ASP.Net Core的核心,我將分享在ASP.Net Core應用中使用依賴注入的一些經驗和建議,而且將會討論這些原則背後的動機是什麼:html
(1)有效地設計服務及其依賴關係。web
(2)防止多線程問題。數據庫
(3)防止內存泄漏。緩存
(4)防止潛在的錯誤。安全
在討論該話題以前,瞭解什麼是服務是生命週期相當重要,當組件經過依賴注入請求另外一個組件時,它接收的實例是否對該組件實例是惟一的取決於生命週期。 所以,設置生存期決定了組件實例化的次數以及組件是否共享。多線程
在ASP.Net Core 依賴注入有三種:mvc
DI容器跟蹤全部已解析的組件, 組件在其生命週期結束時被釋放和處理:async
重要的是要理解,若是將組件A註冊爲單例,則它不能依賴於使用Scoped或Transient生命週期註冊的組件。更通常地說:ide
服務不能依賴於生命週期小於其自身的服務。函數
一般你但願將應用範圍的配置註冊爲單例,數據庫訪問類,好比Entity Framework上下文被推薦以Scoped方式注入,以即可以重用鏈接。若是要並行運行的話,請記住Entity Framework上下文不能由兩個線程共享,若是須要,最好將上下文註冊爲Transient,而後每一個服務都得到本身的上下文實例,而且能夠並行運行。
建議的作法:
儘量將您的服務註冊爲瞬態服務。 由於設計瞬態服務很簡單。 您一般不用關心多線程和內存泄漏,而且您知道該服務的壽命很短。
一、請謹慎使用Scoped,由於若是您建立子服務做用域或從非Web應用程序使用這些服務,則可能會很是棘手。
二、謹慎使用singleton ,由於您須要處理多線程和潛在的內存泄漏問題。
三、在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方法中使用它。
建議的作法:
服務定位器是另一種獲取依賴項的模式,例如:
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"); } }
在中間件中注入組件有三種不一樣的方法:
一、構造函數
二、調用參數
三、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語句的末尾自動釋放/處理。
建議的作法:
單例服務
單例服務一般用來保存應用程序的狀態,緩存是應用程序狀態的一個很好的例子,例如:
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。不然,緩存將沒法按預期工做。
建議的作法:
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請求的實例。你可能認爲本身不會出現這樣的錯誤,可是,你並不能保證別人不會建立子域,並從中解析服務。
建議的作法:
依賴注入起初看起來很簡單,可是若是你不遵循一些嚴格的原則,就會存在潛在的多線程和內存泄漏問題。若是有理解和翻譯不對的地方,還請指出來。到底服務以哪一種方式註冊,仍是要看具體的場景和業務需求,上面是一些建議,能遵照上面的建議,會避免一些沒必要要的問題。可能有些地方理解的還不是很深入,只要在編碼時有這種意識就很是好了,這也是我寫這篇博客的緣由。好了,就聊到這裏,後面還會探討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/
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。