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

这是一个大的题目,需要用几篇文章来说清楚。这是第一篇。

一、前言

在我们的项目中,有时候我们需要在应用程序启动前执行一些一次性的逻辑。比方说:验证配置的正确性、填充缓存、或者运行数据库清理/迁移等。

如何合理、有效、优雅地完成这个任务,是这个文章讨论的主要内容。

要实现这样一个功能,其实我们有几个选择:

  1. 使用IStartupFilter运行同步任务。这是一个内置的解决方案,可以通过一些设置和技巧来运行异步任务;

  2. 使用IStartupFilterIApplicationLifetime事件来运行异步任务,这是一个可选的方案,但有不足,我们会在后面讲;

  3. 使用IHostedService,在不阻塞应用启动的情况下,运行一些一次性的任务;(关于这个内容,我在前一篇文章ASP.NET Core 3.x控制IHostedService启动顺序浅探中有涉及到一部分内容)

  4. Program.cs中运行异步任务。在大多数情况下,从代码的复杂度到效率上,这都是一个比较好的选择。

先提个问题:为什么要在应用启动时运行任务?

二、为什么要在应用启动时运行任务?

在应用启动并开始请求服务之前,很多时候需要运行各种初始化工作。

一个ASP.NET应用启动时,需要完成很多事,例如:

  • 确定当前的宿主环境

  • 加载appsetting.json配置和环境变量

  • 配置并创建依赖注入的容器

  • 配置中间件管道

这是应用启动时要完成的引导内容。

在完成这些内容,运行WebHost并开始监听请求之前,还会有一些一次性任务需要启动,例如:

  • 检查强类型配置的有效性

  • 填充或恢复缓存

  • 数据库清理/迁移(通常来说这不是个好主意,但很多时候没有别的办法)

当然,有些任务也不是一定要在开始监听请求之前运行,这要看具体的运行任务的架构。一般来说,如果缓存处理的完善,是不需要提前启动的。当然,清理/迁移数据库,是必须放在服务启动之前。

在微软官网上,有一个例子是数据保护子系统,用于即时加密(cookie、防伪令牌等),这个就必须在应用监听请求之前完成初始化并加载,这个例子使用了IStartupFilter

三、使用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架构熟悉,就会引出另一个问题:为什么不用健康检查来确认一次性任务的执行结果?

四、为什么不用健康检查?

运行健康检查,是ASP.NET Core 2.2新引入的一个特性,允许查询通过API(HTTP Endpoint)公开的应用的健康状况。当应用部署在Kubernetes,或反向代理HAProxyNginx后面时,可以提供给代理用来检测应用是否准备好开始提供服务。

我们可以使用健康检查来确保应用所有必需的一次性任务完成之前不会开始监听服务。

但是,这种方式会有一点问题。

WebHostKestrel本身会在一次性任务执行前启动。当然,这时他们还不会接收和处理服务请求,但仍然引出了一些问题:

首先是增加了代码的复杂性。除了一次性任务的代码外,还要增加健康检查来测试任务是否完成,并同步和保持任务的状态;其次,如果任务失败了,应用程序的健康检查将会让应用后续的任务无法继续执行。合理的流程是:应用应该立即失败返回。

这儿主要的原因是:健康检查没有定义如何实际运行任务,而只是定义了任务是否成功完成。相对来说,这种状态机制比较单一,在一些简单的任务中可能适用,但不能全面覆盖一次性任务的全部场景。

五、运行异步任务

前边写了一些不太完美的方法。

现在,我们开始进入运行异步方法的一些步骤。当然,运行异步也会有几种方式,适用性上会有一定的区别。

方式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中的配置。不过,这种需求的情况很少。

六、总结

这个部分牵扯到的框架内容比较多。

我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点。

下一篇文章,我会用一些具体的例子,来说清楚这个方式的具体使用,敬请关注。

(未完待续)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307739.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每天一小时python官方文档学习(四)————数据结构之列表

终于进入到第五章数据结构部分了&#xff0c;python中常用的容器有列表、元组、集合和字典&#xff0c;今天主要了解的是最为常用的列表。 5. 数据结构 5.1. 列表的更多特性 对于列表的操作有很多&#xff0c;具体操作看官方文档就行&#xff0c;但是我们对列表有什么操作&a…

leetcode435. 无重叠区间

一&#xff1a;论语 道不同 不相为谋 我们没有理由拿着自己的评判标准 去看待别人所经历的事情&#xff0c;重来就没有真正的感同身受&#xff0c;我们能做的就是尊重他人的看法 &#xff0c;保留自己的态度。 二&#xff1a;题目 三:上码 class Solution { public:/**思路:…

Swagger扩展为你添油加气

关注架构师高级俱乐部开启架构之路不定期福利发放哦~Leon读完需要4分钟速读仅需 2 分钟介绍一款Swagger扩展日常接口开发中都需要用到Swagger来生成接口文档并用 Swagger 自带支持的模拟请求进行测试&#xff0c;但是需要支持认证或者上传文件等操作需要自行去按接口进行开发才…

leetcode763. 划分字母区间

一&#xff1a;论语 保留自己的态度&#xff0c;尊重别人的看法&#xff0c;不强迫别人按照自己的意愿做事。 二:题目 三:上码 class Solution { public:vector<int> partitionLabels(string s) {/**思路:1.在这里我们选取的数据结构是map容器,我们只要下记录每个字母…

每天一小时python官方文档学习(五)————数据结构之元组、集合与字典

昨天介绍完了最常用的列表&#xff0c;之后就是次常用的元组、集合与字典了。 5.3. 元组和序列 元组和之前讲过的列表有很多共同特性&#xff0c;例如索引和切片操作。实际上&#xff0c;他们是 序列 数据类型&#xff08;list, tuple, range&#xff09;中的两种。 一个元组…

跟我一起学.NetCore之依赖注入

前言现阶段而言&#xff0c;依赖注入相关组件如果不会用一两个&#xff0c;感觉在Code的世界里肯定是落伍了&#xff0c;最起码得有工厂模式的思想&#xff0c;知道这样做的好处&#xff1b;提及到依赖注入&#xff0c;通常会关联出两个概念&#xff1a;Ioc(控制反转)和DI(依赖…

每天一小时python官方文档学习(六)————循环和条件控制的进阶用法

学习完数据结构之后&#xff0c;我们就能够对之前在第二天中讲过的流程控制工具&#xff0c;添加更多的用法了&#xff0c;这里主要介绍的是for循环语句和if与while条件判断语句。 5.6. 循环的技巧 我们学过的遍历序列for i in range() 、for i in list() 和 for i in tuple(…

跟我一起学.NetCore之依赖注入作用域和对象释放

前言上一小节简单阐述了依赖注入及Asp.NetCore中自带依赖注入组件的常规用法&#xff0c;其中提到容器管控了自己创建对象的生命周期&#xff0c;包含了三种生命周期&#xff1a;Singleton、Scoped、Transient&#xff0c; 对于Singleton、Transient相对于Scoped来说比较好理解…

leetcode738. 单调递增的数字

一:芭比Q了 又掉一个粉 啊呜呜呜呜呜 如果作为一个领导者来说&#xff0c;首先就是要以身作则&#xff0c;自己都做不到 &#xff0c;那就没什么威信去要求手下人按照要求去做 二:题目 三:上码 class Solution { public:int monotoneIncreasingDigits(int n) {/**思路:1.这…

每天一小时python官方文档学习(七)————模块与包

6. 模块 模块是一个包含Python定义和语句的文件&#xff0c;实际上就是一个正常的.py文件。但是作用和普通需要运行的程序不一样&#xff0c;它实际上有点像函数。如果说函数实现了代码的重用&#xff0c;模块则是实现了函数的重用。我们如果想在不同的程序中使用同一个函数&a…

大揭秘| 我司项目组Gitlab Flow DevOps流程

长话短说&#xff0c;本文全景呈现我司项目组gitlab flow && devopsGit Flow定义了一个项目发布的分支模型&#xff0c;为管理具有预定发布周期的大型项目提供了一个健壮的框架。DevOps 强调的是团队通过自动化的工具协作和高效地沟通来完成软件的生命周期管理&#xf…

leetcode714.买卖股票的

一:题目 二&#xff1a;上码 class Solution { public:int maxProfit(vector<int>& prices, int fee) {/**思路:*/int ans 0;int minPrice prices[0];//最低时买入for(int i 1; i < prices.size(); i) {//低价买入minPrice min(minPrice,prices[i]);if(price…

C++ 学习之旅(1)——编译器Compiler

简单来说&#xff0c;由C代码文件生成可执行文件的过程如下&#xff1a; #mermaid-svg-GQamCVEXMVkYEemz {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GQamCVEXMVkYEemz .error-icon{fill:#552222;}#mermaid-svg-…

掌握Rabbitmq几个重要概念,从一条消息说起

RabbitMQ 是功能强大的开源消息代理。根据官网称&#xff1a;也是使用量最广泛的消息队列。就像他的口号“Messaging that just works”&#xff0c;开箱即用使用简单&#xff0c;支持多种消息传输协议&#xff08;AMQP、STOMP、MQTT&#xff09;。一个应用程序或者服务如何使用…

122. 买卖股票的时机

一&#xff1a;题目 二&#xff1a;上码 class Solution { public:int maxProfit(vector<int>& prices) {/**思路:1.这里用的是贪心算法&#xff08;我们每隔两天计算一次赚的钱 只要大于0的话 那就是赚的&#xff09;2.我们手里最多只能有一只股票,所以我们可以当…

C++ 学习之旅(2)——链接器Linker

每一个.cpp文件经过编译之后都会生成对应的.obj文件&#xff0c;然后通过链接器把它们进行链接&#xff0c;最后就可以生成.exe可执行文件了。 举个例子&#xff0c;假设我们有一个 Math.cpp 文件和 Log.cpp 文件&#xff1a; Math.cpp #include <iostream>void Log(c…

使用 WPF 版简易 SIP 服务器向 GB28181 摄像头发送直播请求

使用 WPF 版简易 SIP 服务器向 GB28181 摄像头发送直播请求目录一、引言二、项目渊源三、软件使用及 SIP INVITE 流程(一) 注册和心跳(二) 直播 INVITE四、注意事项五、资源独立观察员 2020 年 9 月 16 日一、引言之前写过一篇博客《使用 GB28181.Solution ZLMediaKit MediaSe…

leetcode968. 监控二叉树

一:论语 这个有意思,我们可以从中得出的是&#xff0c;一个人过错 其实是潜意思决定的 行为见品质 但知错更改也是nice的 二:题目 三:上码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* Tr…

C++ 学习之旅(3)——头文件Header

我们知道&#xff0c;在 C 中&#xff0c;函数只能定义一次&#xff0c;而在 cpp 文件中如果想使用其他 cpp 文件中定义了的函数&#xff0c;就必须声明&#xff0c;这样才能通过编译&#xff0c;然后链接器才会在调用函数时找到该函数的定义。那么当函数声明很多的时候&#x…

虚虚实实,亦假亦真的 ValueTuple,绝对能眩晕你

一&#xff1a;背景 1. 讲故事前几天在写一个api接口&#xff0c;需要对衣物表进行分页查询&#xff0c;查询的output需要返回两个信息&#xff0c;一个是 totalCount,一个是 clothesList,在以前我可能需要封装一个 PagedClothes 类&#xff0c;如下代码:public class PagedClo…