原文:Running async tasks on app startup in ASP.NET Core (Part 1)

作者:Andrew Lock

译者:Lamond Lu

背景

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

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

为什么我们需要在程序启动时运行异步任务?

在程序启动,开始监听请求之前,运行一些初始化代码是非常普遍的。对于一个ASP.NET Core应用程序,启动前有许多任务需要运行,例如:

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

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

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

有些时候,一些任务并不是非要在程序启动,监听请求前运行。这里我们以填充缓存为例,如果它是设计的比较好的话,在程序启动前是否填充缓存数据是无关紧要的。但是,相对的,你肯定也希望在应用程序开始监听请求之前,迁移你的数据库!

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

使用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中“手动”运行任务。 在下一篇文章中,我将介绍一些代码,使这个模式更容易使用。

如何在ASP.NET Core程序启动时运行异步任务(1)的更多相关文章

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

    原文:Running async tasks on app startup in ASP.NET Core (Part 3) 作者:Andrew Lock 译者:Lamond Lu 之前我写了两篇有关 ...

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

    原文:Running async tasks on app startup in ASP.NET Core (Part 2) 作者:Andrew Lock 译者:Lamond Lu 在我的上一篇博客中 ...

  3. 在 ASP.NET Core 程序启动前运行你的代码

    一.前言 在进行 Web 项目开发的过程中,可能会存在一些需要经常访问的静态数据,针对这种在程序运行过程中可能几乎不会发生变化的数据,我们可以尝试在程序运行前写入到缓存中,这样在系统后续使用时就可以直 ...

  4. 探索ASP.Net Core 3.0系列四:在ASP.NET Core 3.0的应用中启动时运行异步任务

    前言:在本文中,我将介绍ASP.NET Core 3.0 WebHost的微小更改如何使使用IHostedService在应用程序启动时更轻松地运行异步任务. 翻译 :Andrew Lock   ht ...

  5. ASP.NET Core 3.x启动时运行异步任务(一)

    这是一个大的题目,需要用几篇文章来说清楚.这是第一篇.   一.前言 在我们的项目中,有时候我们需要在应用程序启动前执行一些一次性的逻辑.比方说:验证配置的正确性.填充缓存.或者运行数据库清理/迁移等 ...

  6. ASP.NET Core 3.x启动时运行异步任务(二)

    这一篇是接着前一篇在写的.如果没有看过前一篇文章,建议先去看一下前一篇,这儿是传送门   一.前言 前一篇文章,我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点.最 ...

  7. ASP.NET Core 的启动和运行机制

    目录 ASP .NET Core 的运行机制 ASP .NET Core 的启动 ASP .NET Core 的管道和中间件 参考 ASP .NET Core 的运行机制 Web Server: AS ...

  8. 如何在ASP.NET Core中使用JSON Patch

    原文: JSON Patch With ASP.NET Core 作者:.NET Core Tutorials 译文:如何在ASP.NET Core中使用JSON Patch 地址:https://w ...

  9. 如何在 asp.net core 3.x 的 startup.cs 文件中获取注入的服务

    一.前言 从 18 年开始接触 .NET Core 开始,在私底下.工作中也开始慢慢从传统的 mvc 前后端一把梭,开始转向 web api + vue,之前自己有个半成品的 asp.net core ...

随机推荐

  1. html总结01

    lesson01~lesson02基础 <!DOCTYPE html> <html lang="en"> <!-- ################# ...

  2. IntelliJ IDEA(十) :常用操作

    IDEA功能详细,快捷键繁多,但是实际开发时不是所有都能用上,如果我们熟悉一些常用的也足够满足我们日常开发了,多的也只是提高我们的B格. 1.自定义主题 IDEA默认的主题有三款,分别是Intelli ...

  3. Firefox Profile (2)

    一些关于selenium copy Firefox profile to a temp directory的讨论 https://stackoverflow.com/questions/6787095 ...

  4. 用PCA(主成分分析法)进行信号滤波

    用PCA(主成分分析法)进行信号滤波 此文章从我之前的C博客上导入,代码什么的可以参考matlab官方帮助文档 现在网上大多是通过PCA对数据进行降维,其实PCA还有一个用处就是可以进行信号滤波.网上 ...

  5. 【爆料】-《卧龙岗大学毕业证书》UOW一模一样原件

    ☞西悉尼大学毕业证书[微/Q:865121257◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归&a ...

  6. windows 7安装Fiddler抓HTTPS请求的解决办法

    一.下载安装Fiddler,网上随意一搜就可找到安装软件 二.打开已经安装的fiddler,本人的安装目录:D:\AutoTest\fiddle\Fiddler2 三.打开的页面如图: 三.点击too ...

  7. C++的代理类

    怎样在一个容器中包含类型不同,但是彼此有关系的对象?众所周知,C++的容器只能存放类型相同的元素,所以直接在一个容器中存储不同类型的对象本身是不可能的,只能通过以下两种方案实现: 1. 提供一个间接层 ...

  8. 深入理解java虚拟机之垃圾收集器

    Java一个重要的优势就是通过垃圾管理器GC (Garbage Collection)自动管理和回收内存,程序员无需通过调用方法来释放内存.也因此很好多的程序员可能会认为Java程序不会出现内存泄漏的 ...

  9. ASP.NET Core的实时库: SignalR简介及使用

    大纲 本系列会分为2-3篇文章. 第一篇介绍了SignalR的预备知识和原理 本文介绍SignalR以及ASP.NET Core里使用SignalR. 本文的内容: 介绍SignalR 在ASP.NE ...

  10. Spark学习之编程进阶总结(二)

    五.基于分区进行操作 基于分区对数据进行操作可以让我们避免为每个数据元素进行重复的配置工作.诸如打开数据库连接或创建随机数生成器等操作,都是我们应当尽量避免为每个元素都配置一次的工作.Spark 提供 ...