1. 程式人生 > >[ASP.NET Core 3框架揭祕] 非同步執行緒無法使用IServiceProvider?

[ASP.NET Core 3框架揭祕] 非同步執行緒無法使用IServiceProvider?

標題反映的是上週五一個同事諮詢我的問題,我覺得這是一個很好的問題。這個問題有助於我們深入理解依賴注入框架在ASP.NET Core中的應用,以及服務例項的生命週期。

一、問題重現

我們通過一個簡單的例項來模擬該同事遇到的問題。我們採用極簡的方式建立瞭如下這個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();
    }
}