ASP.NET Core 3.x启动时运行异步任务(一)
这是一个大的题目,需要用几篇文章来说清楚。这是第一篇。
一、前言
在我们的项目中,有时候我们需要在应用程序启动前执行一些一次性的逻辑。比方说:验证配置的正确性、填充缓存、或者运行数据库清理/迁移等。
如何合理、有效、优雅地完成这个任务,是这个文章讨论的主要内容。
要实现这样一个功能,其实我们有几个选择:
- 使用
IStartupFilter
运行同步任务。这是一个内置的解决方案,可以通过一些设置和技巧来运行异步任务; - 使用
IStartupFilter
或IApplicationLifetime
事件来运行异步任务,这是一个可选的方案,但有不足,我们会在后面讲; - 使用
IHostedService
,在不阻塞应用启动的情况下,运行一些一次性的任务;(关于这个内容,我在前一篇文章ASP.NET Core 3.x控制IHostedService启动顺序浅探中有涉及到一部分内容) - 在
Program.cs
中运行异步任务。在大多数情况下,从代码的复杂度到效率上,这都是一个比较好的选择。
为防止非授权转发,这儿给出本文的原文链接:https://www.cnblogs.com/tiger-wang/p/13673046.html
先提个问题:为什么要在应用启动时运行任务?
二、为什么要在应用启动时运行任务?
在应用启动并开始请求服务之前,很多时候需要运行各种初始化工作。
一个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
中直接使用依赖注入。这表示我们可以运行有关系统的任何代码。在前边提到的微软官网的例子中,就是创建了一个基于IStartupFilters
的DataProtectionStartupFilter
来初始化数据保护子系统。
此外,IStartupFilter
允许我们通过向依赖注入容器注册服务来增加要执行的任务。这是一个很有用的特性,表示我们可以注册一个在应用启动时运行的任务,而不需要显式的调用。
但是,这儿有个问题。IStartupFilters
通常运行的是同步的任务。看一下上面的代码,Configure()
方法不返回任务。当然,我们硬要使用异步也是可以的,但一般来说,这不算个好主意。原因我后面会写。
写到这儿,如果对ASP.NET Core架构熟悉,就会引出另一个问题:为什么不用健康检查来确认一次性任务的执行结果?
四、为什么不用健康检查?
运行健康检查,是ASP.NET Core 2.2新引入的一个特性,允许查询通过API(HTTP Endpoint)公开的应用的健康状况。当应用部署在Kubernetes
,或反向代理HAProxy
或Nginx
后面时,可以提供给代理用来检测应用是否准备好开始提供服务。
我们可以使用健康检查来确保应用所有必需的一次性任务完成之前不会开始监听服务。
但是,这种方式会有一点问题。
WebHost
和Kestrel
本身会在一次性任务执行前启动。当然,这时他们还不会接收和处理服务请求,但仍然引出了一些问题:
首先是增加了代码的复杂性。除了一次性任务的代码外,还要增加健康检查来测试任务是否完成,并同步和保持任务的状态;其次,如果任务失败了,应用程序的健康检查将会让应用后续的任务无法继续执行。合理的流程是:应用应该立即失败返回。
这儿主要的原因是:健康检查没有定义如何实际运行任务,而只是定义了任务是否成功完成。相对来说,这种状态机制比较单一,在一些简单的任务中可能适用,但不能全面覆盖一次性任务的全部场景。
五、运行异步任务
前边写了一些不太完美的方法。
现在,我们开始进入运行异步方法的一些步骤。当然,运行异步也会有几种方式,适用性上会有一定的区别。
方式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
本身就是异步的,它提供了StartAsync
和StopAsync
。
这种方式下,我们的代码会是这样:
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
中的配置。不过,这种需求的情况很少。
六、总结
这个部分牵扯到的框架内容比较多。
我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点。
下一篇文章,我会用一些具体的例子,来说清楚这个方式的具体使用,敬请关注。
(未完待续)
![]() |
微信公众号:老王Plus 扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送 本文版权归作者所有,转载请保留此声明和原文链接 |
ASP.NET Core 3.x启动时运行异步任务(一)的更多相关文章
- ASP.NET Core 3.x启动时运行异步任务(二)
这一篇是接着前一篇在写的.如果没有看过前一篇文章,建议先去看一下前一篇,这儿是传送门 一.前言 前一篇文章,我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点.最 ...
- 如何在ASP.NET Core程序启动时运行异步任务(3)
原文:Running async tasks on app startup in ASP.NET Core (Part 3) 作者:Andrew Lock 译者:Lamond Lu 之前我写了两篇有关 ...
- 如何在ASP.NET Core程序启动时运行异步任务(2)
原文:Running async tasks on app startup in ASP.NET Core (Part 2) 作者:Andrew Lock 译者:Lamond Lu 在我的上一篇博客中 ...
- 如何在ASP.NET Core程序启动时运行异步任务(1)
原文:Running async tasks on app startup in ASP.NET Core (Part 1) 作者:Andrew Lock 译者:Lamond Lu 背景 当我们做项目 ...
- 探索ASP.Net Core 3.0系列四:在ASP.NET Core 3.0的应用中启动时运行异步任务
前言:在本文中,我将介绍ASP.NET Core 3.0 WebHost的微小更改如何使使用IHostedService在应用程序启动时更轻松地运行异步任务. 翻译 :Andrew Lock ht ...
- asp.net core 系列 9 三种运行环境和IIS发布
一.在asp.net core中使用多个环境 ASP.NET Core 配置是基于运行时环境, 使用环境变量.ASP.NET Core 在应用启动时读取环境变量ASPNETCORE_ENVIRONME ...
- ASP.NET Core 2.1 使用Docker运行
重要提示,本文为 ASP.NET Core 2.1 如果你是 2.2 那么请将文中的镜像换为 microsoft/dotnet:2.2.0-aspnetcore-runtime 即可,其他操作一样 1 ...
- ASP.Net Core MVC6 RC2 启动过程分析[偏源码分析]
入口程序 如果做过Web之外开发的人,应该记得这个是标准的Console或者Winform的入口.为什么会这样呢? .NET Web Development and Tools Blog ASP.NE ...
- C# ASP.NET Core使用HttpClient的同步和异步请求
引用 Newtonsoft.Json // Post请求 public string PostResponse(string url,string postData,out string status ...
随机推荐
- Mybatis中<![cdata[ ]]>
1.<![cdata[ ]]>介绍 <![cdata[ 内容 ]]>是一种xml语法,在CDATA标记中的信息被解析器原封不动地传给应用程序,并且不解析该段信息中的任何控制标记 ...
- 【AHOI 2015】 小岛 - 最短路
描述 西伯利亚北部的寒地,坐落着由 N 个小岛组成的岛屿群,我们把这些小岛依次编号为 1 到 N . 起初,岛屿之间没有任何的航线.后来随着交通的发展,逐渐出现了一些连通两座小岛的航线.例如增加一条在 ...
- python库安装失败的解决方法
安装python库 在https://www.lfd.uci.edu/~gohlke/pythonlibs 中,搜索对应库名称 选取对应版本下载 在cmd窗口中,用命令 pip install+文件路 ...
- unity探索者之socket传输protobuf字节流(四)
版权声明:本文为原创文章,转载请声明http://www.cnblogs.com/unityExplorer/p/7027659.html 上篇已经把socket的传输说的差不多了,这篇主要是说说断线 ...
- webpack 热替换
一. 使用express.js搭建一个简易服务器demo地址,热替换的 先看包 // 清除重复的文件 "clean-webpack-plugin" // css加载器 " ...
- Linux之lldptool工具
1. 描述当我们想在操作系统里面查看网口和交换机连接的状态信息,我们可以使用lldptool这个工具2.LLDP协议LLDP是Link Layer Discovery Protocol 链路层发现协议 ...
- 基于 abp vNext 微服务开发的敏捷应用构建平台 - 项目介绍
缘起 目前使用ABP框架已经将近3年了,大大小小的项目也陆陆续续做了很多.由于现有信息系统的架构模式是在底层的技术平台上直接构建信息系统并采用技术主导,使用业务无关的编程工具来开发信息系统的缺陷使得系 ...
- 第六篇Scrum冲刺博客--Interesting-Corps
第六篇Scrum冲刺博客 站立式会议 1.会议照片 2.队友完成情况 团队成员 昨日完成 今日计划 鲍鱼铭 搜索页面以及音乐详情页面数据导入及测试 各界面数据请求云函数设计及实现 叶学涛 进行页面的优 ...
- Linux服务器上创建新用户
一.在/home目录下新建userName目录 sudo useradd -m -s /bin/bash userName 二.设置密码 sudo passwd userName
- Java14版本特性【一文了解】
「MoreThanJava」 宣扬的是 「学习,不止 CODE」,本系列 Java 基础教程是自己在结合各方面的知识之后,对 Java 基础的一个总回顾,旨在 「帮助新朋友快速高质量的学习」. 当然 ...