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


在這篇文章中,我們將深入研究.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 <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。

 

三、總結:

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

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


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com