如何在ASP.NET Core程序启动时运行异步任务(2)
原文:Running async tasks on app startup in ASP.NET Core (Part 2)
作者:Andrew Lock
译者:Lamond Lu

在我的上一篇博客中,我介绍了如何在ASP.NET Core应用程序启动时运行一些一次性异步任务。本篇博客将继续讨论上一篇的内容,如果你还没有读过,我建议你先读一下前一篇。
在本篇博客中,我将展示上一篇博文中提出的“在Program.cs中手动运行异步任务”的实现方法。该实现会使用一些简单的接口和类来封装应用程序启动时的运行任务逻辑。我还会展示一个替代方法,这个替代方法是在Kestral服务器启动时,使用IServer接口。
在应用程序启动时运行异步任务
这里我们先回顾一下上一遍博客内容,在上一篇中,我们试图寻找一种方案,允许我们在ASP.NET Core应用程序启动时执行一些异步任务。这些任务应该是在ASP.NET Core应用程序启动之前执行,但是由于这些任务可能需要读取配置或者使用服务,所以它们只能在ASP.NET Core的依赖注入容器配置完成后执行。数据库迁移,填充缓存都可以这种异步任务的使用场景。
我们在一篇文章的末尾提出了一个相对完善的解决方案,这个方案是在Program.cs中“手动”运行任务。运行任务的时机是在IWebHostBuilder.Build()和IWebHost.RunAsync()之间。
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>();
}
这种实现方式是可行的,但是有点乱。这里我们将许多不应该属于Program.cs职责的代码放在了Program.cs中,让它看起来有点臃肿了,所以这里我们需要将数据库迁移相关的代码移到另外一个类中。
这里更麻烦的问题是,我们必须要手动调用任务。如果你在多个应用程序中使用相同的模式,那么最好能改成自动调用任务。
在依赖注入容器中注册启动任务
这里我将使用基于IStartupFilter和IHostService使用的模式。它们允许你在依赖注入容器中注册它们的实现类,并在应用程序启动前获取到这些接口的所有实现类,并依次执行它们。
所以,这里首先我们创建一个简单的接口来启动任务。
public interface IStartupTask
{
Task ExecuteAsync(CancellationToken cancellationToken = default);
}
并且创建一个在依赖注入容器中注册任务的便捷方法。
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
where T : class, IStartupTask
=> services.AddTransient<IStartupTask, T>();
}
最后,我们添加一个扩展方法,在应用程序启动时找到所有已注册的IStartupTasks,按顺序运行它们,然后启动IWebHost:
public static class StartupTaskWebHostExtensions
{
public static async Task RunWithTasksAsync(this IWebHost webHost, CancellationToken cancellationToken = default)
{
var startupTasks = webHost.Services.GetServices<IStartupTask>();
foreach (var startupTask in startupTasks)
{
await startupTask.ExecuteAsync(cancellationToken);
}
await webHost.RunAsync(cancellationToken);
}
}
以上就是所有的代码。
下面为了看一下它的实际效果,我将继续使用上一篇中EF Core数据库迁移的例子
例子:异步迁移数据库
实现IStartupTask和实现IStartupFilter非常的相似。你可以从依赖注入容器中注入服务。为了使用依赖注入容器中的服务,这里我们需要手动注入一个IServiceProvider对象,并手动创建一个Scoped服务。
EF Core的数据库迁移启动任务类似以下代码:
public class MigratorStartupFilter: IStartupTask
{
private readonly IServiceProvider _serviceProvider;
public MigratorStartupFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task ExecuteAsync(CancellationToken cancellationToken = default)
{
using(var scope = _seviceProvider.CreateScope())
{
var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
await myDbContext.Database.MigrateAsync();
}
}
}
现在,我们可以在ConfigureServices方法中使用依赖注入容器添加启动任务了。
public void ConfigureServices(IServiceCollection services)
{
services.MyDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration
.GetConnectionString("DefaultConnection")));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddStartupTask<MigrationStartupTask>();
}
最后我们更新一下Program.cs, 使用RunWithTasksAsync()方法替换Run()方法。
public class Program
{
public static async Task Main(string[] args)
{
await CreateWebHostBuilder(args)
.Build()
.RunWithTasksAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
以上代码利用了C# 7.1中引入的异步Task Main的特性。从功能上来说,它与我上一篇博客中的手动代码等同,但是它有一些优点。
- 它的任务实现代码没有放在
Program.cs中。 - 由于上一条的优点,开发人员可以很容易的添加额外的任务。
- 如果不运行任何任务,它的功能和
RunAsync是一样的
对于以上方案,有一个问题需要注意。这里我们定义的任务会在IConfiguration和依赖注入容器配置完成之后运行,这也就意味着,当任务执行时,所有的IStartupFilter都没有运行,中间件管道也没有配置。
就我个人而言,我不认为这是一个问题,因为我暂时想不出任何可能。到目前为止,我所编写的任务都不依赖于IStartupFilter和中间件管道。但这也并不意味着没有这种可能。
不幸的是,使用当前的WebHost代码并没有简单的方法(尽管 在.NET Core 3.0中当ASP.NET Core作为IHostedService运行时,这可能会发生变化)。 问题是应用程序是引导(通过配置中间件管道并运行IStartupFilters)和启动在同一个函数中。 当你在Program.cs中调用WebHost.Run()时,在内部程序会调用WebHost.StartAsync,如下所示,为简洁起见,其中只包含了日志记录和一些其他次要代码:
public virtual async Task StartAsync(CancellationToken cancellationToken = default)
{
_logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();
var application = BuildApplication();
_applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
_hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory);
await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);
_applicationLifetime?.NotifyStarted();
await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);
}
这里问题是我们想要在BuildApplication()和Server.StartAsync之间插入代码,但是现在没有这样做的机制。
我不确定我所给出的解决方案是否优雅,但它可以工作,并为消费者提供更好的体验,因为他们不需要修改Program.cs
使用IServer的替代方案
为了实现在BuildApplication()和Server.StartAsync()之间运行异步代码,我能想到的唯一办法是我们自己的实现一个IServer实现(Kestrel)! 对你来说,听到这个可能感觉非常可怕 - 但是我们真的不打算更换服务器,我们只是去装饰它。
public class TaskExecutingServer : IServer
{
private readonly IServer _server;
private readonly IEnumerable<IStartupTask> _startupTasks;
public TaskExecutingServer(IServer server, IEnumerable<IStartupTask> startupTasks)
{
_server = server;
_startupTasks = startupTasks;
}
public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{
foreach (var startupTask in _startupTasks)
{
await startupTask.ExecuteAsync(cancellationToken);
}
await _server.StartAsync(application, cancellationToken);
}
public IFeatureCollection Features => _server.Features;
public void Dispose() => _server.Dispose();
public Task StopAsync(CancellationToken cancellationToken) => _server.StopAsync(cancellationToken);
}
TaskExecutingServer在其构造函数中获取了一个IServer实例 - 这是ASP.NET Core注册的原始Kestral服务器。我们将大部分IServer的接口实现直接委托给Kestrel,我们只是拦截对StartAsync的调用并首先运行注入的任务。
这个实现最困难部分是使装饰器正常工作。正如我在上一篇文章中所讨论的那样,使用带有默认ASP.NET Core容器的装饰可能会非常棘手。我通常使用Scrutor来创建装饰器,但是如果你不想依赖另一个库,你总是可以手动进行装饰, 但一定要看看Scrutor是如何做到这一点的!
下面我们添加一个用于添加IStartupTask的扩展方法, 这个扩展方法做了两件事,一是将IStartupTask注册到依赖注入容器中,二是装饰了之前注册的IServer实例(这里为了简洁,我省略了Decorate方法的实现)。如果它发现IServer已经被装饰,它会跳过第二步,这样你就可以安全的多次调用AddStartupTask方法。
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddStartupTask<TStartupTask>(this IServiceCollection services)
where TStartupTask : class, IStartupTask
=> services
.AddTransient<IStartupTask, TStartupTask>()
.AddTaskExecutingServer();
private static IServiceCollection AddTaskExecutingServer(this IServiceCollection services)
{
var decoratorType = typeof(TaskExecutingServer);
if (services.Any(service => service.ImplementationType == decoratorType))
{
return services;
}
return services.Decorate<IServer, TaskExecutingServer>();
}
}
使用这两段代码,我们不再需要再对Program.cs文件进行任何更改,并且我们是在完全构建应用程序后执行我们的任务,这其中也包括IStartupFilters和中间件管道。
启动过程的序列图现在看起来有点像这样:

以上就是这种实现方式全部的内容。它的代码非常少, 以至于我自己都在考虑是否要自己编写一个库。不过最后我还是在GitHub和Nuget上创建了一个库NetEscapades.AspNetCore.StartupTasks
这里我只编写了使用后一种IServer实现的库,因为它更容易使用,而且Thomas Levesque已经编写针对第一种方法可用的NuGet包。
在GitHub的实现中,我手动构造了装饰器,以避免强制依赖Scrutor。 但最好的方法可能就是将代码复制并粘贴到您自己的项目中。
总结
在这篇博文中,我展示了两种在ASP.NET Core应用程序启动时异步运行任务的方法。 第一种方法需要稍微修改Program.cs,但是“更安全”,因为它不需要修改像IServer这样的内部实现细节。 第二种方法是装饰IServer,提供更好的用户体验,但感觉更加笨拙。
如何在ASP.NET Core程序启动时运行异步任务(2)的更多相关文章
- 如何在ASP.NET Core程序启动时运行异步任务(3)
原文:Running async tasks on app startup in ASP.NET Core (Part 3) 作者: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 程序启动前运行你的代码
一.前言 在进行 Web 项目开发的过程中,可能会存在一些需要经常访问的静态数据,针对这种在程序运行过程中可能几乎不会发生变化的数据,我们可以尝试在程序运行前写入到缓存中,这样在系统后续使用时就可以直 ...
- 探索ASP.Net Core 3.0系列四:在ASP.NET Core 3.0的应用中启动时运行异步任务
前言:在本文中,我将介绍ASP.NET Core 3.0 WebHost的微小更改如何使使用IHostedService在应用程序启动时更轻松地运行异步任务. 翻译 :Andrew Lock ht ...
- ASP.NET Core 3.x启动时运行异步任务(一)
这是一个大的题目,需要用几篇文章来说清楚.这是第一篇. 一.前言 在我们的项目中,有时候我们需要在应用程序启动前执行一些一次性的逻辑.比方说:验证配置的正确性.填充缓存.或者运行数据库清理/迁移等 ...
- ASP.NET Core 3.x启动时运行异步任务(二)
这一篇是接着前一篇在写的.如果没有看过前一篇文章,建议先去看一下前一篇,这儿是传送门 一.前言 前一篇文章,我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点.最 ...
- ASP.NET Core 的启动和运行机制
目录 ASP .NET Core 的运行机制 ASP .NET Core 的启动 ASP .NET Core 的管道和中间件 参考 ASP .NET Core 的运行机制 Web Server: AS ...
- 如何在ASP.NET Core中使用JSON Patch
原文: JSON Patch With ASP.NET Core 作者:.NET Core Tutorials 译文:如何在ASP.NET Core中使用JSON Patch 地址:https://w ...
- 如何在 asp.net core 3.x 的 startup.cs 文件中获取注入的服务
一.前言 从 18 年开始接触 .NET Core 开始,在私底下.工作中也开始慢慢从传统的 mvc 前后端一把梭,开始转向 web api + vue,之前自己有个半成品的 asp.net core ...
随机推荐
- 安装Navicat for MySQL
注: 以下内容引自 https://www.cnblogs.com/da19951208/p/6403607.html Navicat for MySQL下载.安装与破解 一:下载Navicat ...
- 记录一波由会话堵塞导致tomcat应用故障事件
一.故障基本信息 发生时间 消除时间 故障历时 故障类别 影响 2018-5-17 18:14:30 2018-05-18 08:58:15 16小时 应用故障 业务瘫痪,用户投诉 二.故障现象 AP ...
- 【cogs 775】山海经 ——Segment Tree
题目链接: TP 题解: 我数据结构真心是弱啊= =. 线段树好厉害啊,一直不会区间最大连续和,今天刚学习了一下233. 维护前缀最大和后缀最大,越界最大(?),再维护一个区间最大,瞎搞 ...
- Spring IOC(二)容器初始化
本系列目录: Spring IOC(一)概览 Spring IOC(二)容器初始化 Spring IOC(三)依赖注入 Spring IOC(四)总结 目录 一.ApplicationContext接 ...
- 配置(迁移)Laravel的注意事项
1.如果Laravel是在Linux下运行,如果权限不足,会报错 2.如果是从git上clone下来的项目,需要安装composer,切到项目根目录下 composer install compose ...
- Go 实现 自动检索 API 错误码代码行 并 打印成文档,例 markDown 形式等
作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguan ...
- Python安装和配置
在我厂呆了快一年,终于等来了转岗机会,而且现在正在调动到新成立的AI战略部门,心里无比欣喜和激动.自己作为一个小白,终于有机会踏入AI领域,离自己的梦想更近了一步,个人感到无比的幸运,仿佛天生就有上天 ...
- 集群IPtables转发与防火墙
子网集群通过接入公网的服务器Iptables转发上网 1. 对iptables进行初始化工作 清空filter表 iptables -F 清空nat表 iptables -t nat -F 默认禁止所 ...
- 在javaScript中检测数据类型的几种方式
类型检测的方法 typeof instanceof Object.protype.toString constructor duck type:鸭子类型 typeof 返回一个字符串,适合函数对象和基 ...
- PHP 中move_uploaded_file 上传中文文件名失败
项目需要上传文件名保持不变,发现上传中文失败:错误如下: move_uploaded_file(public/upload/files//-/\开密二次开发.rar): failed to open ...