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

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

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

在我的上一篇博客中,我介紹瞭如何在ASP.NET Core應用程式啟動時運行一些一次性異步任務。本篇博客將繼續討論上一篇的內容,如果你還沒有讀過,我建議你先讀一下前一篇

在本篇博客中,我將展示上一篇博文中提出的“在Program.cs中手動運行異步任務”的實現方法。該實現會使用一些簡單的接口和類來封裝應用程式啟動時的運行任務邏輯。我還會展示一個替代方法,這個替代方法是在Kestral服務器啟動時,使用IServer接口。

在應用程式啟動時運行異步任務

這裡我們先回顧一下上一遍博客內容,在上一篇中,我們試圖尋找一種方案,允許我們在ASP.NET Core應用程式啟動時執行一些異步任務。這些任務應該是在ASP.NET Core應用程式啟動之前執行,但是由於這些任務可能需要讀取配置或者使用服務,所以它們只能在ASP.NET Core的依賴註入容器配置完成後執行。資料庫遷移,填充快取都可以這種異步任務的使用場景。

我們在一篇文章的末尾提出了一個相對完善的解決方案,這個方案是在Program.cs中“手動”運行任務。運行任務的時機是在IWebHostBuilder.Build()IWebHost.RunAsync()之間。

這種實現方式是可行的,但是有點亂。這裡我們將許多不應該屬於Program.cs職責的代碼放在了Program.cs中,讓它看起來有點臃腫了,所以這裡我們需要將資料庫遷移相關的代碼移到另外一個類中。

這裡更麻煩的問題是,我們必須要手動呼叫任務。如果你在多個應用程式中使用相同的樣式,那麼最好能改成自動呼叫任務。

在依賴註入容器中註冊啟動任務

這裡我將使用基於IStartupFilterIHostService使用的樣式。它們允許你在依賴註入容器中註冊它們的實現類,併在應用程式啟動前獲取到這些接口的所有實現類,並依次執行它們。

所以,這裡首先我們創建一個簡單的接口來啟動任務。

並且創建一個在依賴註入容器中註冊任務的便捷方法。

最後,我們添加一個擴展方法,在應用程式啟動時找到所有已註冊的IStartupTasks,按順序運行它們,然後啟動IWebHost:

以上就是所有的代碼。

下麵為了看一下它的實際效果,我將繼續使用上一篇中EF Core資料庫遷移的例子

例子:異步遷移資料庫

實現IStartupTask和實現IStartupFilter非常的相似。你可以從依賴註入容器中註入服務。為了使用依賴註入容器中的服務,這裡我們需要手動註入一個IServiceProvider物件,並手動創建一個Scoped服務。

EF Core的資料庫遷移啟動任務類似以下代碼:

現在,我們可以在ConfigureServices方法中使用依賴註入容器添加啟動任務了。

最後我們更新一下Program.cs, 使用RunWithTasksAsync()方法替換Run()方法。

以上代碼利用了C# 7.1中引入的異步Task Main的特性。從功能上來說,它與我上一篇博客中的手動代碼等同,但是它有一些優點。

  • 它的任務實現代碼沒有放在Program.cs中。
  • 由於上一條的優點,開發人員可以很容易的添加額外的任務。
  • 如果不運行任何任務,它的功能和RunAsync是一樣的

對於以上方案,有一個問題需要註意。這裡我們定義的任務會在IConfiguration和依賴註入容器配置完成之後運行,這也就意味著,當任務執行時,所有的IStartupFilter都沒有運行,中間件管道也沒有配置。

就我個人而言,我不認為這是一個問題,因為我暫時想不出任何可能。到目前為止,我所編寫的任務都不依賴於IStartupFilter和中間件管道。但這也並不意味著沒有這種可能。

不幸的是,使用當前的WebHost代碼並沒有簡單的方法(儘管 在.NET Core 3.0中當ASP.NET Core作為IHostedService運行時,這可能會發生變化)。 問題是應用程式是引導(通過配置中間件管道並運行IStartupFilters)和啟動在同一個函式中。 當你在Program.cs中呼叫WebHost.Run()時,在內部程式會呼叫WebHost.StartAsync,如下所示,為簡潔起見,其中只包含了日誌記錄和一些其他次要代碼:

這裡問題是我們想要在BuildApplication()Server.StartAsync之間插入代碼,但是現在沒有這樣做的機制。

我不確定我所給出的解決方案是否優雅,但它可以工作,併為消費者提供更好的體驗,因為他們不需要修改Program.cs

使用IServer的替代方案

為了實現在BuildApplication()Server.StartAsync()之間運行異步代碼,我能想到的唯一辦法是我們自己的實現一個IServer實現(Kestrel)! 對你來說,聽到這個可能感覺非常可怕 – 但是我們真的不打算更換服務器,我們只是去裝飾它。

TaskExecutingServer在其建構式中獲取了一個IServer實體 – 這是ASP.NET Core註冊的原始Kestral服務器。我們將大部分IServer的接口實現直接委托給Kestrel,我們只是攔截對StartAsync的呼叫並首先運行註入的任務。

這個實現最困難部分是使裝飾器正常工作。正如我在上一篇文章中所討論的那樣,使用帶有預設ASP.NET Core容器的裝飾可能會非常棘手。我通常使用Scrutor來創建裝飾器,但是如果你不想依賴另一個庫,你總是可以手動進行裝飾, 但一定要看看Scrutor是如何做到這一點的!

下麵我們添加一個用於添加IStartupTask的擴展方法, 這個擴展方法做了兩件事,一是將IStartupTask註冊到依賴註入容器中,二是裝飾了之前註冊的IServer實體(這裡為了簡潔,我省略了Decorate方法的實現)。如果它發現IServer已經被裝飾,它會跳過第二步,這樣你就可以安全的多次呼叫AddStartupTask方法。

使用這兩段代碼,我們不再需要再對Program.cs檔案進行任何更改,並且我們是在完全構建應用程式後執行我們的任務,這其中也包括IStartupFilters和中間件管道。

啟動過程的序列圖現在看起來有點像這樣:

以上就是這種實現方式全部的內容。它的代碼非常少, 以至於我自己都在考慮是否要自己編寫一個庫。不過最後我還是在GitHub和Nuget上創建了一個庫NetEscapades.AspNetCore.StartupTasks

這裡我只編寫了使用後一種IServer實現的庫,因為它更容易使用,而且Thomas Levesque已經編寫針對第一種方法可用的NuGet包。

在GitHub的實現中,我手動構造了裝飾器,以避免強制依賴Scrutor。 但最好的方法可能就是將代碼複製並粘貼到您自己的專案中。

總結

在這篇博文中,我展示了兩種在ASP.NET Core應用程式啟動時異步運行任務的方法。 第一種方法需要稍微修改Program.cs,但是“更安全”,因為它不需要修改像IServer這樣的內部實現細節。 第二種方法是裝飾IServer,提供更好的用戶體驗,但感覺更加笨拙。

 

    赞(0)

    分享創造快樂