標題反映的是上週五一個同事諮詢個人問題,我以爲這是一個很好的問題。這個問題有助於咱們深刻理解依賴注入框架在ASP.NET Core中的應用,以及服務實例的生命週期。html
咱們經過一個簡單的實例來模擬該同事遇到的問題。咱們採用極簡的方式建立了以下這個ASP.NET Core MVC應用。以下面的代碼片斷所示,除了註冊與ASP.NET Core MVC框架相關的服務與中間件以外,咱們還調用了IHostBuilder的UseDefaultServiceProvider方法將配置選項ServiceProviderOptions的ValidateScopes屬性設置爲True,以開啓針對服務範圍的驗證。咱們還採用Scoped生命週期模式註冊了服務IFoobar,具體的實現類型Foobar還實現了IDisposable接口。編程
public class Program { public static void Main() { Host .CreateDefaultBuilder() .UseDefaultServiceProvider(options => options.ValidateScopes = true) .ConfigureWebHostDefaults(builder => builder .ConfigureLogging(logging => logging.ClearProviders()) .ConfigureServices(services => services .AddScoped<IFoobar, Foobar>() .AddRouting() .AddControllers()) .Configure(app => app .UseRouting() .UseEndpoints(endpoints => endpoints.MapControllers()))) .Build() .Run(); } } public interface IFoobar { } public class Foobar : IFoobar, IDisposable { public void Dispose() => Console.WriteLine("Foobar.Dispose();"); }
咱們建立了以下這個HomeController,它的構造函數中注入了一個IServiceProvider對象。在Action方法Index中,咱們調用Task的靜態方法Run異步執行了一些操做。具體來講,在異步執行的操做中,咱們利用調用上面注入的這個IServiceProvider對象的GetRequiredService<T>方法試圖獲取一個IFoobar服務實例。因爲這段操做時在一個Try/Catch中執行的,拋出的異常消息的堆棧信息會直接輸出到控制檯上。瀏覽器
public class HomeController: Controller { private readonly IServiceProvider _requestServices; public HomeController(IServiceProvider requestServices) { _requestServices = requestServices; } [HttpGet("/")] public IActionResult Index() { Task.Run(async() => { try { await Task.Delay(100); var foobar = _requestServices.GetRequiredService<IFoobar>(); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } }); return Ok(); } }
在運行該應用程序後,咱們利用瀏覽器採用根路徑(「/」)對Action方法Index發起訪問後,服務端控制檯上會出現以下所示的錯誤信息。app
從上圖所示的錯誤消息能夠看出,問題出在咱們試圖利用一個被Dispose的IServiceProvider來獲取咱們所需的服務實例。咱們知道,ASP.NET Core應用在啓動和請求處理過程當中所需的服務幾乎都是由表明DI容器的IServiceProvider提供的。具體來講,這裏存在着兩種類型的IServiceProvider對象,一種與當前應用的生命週期保持一致,咱們通常將其稱爲ApplicationServices,另外一種則是具體針對每一個請求的IServiceProvider對象,咱們將其稱爲RequestServices。框架
通常來講,ApplicationServices用於提供管道構建過程當中所需的服務實例,具體請求處理過程當中所需的服務實例通常由RequestServices提供。具體來講,對於接收的每個請求,ASP.NET Core框架都會利用ApplicationServices建立一個表明服務範圍的IServiceScope對象,後者就是對RequestServices的封裝。在完成了針對請求的處理以後,服務範圍被終結,RequestServices被Dispose。異步
對於咱們演示的實例來講,注入到HomeController構造函數中的IServiceProvider是RequestServices,因爲針對RequestServices的使用是在另外一個後臺線程中執行的,而且在使用的時候針對當前請求的處理已經結束(由於咱們人爲等待了100毫秒),天然就會出現上圖所示的異常。async
既然與請求綁定的RequestServices不能用,咱們只能使用與應用綁定的ApplicationServices,那麼後者如何獲得呢?ASP.NET Core 3採用了基於IHost/IHostBuilder的承載方式,表示宿主的IHost接口具備以下所示的Services屬性,它返回的正式咱們所需的ApplicationServices。ide
public interface IHost : IDisposable { Task StartAsync(CancellationToken cancellationToken = new CancellationToken()); Task StopAsync(CancellationToken cancellationToken = new CancellationToken()); IServiceProvider Services { get; } }
對於咱們演示的程序來講,咱們能夠採用以下的方式在HomeController的構造中注入IHost服務的方式間接地得到這個ApplicationServices對象。函數
public class HomeController: Controller { private readonly IServiceProvider _applicationServices; public HomeController(IHost host) { _applicationServices = host.Services; } [HttpGet("/")] public IActionResult Index() { Task.Run(async() => { try { await Task.Delay(100); var foobar = _applicationServices.GetRequiredService<IFoobar>(); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } }); return Ok(); } }
當咱們採用如上的方式將RequestServices替換成ApplicationServices以後,咱們的問題是否就解決了呢?在採用上面相同的方式進行測試以後,咱們會發現服務端控制檯上出現了以下所示的錯誤消息。測試
上面的問題是由咱們試圖利用一個表明「根容器」的IServiceProvider對象去解析一個生命週期模式爲Scoped服務實例致使,具體的緣由在《依賴注入[8]:服務實例的生命週期》已經講得很清楚了。爲了解決這個問題,咱們應該根據ApplicationServices建立一個「服務範圍」,並在該服務範圍內提取咱們所需的服務實例。爲了確保服務實例可以被正常回收,咱們還應該將表明服務範圍的IServiceScope對象及時終結掉。以下所示的是正確的編程方式。
public class HomeController: Controller { private readonly IServiceProvider _applicationServices; public HomeController(IHost host) { _applicationServices = host.Services; } [HttpGet("/")] public IActionResult Index() { Task.Run(async() => { await Task.Delay(100); using (var scope = _applicationServices.CreateScope()) { var foobar = scope.ServiceProvider.GetRequiredService<IFoobar>(); } }); return Ok(); } }
以前咱們將問題的解決方案落實在如何獲取與當前應用具備相同生命週期的ApplicationServices上,因此咱們採用注入IHost的方式獲得這個ApplicationServices。若是採用傳統的基於IWebHost/IWebHostBuilder的承載方式,IHost天然是獲取不到了。可是咱們是真的須要這個ApplicationServices對象嗎?其實不是,咱們真正須要的是利用它建立一個表明服務範圍的IServiceScope對象,並在該範圍內消費咱們所需的服務實例。因爲IServiceScope是經過IServiceScopeFactory建立的,因此咱們只須要注入IServiceScopeFactory便可。
public class HomeController : Controller { private readonly IServiceScopeFactory _serviceScopeFactory; public HomeController(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } [HttpGet("/")] public IActionResult Index() { Task.Run(async () => { await Task.Delay(100); using (var scope = _serviceScopeFactory.CreateScope()) { var foobar = scope.ServiceProvider.GetRequiredService<IFoobar>(); } }); return Ok(); } }
原文出處:https://www.cnblogs.com/artech/p/async-di.html