[ASP.NET Core 3框架揭秘] 异步线程没法使用IServiceProvider?

标题反映的是上周五一个同事咨询个人问题,我以为这是一个很好的问题。这个问题有助于咱们深刻理解依赖注入框架在ASP.NET Core中的应用,以及服务实例的生命周期。html

1、问题重现

咱们经过一个简单的实例来模拟该同事遇到的问题。咱们采用极简的方式建立了以下这个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

image

2、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毫秒),天然就会出现上图所示的异常。async

3、如何获取ApplicationServices

既然与请求绑定的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以后,咱们的问题是否就解决了呢?在采用上面相同的方式进行测试以后,咱们会发现服务端控制台上出现了以下所示的错误消息。测试

image

4、服务实例的生命周期

上面的问题是由咱们试图利用一个表明“根容器”的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(); } }

5、统一的解决方案

以前咱们将问题的解决方案落实在如何获取与当前应用具备相同生命周期的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

相关文章
相关标签/搜索