ASP.NET Core 3.x啓動時運行異步任務(一)

這是一個大的題目,需要用幾篇文章來說清楚。這是第一篇。

一、前言

在我們的項目中,有時候我們需要在應用程序啓動前執行一些一次性的邏輯。比方說:驗證配置的正確性、填充緩存、或者運行數據庫清理/遷移等。

如何合理、有效、優雅地完成這個任務,是這個文章討論的主要內容。

要實現這樣一個功能,其實我們有幾個選擇:

  1. 使用IStartupFilter運行同步任務。這是一個內置的解決方案,可以通過一些設置和技巧來運行異步任務;
  2. 使用IStartupFilterIApplicationLifetime事件來運行異步任務,這是一個可選的方案,但有不足,我們會在後面講;
  3. 使用IHostedService,在不阻塞應用啓動的情況下,運行一些一次性的任務;(關於這個內容,我在前一篇文章ASP.NET Core 3.x控制IHostedService啓動順序淺探中有涉及到一部分內容)
  4. Program.cs中運行異步任務。在大多數情況下,從代碼的複雜度到效率上,這都是一個比較好的選擇。

    爲防止非授權轉發,這兒給出本文的原文鏈接:https://www.cnblogs.com/tiger-wang/p/13673046.html

先提個問題:爲什麼要在應用啓動時運行任務?

二、爲什麼要在應用啓動時運行任務?

在應用啓動並開始請求服務之前,很多時候需要運行各種初始化工作。

一個ASP.NET應用啓動時,需要完成很多事,例如:

  • 確定當前的宿主環境
  • 加載appsetting.json配置和環境變量
  • 配置並創建依賴注入的容器
  • 配置中間件管道

這是應用啓動時要完成的引導內容。

在完成這些內容,運行WebHost並開始監聽請求之前,還會有一些一次性任務需要啓動,例如:

  • 檢查強類型配置的有效性
  • 填充或恢復緩存
  • 數據庫清理/遷移(通常來說這不是個好主意,但很多時候沒有別的辦法)

當然,有些任務也不是一定要在開始監聽請求之前運行,這要看具體的運行任務的架構。一般來說,如果緩存處理的完善,是不需要提前啓動的。當然,清理/遷移數據庫,是必須放在服務啓動之前。

在微軟官網上,有一個例子是數據保護子系統,用於即時加密(cookie、防僞令牌等),這個就必須在應用監聽請求之前完成初始化並加載,這個例子使用了IStartupFilter

三、使用IStartupFilter運行同步任務

IStartupFilters作爲配置中間件管道的一部分,通常在Startup.Configure()中運行。它允許我們定製應用的中間件管道,處理我們希望進行的所有任務。

看一個簡單的例子:

public class AutoRequestServicesStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            builder.UseMiddleware<RequestServicesContainerMiddleware>();
            next(builder);
        };
    }
}

IStartupFilter提供了一種可能,在依賴注入容器配置完成之後、應用程序啓動之前運行一些代碼。因此,我們可以在IStartupFilters中直接使用依賴注入。這表示我們可以運行有關係統的任何代碼。在前邊提到的微軟官網的例子中,就是創建了一個基於IStartupFiltersDataProtectionStartupFilter來初始化數據保護子系統。

此外,IStartupFilter允許我們通過向依賴注入容器註冊服務來增加要執行的任務。這是一個很有用的特性,表示我們可以註冊一個在應用啓動時運行的任務,而不需要顯式的調用。

但是,這兒有個問題。IStartupFilters通常運行的是同步的任務。看一下上面的代碼,Configure()方法不返回任務。當然,我們硬要使用異步也是可以的,但一般來說,這不算個好主意。原因我後面會寫。

寫到這兒,如果對ASP.NET Core架構熟悉,就會引出另一個問題:爲什麼不用健康檢查來確認一次性任務的執行結果?

四、爲什麼不用健康檢查?

運行健康檢查,是ASP.NET Core 2.2新引入的一個特性,允許查詢通過API(HTTP Endpoint)公開的應用的健康狀況。當應用部署在Kubernetes,或反向代理HAProxyNginx後面時,可以提供給代理用來檢測應用是否準備好開始提供服務。

我們可以使用健康檢查來確保應用所有必需的一次性任務完成之前不會開始監聽服務。

但是,這種方式會有一點問題。

WebHostKestrel本身會在一次性任務執行前啓動。當然,這時他們還不會接收和處理服務請求,但仍然引出了一些問題:

首先是增加了代碼的複雜性。除了一次性任務的代碼外,還要增加健康檢查來測試任務是否完成,並同步和保持任務的狀態;其次,如果任務失敗了,應用程序的健康檢查將會讓應用後續的任務無法繼續執行。合理的流程是:應用應該立即失敗返回。

這兒主要的原因是:健康檢查沒有定義如何實際運行任務,而只是定義了任務是否成功完成。相對來說,這種狀態機制比較單一,在一些簡單的任務中可能適用,但不能全面覆蓋一次性任務的全部場景。

五、運行異步任務

前邊寫了一些不太完美的方法。

現在,我們開始進入運行異步方法的一些步驟。當然,運行異步也會有幾種方式,適用性上會有一定的區別。

方式1:使用IStartupFilter

前邊說過,使用IStartupFilter時,執行的是同步任務。所以,我們可以通過GetAwater().GetResult()來調用異步。

我們拿數據遷移來舉個例子。在EF Core中,通過myDBContext.database.migrateasync()在運行時進行數據庫遷移。其中,myDBContext是應用程序中DBContext的一個實例。

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;
    }
}

通常,GetAwaiter().GetResult()要注意避免死鎖的問題。但這兒可能不需要,因爲這個代碼只在啓動時運行,這時候還沒有需要處理的請求,所以不太會死鎖。

只能說,這樣可以用。不過習慣上我會避免這麼做。

方式2:使用IApplicationLifetime事件

這是另一個選擇。可以通過IApplicationLifetime事件,在應用啓動和關閉時接收通知,處理任務。

但這個方式也有侷限性。

首先,IApplicationLifetime使用cancellationtoken來註冊回調,也就是說,這又是一個同步方式,又需要使用GetAwaiter().GetResult()來調用異步。

其次,ApplicationStarted事件是在WebHost啓動之後纔會觸發,因此異步任務也是在應用開始監聽請求後才運行。

方式3:使用IHostedService

IHostedService可以讓ASP.NET Core應用在後臺執行長時間的任務。

一般來說,IHostedService用在週期性任務、消息傳遞等任務上,但實際上它並不限於運行這些任務。在ASP.NET Core 3.x上,WebHost本身也是建立在IHostedService上的。

而且,IHostedService本身就是異步的,它提供了StartAsyncStopAsync

這種方式下,我們的代碼會是這樣:

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可以直接運行異步任務。

但是,IHostedService也有侷限性。從微軟官網的說明來看,IHostedService實現期望StartAsync能相對較快的返回。對於後臺任務,傾向於異步啓動,但主要任務在啓動後執行。

在上面這個例子中,數據遷移本身不是問題,但這個長時任務會阻止其它`IHostedService啓動和運行。而且,應用會在IHostedService完成數據遷移前開始監聽並響應請求,這是一個嚴重的問題。

方式4:在Program.cs中運行

上面三個方式,都可以解決啓動時運行異步任務的問題,但都不夠完美,要麼要求使用同步(異步轉同步可以用,但有隱藏問題),要麼不能阻止應用啓動,會造成應用啓動完成後,可能異步任務還未完成的情況。

我在前邊的博文中寫到過關於Program.cs中運行IHostedService的方式。具體可以去看ASP.NET Core 3.x控制IHostedService啓動順序淺探

看一下Program.cs的默認代碼:

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()創建WebHost之後,調用Run()之前,完全可以加入我們需要的代碼。同時,C# 7.1後主函數可以改爲異步運行。

因此,我們可以在這兒做些文章:

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>();
}

這個方案的好處是:

  • 這是真正的異步;
  • 任務完成後,應用程序纔可以監聽並接受請求;
  • 此時已經構建了依賴注入容器,所以可以創建服務;

當然,同樣也會有不足:這兒只是構建了DI容器,但並沒有建立管道(管道在Run()RunAsync()後才建立,然後是IStartupFilters執行,再然後是應用程序啓動)。因此異步任務不能使用管道、IStartupFilters中的配置。不過,這種需求的情況很少。

六、總結

這個部分牽扯到的框架內容比較多。

我們從應用啓動時異步運行任務開始,說到了必要性,也說到了幾種解決方法,及各自的優缺點。

下一篇文章,我會用一些具體的例子,來說清楚這個方式的具體使用,敬請關注。

(未完待續)

 

 


 

微信公衆號:老王Plus

掃描二維碼,關注個人公衆號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此聲明和原文鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章