ASP.NET Core 3.x启动时运行异步任务(一)
這是一個大的題目,需要用幾篇文章來說清楚。這是第一篇。
?
一、前言
在我們的項目中,有時候我們需要在應(yīng)用程序啟動前執(zhí)行一些一次性的邏輯。比方說:驗證配置的正確性、填充緩存、或者運行數(shù)據(jù)庫清理/遷移等。
如何合理、有效、優(yōu)雅地完成這個任務(wù),是這個文章討論的主要內(nèi)容。
?
要實現(xiàn)這樣一個功能,其實我們有幾個選擇:
使用IStartupFilter運行同步任務(wù)。這是一個內(nèi)置的解決方案,可以通過一些設(shè)置和技巧來運行異步任務(wù);
使用IStartupFilter或IApplicationLifetime事件來運行異步任務(wù),這是一個可選的方案,但有不足,我們會在后面講;
使用IHostedService,在不阻塞應(yīng)用啟動的情況下,運行一些一次性的任務(wù);(關(guān)于這個內(nèi)容,我在前一篇文章ASP.NET Core 3.x控制IHostedService啟動順序淺探中有涉及到一部分內(nèi)容)
在Program.cs中運行異步任務(wù)。在大多數(shù)情況下,從代碼的復(fù)雜度到效率上,這都是一個比較好的選擇。
?
先提個問題:為什么要在應(yīng)用啟動時運行任務(wù)?
二、為什么要在應(yīng)用啟動時運行任務(wù)?
在應(yīng)用啟動并開始請求服務(wù)之前,很多時候需要運行各種初始化工作。
一個ASP.NET應(yīng)用啟動時,需要完成很多事,例如:
確定當前的宿主環(huán)境
加載appsetting.json配置和環(huán)境變量
配置并創(chuàng)建依賴注入的容器
配置中間件管道
這是應(yīng)用啟動時要完成的引導(dǎo)內(nèi)容。
在完成這些內(nèi)容,運行WebHost并開始監(jiān)聽請求之前,還會有一些一次性任務(wù)需要啟動,例如:
檢查強類型配置的有效性
填充或恢復(fù)緩存
數(shù)據(jù)庫清理/遷移(通常來說這不是個好主意,但很多時候沒有別的辦法)
當然,有些任務(wù)也不是一定要在開始監(jiān)聽請求之前運行,這要看具體的運行任務(wù)的架構(gòu)。一般來說,如果緩存處理的完善,是不需要提前啟動的。當然,清理/遷移數(shù)據(jù)庫,是必須放在服務(wù)啟動之前。
在微軟官網(wǎng)上,有一個例子是數(shù)據(jù)保護子系統(tǒng),用于即時加密(cookie、防偽令牌等),這個就必須在應(yīng)用監(jiān)聽請求之前完成初始化并加載,這個例子使用了IStartupFilter。
三、使用IStartupFilter運行同步任務(wù)
IStartupFilters作為配置中間件管道的一部分,通常在Startup.Configure()中運行。它允許我們定制應(yīng)用的中間件管道,處理我們希望進行的所有任務(wù)。
看一個簡單的例子:
public?class?AutoRequestServicesStartupFilter?:?IStartupFilter {public?Action<IApplicationBuilder>?Configure(Action<IApplicationBuilder>?next){return?builder?=>{builder.UseMiddleware<RequestServicesContainerMiddleware>();next(builder);};} }IStartupFilter提供了一種可能,在依賴注入容器配置完成之后、應(yīng)用程序啟動之前運行一些代碼。因此,我們可以在IStartupFilters中直接使用依賴注入。這表示我們可以運行有關(guān)系統(tǒng)的任何代碼。在前邊提到的微軟官網(wǎng)的例子中,就是創(chuàng)建了一個基于IStartupFilters的DataProtectionStartupFilter來初始化數(shù)據(jù)保護子系統(tǒng)。
此外,IStartupFilter允許我們通過向依賴注入容器注冊服務(wù)來增加要執(zhí)行的任務(wù)。這是一個很有用的特性,表示我們可以注冊一個在應(yīng)用啟動時運行的任務(wù),而不需要顯式的調(diào)用。
但是,這兒有個問題。IStartupFilters通常運行的是同步的任務(wù)。看一下上面的代碼,Configure()方法不返回任務(wù)。當然,我們硬要使用異步也是可以的,但一般來說,這不算個好主意。原因我后面會寫。
?
寫到這兒,如果對ASP.NET Core架構(gòu)熟悉,就會引出另一個問題:為什么不用健康檢查來確認一次性任務(wù)的執(zhí)行結(jié)果?
四、為什么不用健康檢查?
運行健康檢查,是ASP.NET Core 2.2新引入的一個特性,允許查詢通過API(HTTP Endpoint)公開的應(yīng)用的健康狀況。當應(yīng)用部署在Kubernetes,或反向代理HAProxy或Nginx后面時,可以提供給代理用來檢測應(yīng)用是否準備好開始提供服務(wù)。
我們可以使用健康檢查來確保應(yīng)用所有必需的一次性任務(wù)完成之前不會開始監(jiān)聽服務(wù)。
但是,這種方式會有一點問題。
WebHost和Kestrel本身會在一次性任務(wù)執(zhí)行前啟動。當然,這時他們還不會接收和處理服務(wù)請求,但仍然引出了一些問題:
首先是增加了代碼的復(fù)雜性。除了一次性任務(wù)的代碼外,還要增加健康檢查來測試任務(wù)是否完成,并同步和保持任務(wù)的狀態(tài);其次,如果任務(wù)失敗了,應(yīng)用程序的健康檢查將會讓應(yīng)用后續(xù)的任務(wù)無法繼續(xù)執(zhí)行。合理的流程是:應(yīng)用應(yīng)該立即失敗返回。
這兒主要的原因是:健康檢查沒有定義如何實際運行任務(wù),而只是定義了任務(wù)是否成功完成。相對來說,這種狀態(tài)機制比較單一,在一些簡單的任務(wù)中可能適用,但不能全面覆蓋一次性任務(wù)的全部場景。
五、運行異步任務(wù)
前邊寫了一些不太完美的方法。
現(xiàn)在,我們開始進入運行異步方法的一些步驟。當然,運行異步也會有幾種方式,適用性上會有一定的區(qū)別。
方式1:使用IStartupFilter
前邊說過,使用IStartupFilter時,執(zhí)行的是同步任務(wù)。所以,我們可以通過GetAwater().GetResult()來調(diào)用異步。
?
我們拿數(shù)據(jù)遷移來舉個例子。在EF Core中,通過myDBContext.database.migrateasync()在運行時進行數(shù)據(jù)庫遷移。其中,myDBContext是應(yīng)用程序中DBContext的一個實例。
public?class?MigratorStartupFilter:?IStartupFilter {private?readonly?IServiceProvider?_serviceProvider;public?MigratorStartupFilter(IServiceProvider?serviceProvider){_serviceProvider?=?serviceProvider;}public?Action<IApplicationBuilder>?Configure(Action<IApplicationBuilder>?next){using(var?scope?=?_seviceProvider.CreateScope()){var?myDbContext?=?scope.ServiceProvider.GetRequiredService<MyDbContext>();myDbContext.Database.MigrateAsync().GetAwaiter().GetResult();}return?next;} }通常,GetAwaiter().GetResult()要注意避免死鎖的問題。但這兒可能不需要,因為這個代碼只在啟動時運行,這時候還沒有需要處理的請求,所以不太會死鎖。
只能說,這樣可以用。不過習慣上我會避免這么做。
方式2:使用IApplicationLifetime事件
這是另一個選擇。可以通過IApplicationLifetime事件,在應(yīng)用啟動和關(guān)閉時接收通知,處理任務(wù)。
但這個方式也有局限性。
首先,IApplicationLifetime使用cancellationtoken來注冊回調(diào),也就是說,這又是一個同步方式,又需要使用GetAwaiter().GetResult()來調(diào)用異步。
其次,ApplicationStarted事件是在WebHost啟動之后才會觸發(fā),因此異步任務(wù)也是在應(yīng)用開始監(jiān)聽請求后才運行。
方式3:使用IHostedService
IHostedService可以讓ASP.NET Core應(yīng)用在后臺執(zhí)行長時間的任務(wù)。
一般來說,IHostedService用在周期性任務(wù)、消息傳遞等任務(wù)上,但實際上它并不限于運行這些任務(wù)。在ASP.NET Core 3.x上,WebHost本身也是建立在IHostedService上的。
而且,IHostedService本身就是異步的,它提供了StartAsync和StopAsync。
這種方式下,我們的代碼會是這樣:
public?class?MigratorHostedService:?IHostedService {private?readonly?IServiceProvider?_serviceProvider;public?MigratorStartupFilter(IServiceProvider?serviceProvider){_serviceProvider?=?serviceProvider;}public?async?Task?StartAsync(CancellationToken?cancellationToken){using(var?scope?=?_seviceProvider.CreateScope()){var?myDbContext?=?scope.ServiceProvider.GetRequiredService<MyDbContext>();await?myDbContext.Database.MigrateAsync();}}public?Task?StopAsync(CancellationToken?cancellationToken){return?Task.CompletedTask;} }根據(jù)例子可以看出,IHostedService可以直接運行異步任務(wù)。
但是,IHostedService也有局限性。從微軟官網(wǎng)的說明來看,IHostedService實現(xiàn)期望StartAsync能相對較快的返回。對于后臺任務(wù),傾向于異步啟動,但主要任務(wù)在啟動后執(zhí)行。
在上面這個例子中,數(shù)據(jù)遷移本身不是問題,但這個長時任務(wù)會阻止其它`IHostedService啟動和運行。而且,應(yīng)用會在IHostedService完成數(shù)據(jù)遷移前開始監(jiān)聽并響應(yīng)請求,這是一個嚴重的問題。
方式4:在Program.cs中運行
上面三個方式,都可以解決啟動時運行異步任務(wù)的問題,但都不夠完美,要么要求使用同步(異步轉(zhuǎn)同步可以用,但有隱藏問題),要么不能阻止應(yīng)用啟動,會造成應(yīng)用啟動完成后,可能異步任務(wù)還未完成的情況。
我在前邊的博文中寫到過關(guān)于Program.cs中運行IHostedService的方式。具體可以去看ASP.NET Core 3.x控制IHostedService啟動順序淺探
看一下Program.cs的默認代碼:
public?class?Program {public?static?void?Main(string[]?args){CreateWebHostBuilder(args).Build().Run();}public?static?IWebHostBuilder?CreateWebHostBuilder(string[]?args)?=>WebHost.CreateDefaultBuilder(args).UseStartup<Startup>(); }在Build()創(chuàng)建WebHost之后,調(diào)用Run()之前,完全可以加入我們需要的代碼。同時,C# 7.1后主函數(shù)可以改為異步運行。
因此,我們可以在這兒做些文章:
public?class?Program {public?static?async?Task?Main(string[]?args){IWebHost?webHost?=?CreateWebHostBuilder(args).Build();using?(var?scope?=?webHost.Services.CreateScope()){var?myDbContext?=?scope.ServiceProvider.GetRequiredService<MyDbContext>();await?myDbContext.Database.MigrateAsync();}await?webHost.RunAsync();}public?static?IWebHostBuilder?CreateWebHostBuilder(string[]?args)?=>WebHost.CreateDefaultBuilder(args).UseStartup<Startup>(); }這個方案的好處是:
這是真正的異步;
任務(wù)完成后,應(yīng)用程序才可以監(jiān)聽并接受請求;
此時已經(jīng)構(gòu)建了依賴注入容器,所以可以創(chuàng)建服務(wù);
當然,同樣也會有不足:這兒只是構(gòu)建了DI容器,但并沒有建立管道(管道在Run()、RunAsync()后才建立,然后是IStartupFilters執(zhí)行,再然后是應(yīng)用程序啟動)。因此異步任務(wù)不能使用管道、IStartupFilters中的配置。不過,這種需求的情況很少。
六、總結(jié)
這個部分牽扯到的框架內(nèi)容比較多。
我們從應(yīng)用啟動時異步運行任務(wù)開始,說到了必要性,也說到了幾種解決方法,及各自的優(yōu)缺點。
下一篇文章,我會用一些具體的例子,來說清楚這個方式的具體使用,敬請關(guān)注。
(未完待續(xù))
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core 3.x启动时运行异步任务(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core 下使用 Rabbit
- 下一篇: Swagger扩展为你添油加气