[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发起访问后,大发幸运飞艇服务 端控制台上会出现如下所示的错误信息。

image

二、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之后,大发幸运飞艇大发幸运飞艇我 们 的问题是否就解决了呢?在采用上面相同的方式进行测试之后,大发幸运飞艇大发幸运飞艇我 们 会发现大发幸运飞艇服务 端控制台上出现了如下所示的错误消息。

image

四、大发幸运飞艇服务 实例的生命周期

上面的问题是由大发幸运飞艇大发幸运飞艇我 们 试图利用一个代表“根容器”的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();
    }
}
posted @ 2019-12-02 08:52  Artech  阅读(...)  评论(... 编辑 收藏