原文: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
中,讓它看起來有點臃腫了,所以這裡我們需要將資料庫遷移相關的程式碼移到另外一個類中。
這裡更麻煩的問題是,我們必須要手動呼叫任務。如果你在多個應用程式中使用相同的樣式,那麼最好能改成自動呼叫任務。
在依賴註入容器中註冊啟動任務
這裡我將使用基於IStartupFilter
和IHostService
使用的樣式。它們允許你在依賴註入容器中註冊它們的實現類,併在應用程式啟動前獲取到這些介面的所有實現類,並依次執行它們。
所以,這裡首先我們建立一個簡單的介面來啟動任務。
並且建立一個在依賴註入容器中註冊任務的便捷方法。
最後,我們新增一個擴充套件方法,在應用程式啟動時找到所有已註冊的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,提供更好的使用者體驗,但感覺更加笨拙。