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

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

1、前言

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

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

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

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

    爲防止非受權轉發,這兒給出本文的原文連接:http://www.javashuo.com/article/p-gkhljwug-nb.htmlc#

先提個問題:爲何要在應用啓動時運行任務?緩存

2、爲何要在應用啓動時運行任務?

在應用啓動並開始請求服務以前,不少時候須要運行各類初始化工做。微信

一個ASP.NET應用啓動時,須要完成不少事,例如:cookie

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

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

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

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

固然,有些任務也不是必定要在開始監聽請求以前運行,這要看具體的運行任務的架構。通常來講,若是緩存處理的完善,是不須要提早啓動的。固然,清理/遷移數據庫,是必須放在服務啓動以前。

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

3、使用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架構熟悉,就會引出另外一個問題:爲何不用健康檢查來確認一次性任務的執行結果?

4、爲何不用健康檢查?

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

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

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

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

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

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

5、運行異步任務

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

如今,咱們開始進入運行異步方法的一些步驟。固然,運行異步也會有幾種方式,適用性上會有必定的區別。

方式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中的配置。不過,這種需求的狀況不多。

6、總結

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

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

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

(未完待續)

 

 


 

微信公衆號:老王Plus

掃描二維碼,關注我的公衆號,能夠第一時間獲得最新的我的文章和內容推送

本文版權歸做者全部,轉載請保留此聲明和原文連接

相關文章
相關標籤/搜索