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

扫描二维码,关注我的公众号,能够第一时间获得最新的我的文章和内容推送

本文版权归做者全部,转载请保留此声明和原文连接

相关文章
相关标签/搜索