前言#
问题的起因是在帮同事解决遇到的一个问题,他的本意是在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
方法中[点击查看源码