前言
????問題的起因是在幫同事解決遇到的一個問題,他的本意是在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里創建了新的IServiceScopeusing?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里創建了新的IServiceScopeusing?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里創建了新的IServiceScopeusing?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的羈絆>[1]我們知道,Controller是每次請求都會創建新的實例,我們再次拿出來這段核心的代碼來看一下,在DefaultControllerActivator
類的Create
方法中[點擊查看源碼👈[2]]
internal?class?DefaultControllerActivator?:?IControllerActivator
{private?readonly?ITypeActivatorCache?_typeActivatorCache;public?DefaultControllerActivator(ITypeActivatorCache?typeActivatorCache){_typeActivatorCache?=?typeActivatorCache;}public?object?Create(ControllerContext?controllerContext){//省略一系列判斷代碼var?serviceProvider?=?controllerContext.HttpContext.RequestServices;//這里傳遞的IServiceProvider本質就是來自HttpContext.RequestServicesreturn?_typeActivatorCache.CreateInstance<object>(serviceProvider,?controllerTypeInfo.AsType());}
}
通過這個方法我們可以看到創建Controller實例時,如果存在構造依賴,本質則是通過HttpContext.RequestServices
實例創建出來的,而它本身就是IServiceProvider
的實例,ITypeActivatorCache
實例中則存在真正創建Controller實例的邏輯,具體可以查看TypeActivatorCache
類的實現[點擊查看源碼👈[3]]
internal?class?TypeActivatorCache?:?ITypeActivatorCache
{private?readonly?Func<Type,?ObjectFactory>?_createFactory?=(type)?=>?ActivatorUtilities.CreateFactory(type,?Type.EmptyTypes);private?readonly?ConcurrentDictionary<Type,?ObjectFactory>?_typeActivatorCache?=new?ConcurrentDictionary<Type,?ObjectFactory>();public?TInstance?CreateInstance<TInstance>(IServiceProvider?serviceProvider,Type?implementationType){//省略一系列判斷代碼var?createFactory?=?_typeActivatorCache.GetOrAdd(implementationType,?_createFactory);//創建Controller的時候,需要的依賴實例都是來自IServiceProviderreturn?(TInstance)createFactory(serviceProvider,?arguments:?null);}
}
其實在這里我們就可以得到一個結論,我們在當前請求默認通過構造注入的IServiceProvider
的實例其實就是HttpContext.RequestServices
,也就是針對當前請求的作用域有效,同樣的是來自當前作用域的Scope
周期的對象實例也是在當前請求結束就會釋放。驗證這個很簡單可以寫個demo來演示一下
[Route("api/[controller]/[action]")]
[ApiController]
public?class?InformationController?:?ControllerBase
{private?readonly?IServiceProvider?_serviceProvider;public?InformationController(IServiceProvider?serviceProvider){_serviceProvider?=?serviceProvider;}[HttpGet]public?bool[]?JudgeScope([FromServices]IServiceProvider?scopeProvider){//比較構造注入的和在HttpContext獲取的bool?isEqualOne?=?_serviceProvider?==?HttpContext.RequestServices;//比較通過Action綁定的和在HttpContext獲取的bool?isEqualTwo?=?scopeProvider?==?HttpContext.RequestServices;return?new[]?{?isEqualOne,?isEqualTwo?};}
}
毫無疑問,默認情況
下isEqualOne和isEqualTwo的結構都是true
,這也驗證了我們上面的結論。因此在當前請求默認注入IServiceProvider
實例的時候,都是來自HttpContext.RequestServices
的實例。
請求中的IServiceProvider和IServiceScopeFactory
上面我們看到了在當前請求中獲取IServiceProvider
實例本身就是Scope的,而且在當前請求中通過各種注入方式獲取到的實例都是相同的。那么接下來我們就可以繼續跟蹤,本質的HttpContext.RequestServices
的IServiceProvider到底來自什么地方呢?我們找到HttpContext默認的實現類DefaultHttpContext
中關于RequestServices
屬性的定義[點擊查看源碼👈[4]]
//接受
public?IServiceScopeFactory?ServiceScopeFactory?{?get;?set;?}?=?default!;
//數據來自RequestServicesFeature
private?static?readonly?Func<DefaultHttpContext,?IServiceProvidersFeature>?_newServiceProvidersFeature?=?context?=>?new?RequestServicesFeature(context,?context.ServiceScopeFactory);
//緩存來自_newServiceProvidersFeature
private?IServiceProvidersFeature?ServiceProvidersFeature?=>_features.Fetch(ref?_features.Cache.ServiceProviders,?this,?_newServiceProvidersFeature)!;
//數據來自ServiceProvidersFeature的RequestServices
public?override?IServiceProvider?RequestServices
{get?{?return?ServiceProvidersFeature.RequestServices;?}set?{?ServiceProvidersFeature.RequestServices?=?value;?}
}
通過上面的源碼我們可以看到HttpContext.RequestServices
的數據最終來自RequestServicesFeature
類的RequestServices屬性,我們可以直接找到RequestServicesFeature
類的定義[點擊查看源碼👈[5]]
public?class?RequestServicesFeature?:?IServiceProvidersFeature,?IDisposable,?IAsyncDisposable
{private?readonly?IServiceScopeFactory??_scopeFactory;private?IServiceProvider??_requestServices;private?IServiceScope??_scope;private?bool?_requestServicesSet;private?readonly?HttpContext?_context;public?RequestServicesFeature(HttpContext?context,?IServiceScopeFactory??scopeFactory){_context?=?context;_scopeFactory?=?scopeFactory;}public?IServiceProvider?RequestServices{get{if?(!_requestServicesSet?&&?_scopeFactory?!=?null){//釋放掉之前沒釋放掉的RequestServicesFeature實例_context.Response.RegisterForDisposeAsync(this);//通過IServiceScopeFactory創建Scope_scope?=?_scopeFactory.CreateScope();//RequestServices來自IServiceScopeFactory的CreateScope實例_requestServices?=?_scope.ServiceProvider;//填充已經設置了RequestServices的標識_requestServicesSet?=?true;}return?_requestServices!;}set{_requestServices?=?value;_requestServicesSet?=?true;}}//釋放的真實邏輯public?ValueTask?DisposeAsync(){switch?(_scope){case?IAsyncDisposable?asyncDisposable:var?vt?=?asyncDisposable.DisposeAsync();if?(!vt.IsCompletedSuccessfully){return?Awaited(this,?vt);}vt.GetAwaiter().GetResult();break;case?IDisposable?disposable:disposable.Dispose();break;}//釋放時重置相關屬性_scope?=?null;_requestServices?=?null;return?default;static?async?ValueTask?Awaited(RequestServicesFeature?servicesFeature,?ValueTask?vt){await?vt;servicesFeature._scope?=?null;servicesFeature._requestServices?=?null;}}//IDisposable的Dispose的方法,通過using可隱式調用public?void?Dispose(){DisposeAsync().AsTask().GetAwaiter().GetResult();}
}
通過上面的兩段源碼,我們得到了許多關于IServiceProvider和IServiceScopeFactory的相關信息。
??DefaultHttpContext的RequestServices值來自于RequestServicesFeature實例的RequestServices屬性
??RequestServicesFeature的RequestServices屬性的值通過IServiceScopeFactory通過CreateScope創建的
??構建RequestServicesFeature的IServiceScopeFactory值來自于DefaultHttpContext的ServiceScopeFactory屬性
那么接下來我們直接可以找到DefaultHttpContext的ServiceScopeFactory屬性是誰給它賦的值,我們找到創建HttpContext的地方,在DefaultHttpContextFactory的Create方法里[點擊查看源碼👈[6]]
public?class?DefaultHttpContextFactory?:?IHttpContextFactory
{private?readonly?IHttpContextAccessor??_httpContextAccessor;private?readonly?FormOptions?_formOptions;private?readonly?IServiceScopeFactory?_serviceScopeFactory;public?DefaultHttpContextFactory(IServiceProvider?serviceProvider){_httpContextAccessor?=?serviceProvider.GetService<IHttpContextAccessor>();_formOptions?=?serviceProvider.GetRequiredService<IOptions<FormOptions>>().Value;//通過IServiceProvider的GetRequiredService直接獲取IServiceScopeFactory_serviceScopeFactory?=?serviceProvider.GetRequiredService<IServiceScopeFactory>();}//創建HttpContext實例的方法public?HttpContext?Create(IFeatureCollection?featureCollection){if?(featureCollection?is?null){throw?new?ArgumentNullException(nameof(featureCollection));}var?httpContext?=?new?DefaultHttpContext(featureCollection);Initialize(httpContext);return?httpContext;}private?DefaultHttpContext?Initialize(DefaultHttpContext?httpContext){//IHttpContextAccessor也是在這里賦的值if?(_httpContextAccessor?!=?null){_httpContextAccessor.HttpContext?=?httpContext;}httpContext.FormOptions?=?_formOptions;//DefaultHttpContext的ServiceScopeFactory屬性值來自注入的IServiceProviderhttpContext.ServiceScopeFactory?=?_serviceScopeFactory;return?httpContext;}
}
這里我們可以看到IServiceScopeFactory的實例來自于通過DefaultHttpContextFactory注入的IServiceProvider實例,這里獲取IServiceScopeFactory的地方并沒有CreateScope,所以這里的IServiceScopeFactory
和IServiceProvider
中的實例都是來自根容器
。這個我們還可以通過注冊DefaultHttpContextFactory
地方看到 [點擊查看源碼👈[7]]
services.TryAddSingleton<IHttpContextFactory,?DefaultHttpContextFactory>();
通過這里可以看到DefaultHttpContextFactory
注冊的是單例模式,注冊它的地方則是在IHostBuilder
的ConfigureServices
方法里。關于每次請求的創建流程,不是本文的重點,但是為了讓大家對本文講解的IServiceScopeFactory
和IServiceProvider
來源更清楚,咱們可以大致的描述一下
??
GenericWebHostService
類實現自IHostedService
,在StartAsync
方法中啟動了IServer
實例,默認則是啟動的Kestrel。??
IServer
啟動的方法StartAsync
中會傳遞HostingApplication
實例,構建HostingApplication
實例的時候則會依賴IHttpContextFactory
實例,而IHttpContextFactory
實例則是在構建GenericWebHostService
服務的時候注入進來的。??當每次請求ASP.NET Core服務的時候會調用
HostingApplication
的CreateContext
方法,該方法中則會創建HttpContext
實例,每次請求結束后則調用該類的DisposeContext
釋放HttpContext
實例。
說了這么多其實就是為了方便讓大家得到一個關系,即在每次請求中獲取的IServiceProvider
實例來自HttpContext.RequestServices
實例,HttpContext.RequestServices
實例來自IServiceScopeFactory
來自CreateScope
方法創建的實例,而IServiceScopeFactory
實例則是來自根容器,且DefaultHttpContextFactory
的生命周期則和當前ASP.NET Core保持一致。
后續插曲
就在解決這個問題后不久,有一次不經意間翻閱微軟的官方文檔,發現官方文檔有提到相關的問題,而且也是結合efcore
來講的。標題是《Do not capture services injected into the controllers on background threads》[8]翻譯成中文大概就是不要在后臺線程上捕獲注入控制器的服務,說的正是這個問題,微軟給我們的建議是
??注入一個IServiceScopeFactory以便在后臺工作項中創建一個范圍。
??IServiceScopeFactory是一個單例對象。
??在后臺線程中創建一個新的依賴注入范圍。
??不引用控制器中的任何東西。
??不從傳入請求中捕獲DbContext。
得到的結論和我們在本文描述的基本上是差不多的,而且微軟也很貼心的給我們提供了相關示例
[HttpGet("/fire-and-forget-3")]
public?IActionResult?FireAndForget3([FromServices]IServiceScopeFactory?serviceScopeFactory)
{_?=?Task.Run(async?()?=>{await?Task.Delay(1000);using?(var?scope?=?serviceScopeFactory.CreateScope()){var?context?=?scope.ServiceProvider.GetRequiredService<ContosoDbContext>();context.Contoso.Add(new?Contoso());await?context.SaveChangesAsync();????????????????????????????????????????}});return?Accepted();
}
原來還是自己的坑自己最了解,也不得不說微軟現在的文檔確實挺詳細的,同時也提醒我們有空還是得多翻一翻文檔避免踩坑。
總結
????本文主要是通過幫助同事解決問題而得到的靈感,覺得挺有意思的,希望能幫助更多的人了解這個問題,且能避免這個問題。我們應該深刻理解ASP.NET Core處理每次請求則都會創建一個Scope
,這會影響當前請求獲取的IServiceProvider
實例,和通過IServiceProvider
創建的生命周期為Scope
的實例。如果把握不住,則可以理解為當前請求直接注入的服務,和當前服務直接注入的IServiceProvider實例。如果想獲取根容器的實例則可以通過獲取IServiceScopeFactory
實例獲取,最后請注意IServiceScope
的釋放問題。 ????曾幾何時,特別喜歡去解決遇到的問題,特別喜歡那種解決問題沉浸其中的過程。解決了問題,了解到為什么會讓自己感覺很通透,也更深刻,不經意間的也擴展了自己的認知邊界。這個過程得到的經驗是一種通識,是一種意識。而思維和意識則是我們適應這個不斷在變化時代的底層邏輯。
引用鏈接
[1]
?<ASP.NET Core Controller與IOC的羈絆>:?https://www.cnblogs.com/wucy/p/14222973.html[2]
?點擊查看源碼👈:?https://github.com/dotnet/aspnetcore/blob/v6.0.7/src/Mvc/Mvc.Core/src/Controllers/DefaultControllerActivator.cs#L56[3]
?點擊查看源碼👈:?https://github.com/dotnet/aspnetcore/blob/v6.0.7/src/Mvc/Mvc.Core/src/Infrastructure/TypeActivatorCache.cs#L38[4]
?點擊查看源碼👈:?https://github.com/dotnet/aspnetcore/blob/v6.0.7/src/Http/Http/src/DefaultHttpContext.cs#L176[5]
?點擊查看源碼👈:?https://github.com/dotnet/aspnetcore/blob/v6.0.7/src/Http/Http/src/Features/RequestServicesFeature.cs#L33[6]
?點擊查看源碼👈:?https://github.com/dotnet/aspnetcore/blob/v6.0.7/src/Hosting/Hosting/src/Http/DefaultHttpContextFactory.cs#L45[7]
?點擊查看源碼👈:?https://github.com/dotnet/aspnetcore/blob/v6.0.7/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs#L93[8]
?《Do not capture services injected into the controllers on background threads》:?https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-6.0#do-not-capture-services-injected-into-the-controllers-on-background-threads