ASP.NET Core依賴注入——依賴注入最佳實踐

在這篇文章中,咱們將深刻研究.NET Core和ASP.NET Core MVC中的依賴注入,將介紹幾乎全部可能的選項,依賴注入是ASP.Net Core的核心,我將分享在ASP.Net Core應用中使用依賴注入的一些經驗和建議,而且將會討論這些原則背後的動機是什麼:html

(1)有效地設計服務及其依賴關係。web

(2)防止多線程問題。數據庫

(3)防止內存泄漏。緩存

(4)防止潛在的錯誤。安全

在討論該話題以前,瞭解什麼是服務是生命週期相當重要,當組件經過依賴注入請求另外一個組件時,它接收的實例是否對該組件實例是惟一的取決於生命週期。 所以,設置生存期決定了組件實例化的次數以及組件是否共享。多線程

1、服務的生命週期

在ASP.Net Core 依賴注入有三種:mvc

  • Transient :每次請求時都會建立,而且永遠不會被共享。
  • Scoped : 在同一個Scope內只初始化一個實例 ,能夠理解爲( 每個request級別只建立一個實例,同一個http request會在一個 scope內)
  • Singleton :只會建立一個實例。該實例在須要它的全部組件之間共享。所以老是使用相同的實例。

DI容器跟蹤全部已解析的組件, 組件在其生命週期結束時被釋放和處理:async

  • 若是組件具備依賴關係,則它們也會自動釋放和處理。
  • 若是組件實現IDisposable接口,則在組件釋放時自動調用Dispose方法。

重要的是要理解,若是將組件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容器已經拋出異常。

 

2、註冊服務:

註冊服務是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");
    }
}

在中間件中注入組件三種不一樣的方法:

一、構造函數

二、調用參數

三、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 <stringbyte []> _cache; public FileService()
    {_ 
        cache = new ConcurrentDictionary <stringbyte []>(); 
    }
    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。

 

3、總結:

依賴注入起初看起來很簡單,可是若是你不遵循一些嚴格的原則,就會存在潛在的多線程和內存泄漏問題。若是有理解和翻譯不對的地方,還請指出來。到底服務以哪一種方式註冊,仍是要看具體的場景和業務需求,上面是一些建議,能遵照上面的建議,會避免一些沒必要要的問題。可能有些地方理解的還不是很深入,只要在編碼時有這種意識就很是好了,這也是我寫這篇博客的緣由。好了,就聊到這裏,後面還會探討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/

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。

相關文章
相關標籤/搜索