如何在ASP.NET Core程序启动时运行异步任务(1)

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

背景

当咱们作项目的时候,有时候但愿本身的ASP.NET Core应用在启动前执行一些初始化逻辑。例如,你但愿验证配置是否合法,填充缓存数据,或者运行数据库迁移脚本。在本篇博客中,我将介绍几种可选的方案,而且经过展现一些简单的方法和扩展点来讲明我想要解决的问题。git

开始我将先描述一下ASP.NET Core内置的解决方案,使用IStartupFilter来运行同步任务。而后我将描述几种可选的执行异步任务的方案。你能够(可是可能不该该这样作)使用IStartupFilter或者IApplicationLifetime事件来执行异步任务。你也可使用IHostService接口来运行一次性任务且不会阻塞ASP.NET Core应用启动。最后惟一合理的方案是在program.cs文件中手动运行任务。在下一篇博客中,我会展现一个能够简化这个流程的推荐方案。github

为何咱们须要在程序启动时运行异步任务?

在程序启动,开始监听请求以前,运行一些初始化代码是很是广泛的。对于一个ASP.NET Core应用程序,启动前有许多任务须要运行,例如:web

  • 肯定当前的托管环境
  • 从appsetting.json文件和环境变量中读取配置
  • 配置依赖注入容器
  • 构建依赖注入容器
  • 配置中间件管道

以上几步都四发生在应用程序引导时。然而有些一次性任务须要在WebHost启动,监听请求前运行。例如数据库

  • 检查强类型配置是否合法
  • 使用数据库或者API填充缓存
  • 运行数据库迁移脚本(这一般不是一个很好的方案,可是对于一些应用来讲够用了)

有些时候,一些任务并非非要在程序启动,监听请求前运行。这里咱们以填充缓存为例,若是它是设计的比较好的话,在程序启动前是否填充缓存数据是可有可无的。可是,相对的,你确定也但愿在应用程序开始监听请求以前,迁移你的数据库!json

其实ASP.NET Core框架本身也须要运行一些一次性初始化任务。这个最好的例子就是数据保护,它经常使用来作数据加密,这个模块必需要在应用启动前初始化。为了实现初始化,它们使用了IStartupFilterc#

使用IStartupFilter来运行同步任务

在以前的博客中,我已经介绍过IStartupFilter, 它是一个自定义ASP.NET Core应用的强力接口。缓存

若是你是第一次接触Filter, 我建议你去我以前的博客,这里我只会提供一个简短的总结。

IStartupFilter会在配置中间件管道的进程中被执行(一般在Startup.Configure()中完成)。它们容许你经过插入额外的中间件,分叉或执行任何其余操做来自定义应用程序实际建立的中间件管道。例以下面代码展现的AutoRequestServiceStartupFilter

public class AutoRequestServicesStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            builder.UseMiddleware<RequestServicesContainerMiddleware>();
            next(builder);
        };
    }
}

这很是有用,但它与ASP.NET Core应用程序启动时运行一次性任务有什么关系呢?

IStartupFilter的主要功能是为开发人员提供了一个钩子(hook), 这个钩子触发的时机是在在应用程序配置完成并配置依赖注入容器以后,应用程序启动以前。这意味着,你能够在实现IStartupFilter的类中使用依赖注入,这样你就能够在这里完成许多但愿在应用程序启用前须要运行的任务。以ASP.NET Core内置的DataProtectionStartupFilter为例,它会在程序启用前初始化整个数据保护模块。

IStartupFilter提供的另一个重要功能就是,它容许你经过向依赖注入容器注册服务来添加要执行的任务。这意味着若是你本身编写了一个Library, 你能够在应用程序启动时注册一个任务,而不须要应用程序显式调用它。

问题是IStartupFilter基本上是同步的。Configure方法的返回值不是Task,所以咱们只能使用同步方式执行异步任务,这显然不是好的实现方案。 我稍后会讨论这个,但如今让咱们先跳过它。

为何不用健康检查?

ASP.NET Core 2.2中加入了一个新的健康检查功能,它经过暴露一个HTTP节点,让你能够查询当前应用的健康状态。当应用部署以后,像Kubernetes这样的编排引擎或HAProxy和NGINX等反向代理能够查询此HTTP节点以检查你应用是否已准备好开始接收请求。

你可使用健康检查功能来确保你的应用程序不会开始处理请求,直到全部必需的一次性初始化任务完成为止。然而,这有一些缺点:

  • WebHost和Kestrel自己将在执行一次性初始化任务以前启动,虽然他们不会收到可能存在问题的“真实”请求(仅健康检查请求)。
  • 这种方式会引入了额外的复杂度,除了添加运行一次性任务的代码以外,还须要添加运行情况检查以测试任务是否完成,并同步任务的状态。
  • 应用程序的启动会有延迟,由于须要等待全部任务完成,因此不太可能减小启动时间。
  • 若是任务失败,应用程序不会终止,并且健康检查也永远不会经过。这多是能够接受的,可是我我的更喜欢让应用程序马上终止。
  • 使用健康检查,并不能知道一次性任务运行的怎么样,你只能了解到任务是否完成。

在我看来,健康检查并不适合一次性任务的场景,他们可能对我描述的一些例子颇有用,但我不认为它适用于全部状况。我真的但愿能在WebHost启动以前,运行一些一次性任务。

运行异步任务

我已经花了很长的篇幅来讨论了全部不能完成个人目标的全部方法,那么哪些才是可行的方案!在这一节中,我将描述几种运行异步任务的方案(即方法返回Task, 而且须要等待的),其中有一些较好的方案,也有一些须要规避的方案。

这里为了更清楚的描述这些方案,我选用数据库迁移做为例子。在EF Core中,你能够在运行时调用myDbContext.Database.MigrateAsync()来迁移数据库,其中myDbContext是当前应用程序的数据库上下文实例。

EF还提供了一个同步的数据库迁移方法Database.Migrate(),可是这里咱们不须要使用它。

使用IStartupFilter

我以前描述过如何使用IStartupFilter在应用程序启动时运行同步任务。 不过,这里为了异步方法,咱们使用了GetAwaiter()GetResult()阻塞了线程, 将异步方法变成了一个同步方法。

警告:这是一种很是很差的异步实践方式

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

这段代码可能不会引发任何问题,它会在应用程序启动且未开始监听请求时运行,因此不太可能出现死锁。可是坦率的说,我会尽量不用这种方式。

使用IApplicationLifetime 事件

我以前尚未讨论过和这个事件相关的内容,可是当你的应用程序启动和关闭前,你可使用IApplicationLifetime接口接收到通知。这里我不会详细介绍它,由于使用它来实现咱们的目的会有一些问题。

IApplicationLifetime使用CancellationTokens来注册回调,这意味着你只能同步执行回调。 这实际上意味着不管你作什么,你都会遇到同步异步模式。
ApplicationStarted事件仅在WebHost启动后触发,所以任务在应用程序开始接受请求后运行。
鉴于他们没有解决IStartupFilter使用同步方式处理异步任务的问题,也没有阻止应用启动,因此我只是将它列出来仅供参考。

使用IHostedService运行异步事件

IHostService容许在ASP.NET Core应用程序生命周期内,之后台程序的方式执行长时间运行的任务。它有许多不一样的用途,你可使用它在计数器上运行按期任务,或者监听RabbitMQ消息。在ASP.NET Core 3.0中, Web Host也多是使用IHostService构建的。

IHostService本质上是异步的,他提供了StartAsyncStopAsync方法。这对咱们来讲很是的有用,它再也不是使用同步方式处理异步任务了。使用IHostService,咱们的数据库迁移任务能够变成一个托管服务。

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并非咱们但愿的灵丹妙药。 它容许咱们编写真正的异步代码,但它有几个问题:

  • IHostService的典型实现指望StartAsync方法可以相对快速返回。对于后台任务来讲,它但愿你可以以异步分当时启动服务,可是大多数任务都是在启动代码以外。迁移数据库的任务会阻止其余IHostService启动(这里我不太理解做者的意思,只是按字面意思翻译,后续会更新这里)。
  • 第二个问题是最大的问题,你的应用程序会在IHostService运行数据库迁移以前开始接受请求,这显然不是咱们想要的。

Program.cs中手动运行任务

到如今为止,咱们都没有提供一种完善的解决方案,他们或者是使用同步方式处理异步任务,或者是不能阻止程序启动。

如今让咱们中止尝试使用框架机制,手动来完成工做。

ASP.NET Core模板中使用的默认Program.csMain函数的一个语句中构建并运行IWebHost

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()方法以后, Run()方法以前,你能够添加一些自定义的代码,再加上C# 7.1中容许使用异步方式运行Main方法,因此这里咱们有了一个合理的方案。

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

这个方案有如下优势:

  • 咱们使用的是真正的异步,而不是使用同步方式处理异步任务
  • 咱们可使用异步方式运行任务
  • 只有当咱们的异步任务都完成以后,WebHost才会启动
  • 在这个时间点,依赖注入容易已经构建完成,咱们可使用它来建立服务

可是这种方法也存在一些问题:

  • 即便依赖注入容器构建完成,可是中间件管道却尚未完成构建。只有当你调用Run()或者RunAsync()方法以后,中间件管道才开始构建。当构建中间件管道时,IStartupFilter才会被执行,而后程序启动。若是你的异步任务须要在以上任何步骤中配置,那你就不走运了。
  • 咱们失去了经过向依赖注入容器添加服务来自动运行任务的能力。 咱们只能手动运行任务。

若是这些问题都不是问题,那么我认为这个最终选项提供了解决问题的最佳方案。 在个人下一篇文章中,我将展现一些方法,咱们能够在这个例子的基础上构建,以使某些内容更容易使用。

总结

在这篇文章中,我讨论了在ASP.NET Core应用程序启动时执行异步运行任务的必要性。 我描述了这样作的一些问题和挑战。 对于同步任务,IStartupFilter为ASP.NET Core应用程序启动过程提供了一个有用的钩子,可是须要使用同步方式运行异步任务,这一般是一个坏主意。 我描述了运行异步任务的一些可能的选项,我发现其中最好的是在Program.cs中“手动”运行任务。 在下一篇文章中,我将介绍一些代码,使这个模式更容易使用。

相关文章
相关标签/搜索