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

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

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

相关文章
相关标签/搜索