原文:Running async tasks on app startup in ASP.NET Core (Part 1)
做者:Andrew Lock
譯者:Lamond Luhtml
當咱們作項目的時候,有時候但願本身的ASP.NET Core應用在啓動前執行一些初始化邏輯。例如,你但願驗證配置是否合法,填充緩存數據,或者運行數據庫遷移腳本。在本篇博客中,我將介紹幾種可選的方案,而且經過展現一些簡單的方法和擴展點來講明我想要解決的問題。git
開始我將先描述一下ASP.NET Core內置的解決方案,使用IStartupFilter
來運行同步任務。而後我將描述幾種可選的執行異步任務的方案。你能夠(可是可能不該該這樣作)使用IStartupFilter
或者IApplicationLifetime
事件來執行異步任務。你也可使用IHostService
接口來運行一次性任務且不會阻塞ASP.NET Core應用啓動。最後惟一合理的方案是在program.cs
文件中手動運行任務。在下一篇博客中,我會展現一個能夠簡化這個流程的推薦方案。github
在程序啓動,開始監聽請求以前,運行一些初始化代碼是很是廣泛的。對於一個ASP.NET Core應用程序,啓動前有許多任務須要運行,例如:web
以上幾步都四發生在應用程序引導時。然而有些一次性任務須要在WebHost
啓動,監聽請求前運行。例如數據庫
有些時候,一些任務並非非要在程序啓動,監聽請求前運行。這裏咱們以填充緩存爲例,若是它是設計的比較好的話,在程序啓動前是否填充緩存數據是可有可無的。可是,相對的,你確定也但願在應用程序開始監聽請求以前,遷移你的數據庫!json
其實ASP.NET Core框架本身也須要運行一些一次性初始化任務。這個最好的例子就是數據保護,它經常使用來作數據加密,這個模塊必需要在應用啓動前初始化。爲了實現初始化,它們使用了IStartupFilter
。c#
IStartupFilter
來運行同步任務在以前的博客中,我已經介紹過IStartupFilter
, 它是一個自定義ASP.NET Core應用的強力接口。緩存
若是你是第一次接觸Filter, 我建議你去我以前的博客,這裏我只會提供一個簡短的總結。
IStartupFilter
會在配置中間件管道的進程中被執行(一般在Startup.Configure()
中完成)。它們容許你經過插入額外的中間件,分叉或執行任何其餘操做來自定義應用程序實際建立的中間件管道。例以下面代碼展現的AutoRequestServiceStartupFilter
public class AutoRequestServicesStartupFilter : IStartupFilter { public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) { return builder => { builder.UseMiddleware<RequestServicesContainerMiddleware>(); next(builder); }; } }
這很是有用,但它與ASP.NET Core應用程序啓動時運行一次性任務有什麼關係呢?
IStartupFilter
的主要功能是爲開發人員提供了一個鉤子(hook), 這個鉤子觸發的時機是在在應用程序配置完成並配置依賴注入容器以後,應用程序啓動以前。這意味着,你能夠在實現IStartupFilter
的類中使用依賴注入,這樣你就能夠在這裏完成許多但願在應用程序啓用前須要運行的任務。以ASP.NET Core內置的DataProtectionStartupFilter爲例,它會在程序啓用前初始化整個數據保護模塊。
IStartupFilter
提供的另一個重要功能就是,它容許你經過向依賴注入容器註冊服務來添加要執行的任務。這意味着若是你本身編寫了一個Library, 你能夠在應用程序啓動時註冊一個任務,而不須要應用程序顯式調用它。
問題是IStartupFilter
基本上是同步的。Configure
方法的返回值不是Task
,所以咱們只能使用同步方式執行異步任務,這顯然不是好的實現方案。 我稍後會討論這個,但如今讓咱們先跳過它。
ASP.NET Core 2.2中加入了一個新的健康檢查功能,它經過暴露一個HTTP節點,讓你能夠查詢當前應用的健康狀態。當應用部署以後,像Kubernetes這樣的編排引擎或HAProxy和NGINX等反向代理能夠查詢此HTTP節點以檢查你應用是否已準備好開始接收請求。
你可使用健康檢查功能來確保你的應用程序不會開始處理請求,直到全部必需的一次性初始化任務完成爲止。然而,這有一些缺點:
在我看來,健康檢查並不適合一次性任務的場景,他們可能對我描述的一些例子頗有用,但我不認爲它適用於全部狀況。我真的但願能在WebHost
啓動以前,運行一些一次性任務。
我已經花了很長的篇幅來討論了全部不能完成個人目標的全部方法,那麼哪些纔是可行的方案!在這一節中,我將描述幾種運行異步任務的方案(即方法返回Task
, 而且須要等待的),其中有一些較好的方案,也有一些須要規避的方案。
這裏爲了更清楚的描述這些方案,我選用數據庫遷移做爲例子。在EF Core中,你能夠在運行時調用myDbContext.Database.MigrateAsync()
來遷移數據庫,其中myDbContext
是當前應用程序的數據庫上下文實例。
EF還提供了一個同步的數據庫遷移方法
Database.Migrate()
,可是這裏咱們不須要使用它。
IStartupFilter
我以前描述過如何使用IStartupFilter
在應用程序啓動時運行同步任務。 不過,這裏爲了異步方法,咱們使用了GetAwaiter()
和GetResult()
阻塞了線程, 將異步方法變成了一個同步方法。
警告:這是一種很是很差的異步實踐方式
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; } }
這段代碼可能不會引發任何問題,它會在應用程序啓動且未開始監聽請求時運行,因此不太可能出現死鎖。可是坦率的說,我會盡量不用這種方式。
IApplicationLifetime
事件我以前尚未討論過和這個事件相關的內容,可是當你的應用程序啓動和關閉前,你可使用IApplicationLifetime
接口接收到通知。這裏我不會詳細介紹它,由於使用它來實現咱們的目的會有一些問題。
IApplicationLifetime
使用CancellationTokens
來註冊回調,這意味着你只能同步執行回調。 這實際上意味着不管你作什麼,你都會遇到同步異步模式。
ApplicationStarted事件僅在WebHost啓動後觸發,所以任務在應用程序開始接受請求後運行。
鑑於他們沒有解決IStartupFilter
使用同步方式處理異步任務的問題,也沒有阻止應用啓動,因此我只是將它列出來僅供參考。
IHostedService
運行異步事件IHostService
容許在ASP.NET Core應用程序生命週期內,之後臺程序的方式執行長時間運行的任務。它有許多不一樣的用途,你可使用它在計數器上運行按期任務,或者監聽RabbitMQ消息。在ASP.NET Core 3.0中, Web Host也多是使用IHostService
構建的。
IHostService
本質上是異步的,他提供了StartAsync
和StopAsync
方法。這對咱們來講很是的有用,它再也不是使用同步方式處理異步任務了。使用IHostService
,咱們的數據庫遷移任務能夠變成一個託管服務。
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; } }
不幸的是,IHostedService
並非咱們但願的靈丹妙藥。 它容許咱們編寫真正的異步代碼,但它有幾個問題:
IHostService
的典型實現指望StartAsync
方法可以相對快速返回。對於後臺任務來講,它但願你可以以異步分當時啓動服務,可是大多數任務都是在啓動代碼以外。遷移數據庫的任務會阻止其餘IHostService
啓動(這裏我不太理解做者的意思,只是按字面意思翻譯,後續會更新這裏)。IHostService
運行數據庫遷移以前開始接受請求,這顯然不是咱們想要的。Program.cs
中手動運行任務到如今爲止,咱們都沒有提供一種完善的解決方案,他們或者是使用同步方式處理異步任務,或者是不能阻止程序啓動。
如今讓咱們中止嘗試使用框架機制,手動來完成工做。
ASP.NET Core模板中使用的默認Program.cs
在Main
函數的一個語句中構建並運行IWebHost
:
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()
方法以後, Run()
方法以前,你能夠添加一些自定義的代碼,再加上C# 7.1中容許使用異步方式運行Main
方法,因此這裏咱們有了一個合理的方案。
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>(); }
這個方案有如下優勢:
可是這種方法也存在一些問題:
Run()
或者RunAsync()
方法以後,中間件管道纔開始構建。當構建中間件管道時,IStartupFilter
纔會被執行,而後程序啓動。若是你的異步任務須要在以上任何步驟中配置,那你就不走運了。若是這些問題都不是問題,那麼我認爲這個最終選項提供瞭解決問題的最佳方案。 在個人下一篇文章中,我將展現一些方法,咱們能夠在這個例子的基礎上構建,以使某些內容更容易使用。
在這篇文章中,我討論了在ASP.NET Core應用程序啓動時執行異步運行任務的必要性。 我描述了這樣作的一些問題和挑戰。 對於同步任務,IStartupFilter
爲ASP.NET Core應用程序啓動過程提供了一個有用的鉤子,可是須要使用同步方式運行異步任務,這一般是一個壞主意。 我描述了運行異步任務的一些可能的選項,我發現其中最好的是在Program.cs
中「手動」運行任務。 在下一篇文章中,我將介紹一些代碼,使這個模式更容易使用。