歡迎光臨
每天分享高質量文章

如何在ASP.NET Core程式啟動時運行異步任務(1)

原文:Running async tasks on app startup in ASP.NET Core (Part 1)
作者:Andrew Lock
譯者:Lamond Lu

背景

我們做專案的時候,有時候希望自己的ASP.NET Core應用在啟動前執行一些初始化邏輯。例如,你希望驗證配置是否合法,填充快取資料,或者運行資料庫遷移腳本。在本篇博客中,我將介紹幾種可選的方案,並且通過展示一些簡單的方法和擴展點來說明我想要解決的問題。

開始我將先描述一下ASP.NET Core內置的解決方案,使用IStartupFilter來運行同步任務。然後我將描述幾種可選的執行異步任務的方案。你可以(但是可能不應該這樣做)使用IStartupFilter或者IApplicationLifetime事件來執行異步任務。你也可以使用IHostService接口來運行一次性任務且不會阻塞ASP.NET Core應用啟動。最後唯一合理的方案是在program.cs檔案中手動運行任務。在下一篇博客中,我會展示一個可以簡化這個流程的推薦方案。

為什麼我們需要在程式啟動時運行異步任務?

在程式啟動,開始監聽請求之前,運行一些初始化代碼是非常普遍的。對於一個ASP.NET Core應用程式,啟動前有許多任務需要運行,例如:

  • 確定當前的托管環境

  • 從appsetting.json檔案和環境變數中讀取配置

  • 配置依賴註入容器

  • 構建依賴註入容器

  • 配置中間件管道

以上幾步都四發生在應用程式引導時。然而有些一次性任務需要在WebHost啟動,監聽請求前運行。例如

  • 檢查強型別配置是否合法

  • 使用資料庫或者API填充快取

  • 運行資料庫遷移腳本(這通常不是一個很好的方案,但是對於一些應用來說夠用了)

有些時候,一些任務並不是非要在程式啟動,監聽請求前運行。這裡我們以填充快取為例,如果它是設計的比較好的話,在程式啟動前是否填充快取資料是無關緊要的。但是,相對的,你肯定也希望在應用程式開始監聽請求之前,遷移你的資料庫!

其實ASP.NET Core框架自己也需要運行一些一次性初始化任務。這個最好的例子就是資料保護,它常用來做資料加密,這個模塊必須要在應用啟動前初始化。為了實現初始化,它們使用了IStartupFilter

使用IStartupFilter來運行同步任務

在之前的博客中,我已經介紹過IStartupFilter, 它是一個自定義ASP.NET Core應用的強力接口。

如果你是第一次接觸Filter, 我建議你去我之前的博客,這裡我只會提供一個簡短的總結。

IStartupFilter會在配置中間件管道的行程中被執行(通常在Startup.Configure()中完成)。它們允許你通過插入額外的中間件,分叉或執行任何其他操作來自定義應用程式實際創建的中間件管道。例如下麵代碼展示的AutoRequestServiceStartupFilter

這非常有用,但它與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和Kestrel本身將在執行一次性初始化任務之前啟動,雖然他們不會收到可能存在問題的“真實”請求(僅健康檢查請求)。

  • 這種方式會引入了額外的複雜度,除了添加運行一次性任務的代碼之外,還需要添加運行狀況檢查以測試任務是否完成,並同步任務的狀態。

  • 應用程式的啟動會有延遲,因為需要等待所有任務完成,所以不太可能減少啟動時間。

  • 如果任務失敗,應用程式不會終止,而且健康檢查也永遠不會通過。這可能是可以接受的,但是我個人更喜歡讓應用程式立刻終止。

  • 使用健康檢查,並不能知道一次性任務運行的怎麼樣,你只能瞭解到任務是否完成。

在我看來,健康檢查並不適合一次性任務的場景,他們可能對我描述的一些例子很有用,但我不認為它適用於所有情況。我真的希望能在WebHost啟動之前,運行一些一次性任務。

運行異步任務

我已經花了很長的篇幅來討論了所有不能完成我的標的的所有方法,那麼哪些才是可行的方案!在這一節中,我將描述幾種運行異步任務的方案(即方法傳回Task, 並且需要等待的),其中有一些較好的方案,也有一些需要規避的方案。

這裡為了更清楚的描述這些方案,我選用資料庫遷移作為例子。在EF Core中,你可以在運行時呼叫myDbContext.Database.MigrateAsync()來遷移資料庫,其中myDbContext是當前應用程式的資料庫背景關係實體。

EF還提供了一個同步的資料庫遷移方法Database.Migrate(),但是這裡我們不需要使用它。

使用IStartupFilter

我之前描述過如何使用IStartupFilter在應用程式啟動時運行同步任務。 不過,這裡為了異步方法,我們使用了GetAwaiter()GetResult()阻塞了執行緒, 將異步方法變成了一個同步方法。

警告:這是一種非常不好的異步實踐方式

這段代碼可能不會引起任何問題,它會在應用程式啟動且未開始監聽請求時運行,所以不太可能出現死鎖。但是坦率的說,我會盡可能不用這種方式。

使用IApplicationLifetime 事件

我之前還沒有討論過和這個事件相關的內容,但是當你的應用程式啟動和關閉前,你可以使用IApplicationLifetime接口接收到通知。這裡我不會詳細介紹它,因為使用它來實現我們的目的會有一些問題。

IApplicationLifetime使用CancellationTokens來註冊回呼,這意味著你只能同步執行回呼。 這實際上意味著無論你做什麼,你都會遇到同步異步樣式。
ApplicationStarted事件僅在WebHost啟動後觸發,因此任務在應用程式開始接受請求後運行。
鑒於他們沒有解決
IStartupFilter使用同步方式處理異步任務的問題,也沒有阻止應用啟動,所以我只是將它列出來僅供參考。

使用IHostedService運行異步事件

IHostService允許在ASP.NET Core應用程式生命周期內,以後臺程式的方式執行長時間運行的任務。它有許多不同的用途,你可以使用它在計數器上運行定期任務,或者監聽RabbitMQ訊息。在ASP.NET Core 3.0中, Web Host也可能是使用IHostService構建的。

IHostService本質上是異步的,他提供了StartAsyncStopAsync方法。這對我們來說非常的有用,它再在是使用同步方式處理異步任務了。使用IHostService,我們的資料庫遷移任務可以變成一個托管服務。

不幸的是,IHostedService並不是我們希望的靈丹妙藥。 它允許我們編寫真正的異步代碼,但它有幾個問題:

  • IHostService的典型實現期望StartAsync方法能夠相對快速傳回。對於後臺任務來說,它希望你能夠以異步分當時啟動服務,但是大多數任務都是在啟動代碼之外。遷移資料庫的任務會阻止其他IHostService啟動(這裡我不太理解作者的意思,只是按字面意思翻譯,後續會更新這裡)。

  • 第二個問題是最大的問題,你的應用程式會在IHostService運行資料庫遷移之前開始接受請求,這顯然不是我們想要的。

Program.cs中手動運行任務

到現在為止,我們都沒有提供一種完善的解決方案,他們或者是使用同步方式處理異步任務,或者是不能阻止程式啟動。

現在讓我們停止嘗試使用框架機制,手動來完成工作。

ASP.NET Core模板中使用的預設Program.csMain函式的一個陳述句中構建並運行IWebHost

這裡你可能會發現在Build()方法之後, Run()方法之前,你可以添加一些自定義的代碼,再加上C# 7.1中允許使用異步方式運行Main方法,所以這裡我們有了一個合理的方案。

這個方案有以下優點:

  • 我們使用的是真正的異步,而不是使用同步方式處理異步任務

  • 我們可以使用異步方式運行任務

  • 只有當我們的異步任務都完成之後,WebHost才會啟動

  • 在這個時間點,依賴註入容易已經構建完成,我們可以使用它來創建服務

但是這種方法也存在一些問題:

  • 即使依賴註入容器構建完成,但是中間件管道卻還沒有完成構建。只有當你呼叫Run()或者RunAsync()方法之後,中間件管道才開始構建。當構建中間件管道時,IStartupFilter才會被執行,然後程式啟動。如果你的異步任務需要在以上任何步驟中配置,那你就不走運了。

  • 我們失去了通過向依賴註入容器添加服務來自動運行任務的能力。 我們只能手動運行任務。

如果這些問題都不是問題,那麼我認為這個最終選項提供瞭解決問題的最佳方案。 在我的下一篇文章中,我將展示一些方法,我們可以在這個例子的基礎上構建,以使某些內容更容易使用。

總結

在這篇文章中,我討論了在ASP.NET Core應用程式啟動時執行異步運行任務的必要性。 我描述了這樣做的一些問題和挑戰。 對於同步任務,IStartupFilter為ASP.NET Core應用程式啟動過程提供了一個有用的鉤子,但是需要使用同步方式運行異步任務,這通常是一個壞主意。 我描述了運行異步任務的一些可能的選項,我發現其中最好的是在Program.cs中“手動”運行任務。 在下一篇文章中,我將介紹一些代碼,使這個樣式更容易使用。