[ASP.NET Core 3框架揭秘] 異步線程無法使用IServiceProvider?
- 2019 年 12 月 2 日
- 筆記
一、問題重現
我們通過一個簡單的實例來模擬該同事遇到的問題。我們採用極簡的方式創建了如下這個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發起訪問後,服務端控制台上會出現如下所示的錯誤信息。

二、ApplicationServices與RequestServices
從上圖所示的錯誤消息可以看出,問題出在我們試圖利用一個被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毫秒),自然就會出現上圖所示的異常。
三、如何獲取ApplicationServices
既然與請求綁定的RequestServices不能用,我們只能使用與應用綁定的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的構造中注入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(); } }