問題的起因是在幫同事解決遇到的一個問題,他的本意是在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中的範例作用域的問題。
在之前的文章<ASP.NET Core Controller與IOC的羈絆>我們知道,Controller是每次請求都會建立新的範例,我們再次拿出來這段核心的程式碼來看一下,在DefaultControllerActivator
類的Create
方法中[點選檢視原始碼