天天看點

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

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

一、前言

前一篇文章,我們從應用啟動時異步運作任務開始,說到了必要性,也說到了幾種解決方法,及各自的優缺點。最後,還提出了一個比較合理的解決方法:通過在

Program.cs

裡加入代碼,來實作

IWebHost

啟動前運作異步任務。

實作的代碼再貼一下:

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

中。尤其是在多個應用中,使用相同的模式時,這種方式會很麻煩。

    為防止非授權轉發,這兒給出本文的原文連結:https://www.cnblogs.com/tiger-wang/p/13714679.html

也許,我們可以采用向DI容器中注入啟動任務?

二、向DI容器中注入啟動任務

這種方式,是基于

IStartupFilter

IHostedService

兩個接口,通過這兩個接口可以向依賴注入容器中注冊類。

首先,我們為啟動任務建立一個簡單接口:

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

再建一個擴充方法,用來向DI容器注冊啟動任務:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
        where T : class, IStartupTask
        => 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);
    }
}
           

這樣就齊活了。

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

三、示例 - 資料遷移

實作

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容器添加額外的任務,而不需要任何其它的更改。

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

四、不夠完美的地方

如果要照着完美去做,好像還差一點點。

這個一點點是在于:任務現在運作在

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

,并裝飾了之前注冊的

執行個體。裝飾方法

Decorate()

我略過了,有興趣的可以去了解一下 - 裝飾模式。

Program.cs

的代碼和第三節的代碼相同,略過。 &emsp; 我們終于做到了在應用程式完全建構完成後去執行我們的任務,包括

IStartupFilters`和中間件管道。

現在的流程,類似于下面這個微軟官方的圖:

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

(全文完)

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

微信公衆号:老王Plus

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

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

繼續閱讀