[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(); } }
- [ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”
- [ASP.NET Core 3框架揭秘] 文件系统[2]:总体设计
- Asp.Net Core 中无法使用 ConfigurationManager.AppSettings
- (WebSite----Asp.Net Configuration----->无法连接到SQL Server数据库------>选择数据存储区---->应用程序当前被配置为使用提供程序:AspNetSqlProvider)解决方案
- ASP.NET Core使用HttpClient的同步和异步请求
- [ASP.NET Core 3框架揭秘] 文件系统[3]:物理文件系统
- asp.net core使用jexus部署在linux无法正确 获取远程ip的解决办法
- EF Core使用SQL调用返回其他类型的查询 ASP.NET Core 2.0 使用NLog实现日志记录 CSS 3D transforms cSharp:use Activator.CreateInstance with an Interface? SqlHelper DBHelper C# Thread.Abort方法真的让线程停止了吗? 注意!你的Thread.Abort方法真
- 关于ASP.NET 中使用Ajax进行异步调用问题,前台参数无法跳转到后台,提示500 internal server error
- (WebSite----Asp.Net Configuration----->无法连接到SQL Server数据库------>选择数据存储区---->应用程序当前被配置为使用提供程序:AspNetSqlProvider)解决方案
- ASP.NET Core 新建线程中使用依赖注入的问题
- C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志
- ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【解读ServiceCallSite 】
- ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】
- ASP.NET Boilerplate 学习 AspNet Core2 浏览器缓存使用 c#基础,单线程,跨线程访问和线程带参数 wpf 禁用启用webbroswer右键菜单 EF Core 2.0使用MsSql/MySql实现DB First和Code First ASP.NET Core部署到Windows IIS QRCode.js:使用 JavaScript 生成
- [ASP.NET Core 3框架揭秘] 依赖注入[5]: 利用容器提供服务
- 如何在ASP.NET页面中使用异步任务(PageAsyncTask)
- Asp.net Mvc中MVCContrib中无法使用Castle的发解决方案
- MattPowell介绍了如何在服务器端使用异步Web方法,来创建高性能的 Microsoft ASP.NET Web 服务
- Asp.Net 4.0 新特性之 使用自定义OutputCache Provider