如何在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 ...
随机推荐
- scala 访问阿里云oss
我们的数据一天就一个T,数据量不断增大,集群磁盘有限,所以把冷数据放到了oss,偶尔会使用到冷数据,如果使用的时候还的从oss上拉数据这样很浪费时间后来想了个办法可以直接获取到oss上的数据.案例:o ...
- 如何最简便的利用Python实现数据可视化?当然离不开matplotlib!
01|Figure和Subplot: matplotlib的图像全部在figure对象里面,就像是一片画布.figsize是figure的一个设置大小的属性.一个figure里面可以有无数个subpl ...
- Django的ORM操作
ORM操作 select * from tb where id > 1 # 对应关系 models.tb.objects.filter(id__gt=1) models.tb.objects.f ...
- hibernate MTM 联合主键
//适用于表里没有其他列,只有主键 //Course.java实体类 package com.tao.pojo; import java.util.HashSet; import java.util. ...
- LOJ_6045_「雅礼集训 2017 Day8」价 _最小割
LOJ_6045_「雅礼集训 2017 Day8」价 _最小割 描述: 有$n$种减肥药,$n$种药材,每种减肥药有一些对应的药材和一个收益. 假设选择吃下$K$种减肥药,那么需要这$K$种减肥药包含 ...
- BZOJ_3994_[SDOI2015]约数个数和_莫比乌斯反演
BZOJ_3994_[SDOI2015]约数个数和_莫比乌斯反演 Description 设d(x)为x的约数个数,给定N.M,求 Input 输入文件包含多组测试数据. 第一行,一个整数T,表 ...
- 在Jenkins中使用sonar进行静态代码检查
要解决的问题 jenkins自动构建完成后,希望能通过sonar静态代码检查生成一份报告,给与开发人员对当前代码的做一个质量评估和修改意见 1.安装并配置sonar服务器 懒得说,跟着官方文档走就行, ...
- Ubuntu 18 安装chrome
1.下载chrome文件 32位使用如下命令 wget https://dl.google.com/linux/direct/google-chrome-stable_current_i386.deb ...
- Shell 脚本中调用另一个 Shell 脚本的三种方式
主要以下有几种方式: Command Explanation fork 新开一个子 Shell 执行,子 Shell 可以从父 Shell 继承环境变量,但是子 Shell 中的环境变量不会带回给父 ...
- ASP.NET中的Cookie对象
1.Cookie对象 Cookie对象一般用于在客户端保存一些针对某个用户的信息. Cookie本质上只是一小段文本字符串,改字符串一般保存在用户计算机特定文件夹下的某个文件中,每个Cookie都保存 ...