目录
- 前言
- 问题演示
- 解决问题
- 问题探究
- 请求中的IServiceProvider
- 请求中的IServiceProvider和IServiceScopeFactory
- 后续插曲
- 总结
前言
问题的起因是在帮同事解决遇到的一个问题,他的本意是在EF Core中为了解决避免多个线程使用同一个DbContext
实例的问题。但是由于对Microsoft.Extensions.DependencyInjection
体系的深度不是很了解,结果遇到了新的问题,当时整得我也有点蒙了,所以当时也没解决,而且当时快下班了,就想着第二天再解决。在地铁上,经过我一系列的思维跳跃,终于想到了问题的原因,第二天也顺利的解决了这个问题。虽然我前面说了EFCore,但是本质和EFCore没有关系,只是凑巧。解决了之后觉得这个问题是个易错题,觉得挺有意思的,便趁机记录一下。
问题演示
接下来我们还原一下当时的场景,以下代码只是作为演示,无任何具体含义,只是为了让操作显得更清晰一下,接下来就贴一下当时的场景代码
[Route("api/[controller]/[action]")] [ApiController] public class InformationController : ControllerBase { private readonly LibraryContext _libraryContext; private readonly IServiceProvider _serviceProvider; private readonly ILogger<InformationController> _logger; public InformationController(LibraryContext libraryContext, IServiceProvider serviceProvider, ILogger<InformationController> logger) { _libraryContext = libraryContext; _serviceProvider = serviceProvider; _logger = logger; } [HttpGet] public string GetFirst() { var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault(); //这里直接使用了Task方式 Task.Run(() => { try { //Task里创建了新的IServiceScope using var scope = _serviceProvider.CreateScope(); //通过IServiceScope创建具体实例 LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>(); var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList(); } catch (Exception ex) { _logger.LogError(ex.Message, ex); } }); return caseInfo.Title; } }
再次强调一下,上述代码纯粹是为了让演示更清晰,无任何业务含义,不喜勿喷。咱们首先看一下这段代码表现出来的意思,就是在ASP.NET Core
的项目里,在Task.Run
里使用IServiceProvider
去创建Scope的场景。如果对ASP.NET Core Controller生命周期和IServiceProvider不够了解的话,会很容易遇到这个问题,且不知道是什么原因。上述这段代码会偶现
一个错误
Cannot access a disposed object.
Object name: 'IServiceProvider'.
这里为什么说是偶现呢?因为会不会出现异常完全取决于Task.Run
里的代码是在当前请求输出之前执行完成还是之后完成。说到这里相信有一部分同学已经猜到了代码报错的原因了。问题的本质很简单,是因为IServiceProvider
被释放掉了。我们知道默认情况下ASP.NET Core
为每次请求处理会创建单独的IServiceScope
,这会关乎到声明周期为Scope
对象的声明周期。所以如果Task.Run
里的逻辑在请求输出之前执行完成,那么代码运行没任何问题。如果是在请求完成之后完成再执行CreateScope
操作,那必然会报错。因为Task.Run
里的逻辑何时被执行,这个是由系统CPU调度本身决定的,特别是CPU比较繁忙的时候,这种异常会变得更加频繁。
这个问题不仅仅是在Task.Run
这种场景里,类似的本质就是在一个IServiceScope
里创建一个新的子Scope作用域的时候,这个时候需要注意的是父级的IServiceProvider
释放问题,如果父级的IServiceProvider
已经被释放了,那么基于这个Provider再去创建Scope则会出现异常。但是这个问题在结合Task
或者多线程的时候,更容易出现问题。
解决问题
既然我们知道了它为何会出现异常,那么解决起来也就顺理成章了。那就是保证当前请求执行完成之前,最好保证Task.Run
里的逻辑也要执行完成,所以我们上述的代码会变成这样
[HttpGet] public async Task<string> GetFirst() { var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault(); //这里使用了await Task方式 await Task.Run(() => { try { //Task里创建了新的IServiceScope using var scope = _serviceProvider.CreateScope(); //通过IServiceScope创建具体实例 LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>(); var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList(); } catch (Exception ex) { _logger.LogError(ex.Message, ex); } }); return caseInfo.Title; }
试一下,发现确实能解决问题,因为等待Task完成能保证Task里的逻辑能在请求执行完成之前完成。但是,很多时候我们并不需要等待Task执行完成,因为我们就是希望它在后台线程去执行这些操作,而不需要阻塞执行。
上面我们提到了本质是解决在IServiceScope
创建子Scope时遇到的问题,因为这里注入进来的IServiceProvider
本身是Scope的,只在当前请求内有效,所以基于IServiceProvider去创建IServiceScope要考虑到当前IServiceProvider是否释放。那么我们就得打破这个枷锁,我们要想办法在根容器
中去创建新的IServiceScope。这一点我大微软自然是考虑到了,在Microsoft.Extensions.DependencyInjection
体系中提供了IServiceScopeFactory
这个根容器的作用域,基于根容器创建的IServiceScope可以得到平行与当前请求作用域的独立的作用域,而不受当前请求的影响。改造上面的代码用以下形式
[Route("api/[controller]/[action]")] [ApiController] public class InformationController : ControllerBase { private readonly LibraryContext _libraryContext; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<InformationController> _logger; public InformationController(LibraryContext libraryContext, IServiceScopeFactory scopeFactory, ILogger<InformationController> logger) { _libraryContext = libraryContext; _scopeFactory = scopeFactory; _logger = logger; } [HttpGet] public string GetFirst() { var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault(); //这里直接使用了Task方式 Task.Run(() => { try { //Task里创建了新的IServiceScope using var scope = _scopeFactory.CreateScope(); //通过IServiceScope创建具体实例 LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>(); var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList(); } catch (Exception ex) { _logger.LogError(ex.Message, ex); } }); return caseInfo.Title; } }
如果你是调试起来的话你可以看到IServiceScopeFactory的具体实例是Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope
类型的,它里面包含了一个IsRootScope
属性,通过这个属性我们可以知道当前容器作用域是否是根容器作用域。当使用IServiceProvider
实例的时候IsRootScope
为false
,当使用IServiceScopeFactory
实例的时候IsRootScope
为true
。使用CreateScope
创建IServiceScope
实例的时候,注意用完了需要释放,否则可能会导致Transient
和Scope
类型的实例得不到释放。在之前的文章咱们曾提到过Transient
和Scope
类型的实例都是在当前容器作用域释放的时候释放的,这个需要注意一下。
问题探究
上面我们了解到了在每次请求的时候使用IServiceProvider
和使用IServiceScopeFactory
的时候他们作用域的实例来源是不一样的。IServiceScopeFactory
来自根容器,IServiceProvider
则是来自当前请求的Scope。顺着这个思路我们可以看一下他们两个究竟是如何的不相同。这个问题还得从构建Controller实例的时候,注入到Controller中的实例作用域的问题。
请求中的IServiceProvider
在之前的文章<ASP.NET Core Controller与IOC的羁绊>我们知道,Controller是每次请求都会创建新的实例,我们再次拿出来这段核心的代码来看一下,在DefaultControllerActivator
类的Create
方法中[点击查看源码