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

這一篇是接着前一篇在寫的。若是沒有看過前一篇文章,建議先去看一下前一篇,這兒是傳送門html

1、前言

前一篇文章,咱們從應用啓動時異步運行任務開始,說到了必要性,也說到了幾種解決方法,及各自的優缺點。最後,還提出了一個比較合理的解決方法:經過在Program.cs里加入代碼,來實現IWebHost啓動前運行異步任務。web

實現的代碼再貼一下:c#

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

這個方法是有效的。可是,也會有一點不足。微信

從.Net Core的最簡規則來講,咱們不該該在Program.cs中加入其它代碼。固然,咱們能夠把這部分代碼轉到一個外部類中,但最後也必須手動加入到Program.cs中。尤爲是在多個應用中,使用相同的模式時,這種方式會很麻煩。app

    爲防止非受權轉發,這兒給出本文的原文連接:http://www.javashuo.com/article/p-djhafzou-nc.html框架

也許,咱們能夠採用向DI容器中注入啓動任務?異步

2、向DI容器中注入啓動任務

這種方式,是基於IStartupFilterIHostedService兩個接口,經過這兩個接口能夠向依賴注入容器中註冊類。async

首先,咱們爲啓動任務建立一個簡單接口:ide

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

再建一個擴展方法,用來向DI容器註冊啓動任務:ui

public static class ServiceCollectionExtensions
{

    public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
        where T : classIStartupTask
        => services.AddTransient<IStartupTask, T>();

}

最後,再建一個擴展方法,在應用啓動時,查找全部已註冊的IStartupTask,按順序執行他們,而後啓動IWebHost

public static class StartupTaskWebHostExtensions
{

    public static async Task RunWithTasksAsync(this IHost webHost, CancellationToken cancellationToken = default)
    
{
        var startupTasks = webHost.Services.GetServices<IStartupTask>();

        foreach (var startupTask in startupTasks)
        {
            await startupTask.ExecuteAsync(cancellationToken);
        }

        await webHost.RunAsync(cancellationToken);
    }
}

這樣就齊活了。

仍是用一個例子來看看這個方式的具體應用。

3、示例 - 數據遷移

實現IStartupTask其實和實現IStartupFilter很類似,能夠從DI容器中注入。若是須要考慮做用域,還能夠注入IServiceProvider,並手動建立做用域。

例子中,數據遷移類能夠寫成這樣:

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

    public async 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.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddStartupTask<MigrationStartupTask>();
}

最後,用上一節中的擴展方法RunWithTasksAsync()來替代Program.cs中的Run():

public class Program
{

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

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

從功能上來講,跟上一篇的代碼區別不大,但這樣的寫法,又多了一些優勢:

  1. 任務代碼放到了Program.cs以外。這符合微軟的建議,也更容易理解;
  2. 任務放到了DI容器中,這樣更容易添加額外的任務;
  3. 若是沒有額外任務,這個代碼和標準的Run()同樣,因此這個代碼能夠獨立成一個模板。

簡單來講,使用RunWithTasksAsync()後,能夠輕鬆地向DI容器添加額外的任務,而不須要任何其它的更改。

滿意了嗎?好像感受還差一點點…

4、不夠完美的地方

若是要照着完美去作,好像還差一點點。

這個一點點是在於:任務如今運行在IConfiguration和DI容器配置完成後,IStartupFilters運行和中間件管道配置完成以前。換句話說,若是任務須要依賴於IStartupFilters,那這個方案行不通。

在大多數狀況下,這沒什麼問題。以我本身的經驗來看,好像沒有什麼功能須要依賴於IStartupFilters。但做爲一個框架類的代碼,須要考慮這種狀況發生的可能性。

以目前的方案來講,好像還沒辦法解決。

應用啓動時,當調用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()的調用之間,該怎麼辦?

這段代碼能給出答案:咱們須要裝飾IServer。 ¨K16K 首先,咱們替換IServer的實現: ¨G8G 在這段代碼中,咱們攔截StartAsync()調用並注入任務,而後回到內置處理。 下面是對應的擴展代碼: ¨G9G 這個擴展代碼作了兩件事:在DI容器中註冊了IStartupTask,並裝飾了以前註冊的IServer實例。裝飾方法Decorate()我略過了,有興趣的能夠去了解一下 - 裝飾模式。 Program.cs的代碼和第三節的代碼相同,略過。 &emsp; 咱們終於作到了在應用程序徹底構建完成後去執行咱們的任務,包括IStartupFilters`和中間件管道。

如今的流程,相似於下面這個微軟官方的圖:

(全文完)

 

 


 

微信公衆號:老王Plus

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

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

相關文章
相關標籤/搜索