ASP.NET Core 3框架揭秘之 異步線程無法使用IServiceProvider問題
標(biāo)題反映的是上周五一個同事咨詢我的問題,我覺得這是一個很好的問題。這個問題有助于我們深入理解依賴注入框架在ASP.NET Core中的應(yīng)用,以及服務(wù)實例的生命周期。
一、問題重現(xiàn)
我們通過一個簡單的實例來模擬該同事遇到的問題。我們采用極簡的方式創(chuàng)建了如下這個ASP.NET Core MVC應(yīng)用。如下面的代碼片段所示,除了注冊與ASP.NET Core MVC框架相關(guān)的服務(wù)與中間件之外,我們還調(diào)用了IHostBuilder的UseDefaultServiceProvider方法將配置選項ServiceProviderOptions的ValidateScopes屬性設(shè)置為True,以開啟針對服務(wù)范圍的驗證。我們還采用Scoped生命周期模式注冊了服務(wù)IFoobar,具體的實現(xiàn)類型Foobar還實現(xiàn)了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();"); }
我們創(chuàng)建了如下這個HomeController,它的構(gòu)造函數(shù)中注入了一個IServiceProvider對象。在Action方法Index中,我們調(diào)用Task的靜態(tài)方法Run異步執(zhí)行了一些操作。具體來說,在異步執(zhí)行的操作中,我們利用調(diào)用上面注入的這個IServiceProvider對象的GetRequiredService<T>方法試圖獲取一個IFoobar服務(wù)實例。由于這段操作時在一個Try/Catch中執(zhí)行的,拋出的異常消息的堆棧信息會直接輸出到控制臺上。
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(); } }
在運(yùn)行該應(yīng)用程序后,我們利用瀏覽器采用根路徑(“/”)對Action方法Index發(fā)起訪問后,服務(wù)端控制臺上會出現(xiàn)如下所示的錯誤信息。
二、ApplicationServices與RequestServices
從上圖所示的錯誤消息可以看出,問題出在我們試圖利用一個被Dispose的IServiceProvider來獲取我們所需的服務(wù)實例。我們知道,ASP.NET Core應(yīng)用在啟動和請求處理過程中所需的服務(wù)幾乎都是由代表DI容器的IServiceProvider提供的。具體來說,這里存在著兩種類型的IServiceProvider對象,一種與當(dāng)前應(yīng)用的生命周期保持一致,我們一般將其稱為ApplicationServices,另一種則是具體針對每個請求的IServiceProvider對象,我們將其稱為RequestServices。
一般來說,ApplicationServices用于提供管道構(gòu)建過程中所需的服務(wù)實例,具體請求處理過程中所需的服務(wù)實例一般由RequestServices提供。具體來說,對于接收的每一個請求,ASP.NET Core框架都會利用ApplicationServices創(chuàng)建一個代表服務(wù)范圍的IServiceScope對象,后者就是對RequestServices的封裝。在完成了針對請求的處理之后,服務(wù)范圍被終結(jié),RequestServices被Dispose。
對于我們演示的實例來說,注入到HomeController構(gòu)造函數(shù)中的IServiceProvider是RequestServices,由于針對RequestServices的使用是在另一個后臺線程中執(zhí)行的,并且在使用的時候針對當(dāng)前請求的處理已經(jīng)結(jié)束(因為我們?nèi)藶榈却?00毫秒),自然就會出現(xiàn)上圖所示的異常。
三、如何獲取ApplicationServices
既然與請求綁定的RequestServices不能用,我們只能使用與應(yīng)用綁定的ApplicationServices,那么后者如何得到呢?ASP.NET Core 3采用了基于IHost/IHostBuilder的承載方式,表示宿主的IHost接口具有如下所示的Services屬性,它返回的正式我們所需的ApplicationServices。
public interface IHost : IDisposable { Task StartAsync(CancellationToken cancellationToken = new CancellationToken()); Task StopAsync(CancellationToken cancellationToken = new CancellationToken()); IServiceProvider Services { get; } }
對于我們演示的程序來說,我們可以采用如下的方式在HomeController的構(gòu)造中注入IHost服務(wù)的方式間接地獲得這個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(); } }
當(dāng)我們采用如上的方式將RequestServices替換成ApplicationServices之后,我們的問題是否就解決了呢?在采用上面相同的方式進(jìn)行測試之后,我們會發(fā)現(xiàn)服務(wù)端控制臺上出現(xiàn)了如下所示的錯誤消息。
四、服務(wù)實例的生命周期
上面的問題是由我們試圖利用一個代表“根容器”的IServiceProvider對象去解析一個生命周期模式為Scoped服務(wù)實例導(dǎo)致,具體的原因在《依賴注入[8]:服務(wù)實例的生命周期》已經(jīng)講得很清楚了。為了解決這個問題,我們應(yīng)該根據(jù)ApplicationServices創(chuàng)建一個“服務(wù)范圍”,并在該服務(wù)范圍內(nèi)提取我們所需的服務(wù)實例。為了確保服務(wù)實例能夠被正常回收,我們還應(yīng)該將代表服務(wù)范圍的IServiceScope對象及時終結(jié)掉。如下所示的是正確的編程方式。
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(); } }
五、統(tǒng)一的解決方案
之前我們將問題的解決方案落實在如何獲取與當(dāng)前應(yīng)用具有相同生命周期的ApplicationServices上,所以我們采用注入IHost的方式得到這個ApplicationServices。如果采用傳統(tǒng)的基于IWebHost/IWebHostBuilder的承載方式,IHost自然是獲取不到了。但是我們是真的需要這個ApplicationServices對象嗎?其實不是,我們真正需要的是利用它創(chuàng)建一個代表服務(wù)范圍的IServiceScope對象,并在該范圍內(nèi)消費(fèi)我們所需的服務(wù)實例。由于IServiceScope是通過IServiceScopeFactory創(chuàng)建的,所以我們只需要注入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(); } }
總結(jié)
以上所述是小編給大家介紹的ASP.NET Core 3框架揭秘之 異步線程無法使用IServiceProvider問題,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
相關(guān)文章
.NET微信開發(fā)之PC 端微信掃碼注冊和登錄功能實現(xiàn)
這篇文章主要介紹了.NET微信開發(fā)之PC 端微信掃碼注冊和登錄功能實現(xiàn)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09OpenCV 3.1.0+VS2015開發(fā)環(huán)境配置教程
這篇文章主要為大家詳細(xì)介紹了OpenCV 3.1.0+VS2015開發(fā)環(huán)境配置教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11ASP.NET Ref和Out關(guān)鍵字區(qū)別分析
類型介紹 在幾乎所有的OOP語言中,都存在2種類型的值。2009-02-02