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

原文: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

  • 肯定當前的託管環境
  • 從appsetting.json文件和環境變量中讀取配置
  • 配置依賴注入容器
  • 構建依賴注入容器
  • 配置中間件管道

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

  • 檢查強類型配置是否合法
  • 使用數據庫或者API填充緩存
  • 運行數據庫遷移腳本(這一般不是一個很好的方案,可是對於一些應用來講夠用了)

有些時候,一些任務並非非要在程序啓動,監聽請求前運行。這裏咱們以填充緩存爲例,若是它是設計的比較好的話,在程序啓動前是否填充緩存數據是可有可無的。可是,相對的,你確定也但願在應用程序開始監聽請求以前,遷移你的數據庫!json

其實ASP.NET Core框架本身也須要運行一些一次性初始化任務。這個最好的例子就是數據保護,它經常使用來作數據加密,這個模塊必需要在應用啓動前初始化。爲了實現初始化,它們使用了IStartupFilterc#

使用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和Kestrel自己將在執行一次性初始化任務以前啓動,雖然他們不會收到可能存在問題的「真實」請求(僅健康檢查請求)。
  • 這種方式會引入了額外的複雜度,除了添加運行一次性任務的代碼以外,還須要添加運行情況檢查以測試任務是否完成,並同步任務的狀態。
  • 應用程序的啓動會有延遲,由於須要等待全部任務完成,因此不太可能減小啓動時間。
  • 若是任務失敗,應用程序不會終止,並且健康檢查也永遠不會經過。這多是能夠接受的,可是我我的更喜歡讓應用程序馬上終止。
  • 使用健康檢查,並不能知道一次性任務運行的怎麼樣,你只能瞭解到任務是否完成。

在我看來,健康檢查並不適合一次性任務的場景,他們可能對我描述的一些例子頗有用,但我不認爲它適用於全部狀況。我真的但願能在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本質上是異步的,他提供了StartAsyncStopAsync方法。這對咱們來講很是的有用,它再也不是使用同步方式處理異步任務了。使用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.csMain函數的一個語句中構建並運行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>();
}

這個方案有如下優勢:

  • 咱們使用的是真正的異步,而不是使用同步方式處理異步任務
  • 咱們可使用異步方式運行任務
  • 只有當咱們的異步任務都完成以後,WebHost纔會啓動
  • 在這個時間點,依賴注入容易已經構建完成,咱們可使用它來建立服務

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

  • 即便依賴注入容器構建完成,可是中間件管道卻尚未完成構建。只有當你調用Run()或者RunAsync()方法以後,中間件管道纔開始構建。當構建中間件管道時,IStartupFilter纔會被執行,而後程序啓動。若是你的異步任務須要在以上任何步驟中配置,那你就不走運了。
  • 咱們失去了經過向依賴注入容器添加服務來自動運行任務的能力。 咱們只能手動運行任務。

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

總結

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

相關文章
相關標籤/搜索