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

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

在個人上一篇博客中,我介紹瞭如何在ASP.NET Core應用程序啓動時運行一些一次性異步任務。本篇博客將繼續討論上一篇的內容,若是你尚未讀過,我建議你先讀一下前一篇。git

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

在應用程序啓動時運行異步任務

這裏咱們先回顧一下上一遍博客內容,在上一篇中,咱們試圖尋找一種方案,容許咱們在ASP.NET Core應用程序啓動時執行一些異步任務。這些任務應該是在ASP.NET Core應用程序啓動以前執行,可是因爲這些任務可能須要讀取配置或者使用服務,因此它們只能在ASP.NET Core的依賴注入容器配置完成後執行。數據庫遷移,填充緩存均可以這種異步任務的使用場景。web

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

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

這種實現方式是可行的,可是有點亂。這裏咱們將許多不該該屬於Program.cs職責的代碼放在了Program.cs中,讓它看起來有點臃腫了,因此這裏咱們須要將數據庫遷移相關的代碼移到另一個類中。c#

這裏更麻煩的問題是,咱們必需要手動調用任務。若是你在多個應用程序中使用相同的模式,那麼最好能改爲自動調用任務。緩存

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

這裏我將使用基於IStartupFilterIHostService使用的模式。它們容許你在依賴注入容器中註冊它們的實現類,並在應用程序啓動前獲取到這些接口的全部實現類,並依次執行它們。安全

因此,這裏首先咱們建立一個簡單的接口來啓動任務。服務器

public interface IStartupTask
{
    Task ExecuteAsync(CancellationToken cancellationToken = default);
}

而且建立一個在依賴注入容器中註冊任務的便捷方法。app

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
        where T : class, IStartupTask
        => services.AddTransient<IStartupTask, T>();
}

最後,咱們添加一個擴展方法,在應用程序啓動時找到全部已註冊的IStartupTasks,按順序運行它們,而後啓動IWebHost:

public static class StartupTaskWebHostExtensions
{
    public static async Task RunWithTasksAsync(this IWebHost webHost, CancellationToken cancellationToken = default)
    {
        var startupTasks = webHost.Services.GetServices<IStartupTask>();
       
        foreach (var startupTask in startupTasks)
        {
            await startupTask.ExecuteAsync(cancellationToken);
        }

        await webHost.RunAsync(cancellationToken);
    }
}

以上就是全部的代碼。

下面爲了看一下它的實際效果,我將繼續使用上一篇中EF Core數據庫遷移的例子

例子:異步遷移數據庫

實現IStartupTask和實現IStartupFilter很是的類似。你能夠從依賴注入容器中注入服務。爲了使用依賴注入容器中的服務,這裏咱們須要手動注入一個IServiceProvider對象,並手動建立一個Scoped服務。

EF Core的數據庫遷移啓動任務相似如下代碼:

public class MigratorStartupFilter: IStartupTask
{
    private readonly IServiceProvider _serviceProvider;
    public MigratorStartupFilter(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public Task ExecuteAsync(CancellationToken cancellationToken = default)
    {
        using(var scope = _seviceProvider.CreateScope())
        {
            var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

            await myDbContext.Database.MigrateAsync();
        }
    }
}

如今,咱們能夠在ConfigureServices方法中使用依賴注入容器添加啓動任務了。

public void ConfigureServices(IServiceCollection services)
{
    services.MyDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration               
             .GetConnectionString("DefaultConnection")));

    services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddStartupTask<MigrationStartupTask>();
}

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

public class Program
{
    public static async Task Main(string[] args)
    {
        await CreateWebHostBuilder(args)
            .Build()
            .RunWithTasksAsync();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

以上代碼利用了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,以下所示,爲簡潔起見,其中只包含了日誌記錄和一些其餘次要代碼:

public virtual async Task StartAsync(CancellationToken cancellationToken = default)
{
    _logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();

   
    var application = BuildApplication();

    _applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
    _hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
    var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
    var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
    var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory);

    await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

    _applicationLifetime?.NotifyStarted();

    await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);
}

這裏問題是咱們想要在BuildApplication()Server.StartAsync之間插入代碼,可是如今沒有這樣作的機制。

我不肯定我所給出的解決方案是否優雅,但它能夠工做,併爲消費者提供更好的體驗,由於他們不須要修改Program.cs

使用IServer的替代方案

爲了實如今BuildApplication()Server.StartAsync()之間運行異步代碼,我能想到的惟一辦法是咱們本身的實現一個IServer實現(Kestrel)! 對你來講,聽到這個可能感受很是可怕 - 可是咱們真的不打算更換服務器,咱們只是去裝飾它。

public class TaskExecutingServer : IServer
{
    private readonly IServer _server;
    private readonly IEnumerable<IStartupTask> _startupTasks;
    public TaskExecutingServer(IServer server, IEnumerable<IStartupTask> startupTasks)
    {
        _server = server;
        _startupTasks = startupTasks;
    }

    public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
    {
        foreach (var startupTask in _startupTasks)
        {
            await startupTask.ExecuteAsync(cancellationToken);
        }

        await _server.StartAsync(application, cancellationToken);
    }

    public IFeatureCollection Features => _server.Features;
    public void Dispose() => _server.Dispose();
    public Task StopAsync(CancellationToken cancellationToken) => _server.StopAsync(cancellationToken);
}

TaskExecutingServer在其構造函數中獲取了一個IServer實例 - 這是ASP.NET Core註冊的原始Kestral服務器。咱們將大部分IServer的接口實現直接委託給Kestrel,咱們只是攔截對StartAsync的調用並首先運行注入的任務。

這個實現最困難部分是使裝飾器正常工做。正如我在上一篇文章中所討論的那樣,使用帶有默認ASP.NET Core容器的裝飾可能會很是棘手。我一般使用Scrutor來建立裝飾器,可是若是你不想依賴另外一個庫,你老是能夠手動進行裝飾, 但必定要看看Scrutor是如何作到這一點的!

下面咱們添加一個用於添加IStartupTask的擴展方法, 這個擴展方法作了兩件事,一是將IStartupTask註冊到依賴注入容器中,二是裝飾了以前註冊的IServer實例(這裏爲了簡潔,我省略了Decorate方法的實現)。若是它發現IServer已經被裝飾,它會跳過第二步,這樣你就能夠安全的屢次調用AddStartupTask 方法。

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddStartupTask<TStartupTask>(this IServiceCollection services)
        where TStartupTask : class, IStartupTask
        => services
            .AddTransient<IStartupTask, TStartupTask>()
            .AddTaskExecutingServer();

    private static IServiceCollection AddTaskExecutingServer(this IServiceCollection services)
    {
        var decoratorType = typeof(TaskExecutingServer);
        if (services.Any(service => service.ImplementationType == decoratorType))
        {
            return services;
        }

        return services.Decorate<IServer, TaskExecutingServer>();
    }
}

使用這兩段代碼,咱們再也不須要再對Program.cs文件進行任何更改,而且咱們是在徹底構建應用程序後執行咱們的任務,這其中也包括IStartupFilters和中間件管道。

啓動過程的序列圖如今看起來有點像這樣:

以上就是這種實現方式所有的內容。它的代碼很是少, 以致於我本身都在考慮是否要本身編寫一個庫。不過最後我仍是在GitHubNuget上建立了一個庫NetEscapades.AspNetCore.StartupTasks

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

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

總結

在這篇博文中,我展現了兩種在ASP.NET Core應用程序啓動時異步運行任務的方法。 第一種方法須要稍微修改Program.cs,可是「更安全」,由於它不須要修改像IServer這樣的內部實現細節。 第二種方法是裝飾IServer,提供更好的用戶體驗,但感受更加笨拙。

相關文章
相關標籤/搜索