原文:如何在ASP.NET Core程序启动时运行异步任务(2)
原文:Running async tasks on app startup in ASP.NET Core (Part 2)
作者:Andrew Lock
译者:Lamond Lu
在我的上一篇博客中,我介绍了如何在ASP.NET Core应用程序启动时运行一些一次性异步任务。本篇博客将继续讨论上一篇的内容,如果你还没有读过,我建议你先读一下前一篇。
在本篇博客中,我将展示上一篇博文中提出的“在
在应用程序启动时运行异步任务#
这里我们先回顾一下上一遍博客内容,在上一篇中,我们试图寻找一种方案,允许我们在ASP.NET Core应用程序启动时执行一些异步任务。这些任务应该是在ASP.NET Core应用程序启动之前执行,但是由于这些任务可能需要读取配置或者使用服务,所以它们只能在ASP.NET Core的依赖注入容器配置完成后执行。数据库迁移,填充缓存都可以这种异步任务的使用场景。
我们在一篇文章的末尾提出了一个相对完善的解决方案,这个方案是在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_0" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>Program</SPAN> { <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> <SPAN class=hljs-keyword>async</SPAN> Task <SPAN class=hljs-title>Main</SPAN>(<SPAN class=hljs-params><SPAN class=hljs-keyword>string</SPAN>[] args</SPAN>) </SPAN>{ IWebHost webHost = CreateWebHostBuilder(args).Build(); <SPAN class=hljs-keyword>using</SPAN> (<SPAN class=hljs-keyword>var</SPAN> scope = webHost.Services.CreateScope()) { <SPAN class=hljs-keyword>var</SPAN> myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>(); <SPAN class=hljs-keyword>await</SPAN> myDbContext.Database.MigrateAsync(); } <SPAN class=hljs-keyword>await</SPAN> webHost.RunAsync(); } <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> IWebHostBuilder <SPAN class=hljs-title>CreateWebHostBuilder</SPAN>(<SPAN class=hljs-params><SPAN class=hljs-keyword>string</SPAN>[] args</SPAN>) </SPAN>=> WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); } |
这种实现方式是可行的,但是有点乱。这里我们将许多不应该属于
这里更麻烦的问题是,我们必须要手动调用任务。如果你在多个应用程序中使用相同的模式,那么最好能改成自动调用任务。
在依赖注入容器中注册启动任务#
这里我将使用基于
所以,这里首先我们创建一个简单的接口来启动任务。
1 2 3 4 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_1" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>interface</SPAN> <SPAN class=hljs-title>IStartupTask</SPAN> { <SPAN class=hljs-function>Task <SPAN class=hljs-title>ExecuteAsync</SPAN>(<SPAN class=hljs-params>CancellationToken cancellationToken = <SPAN class=hljs-keyword>default</SPAN></SPAN>)</SPAN>; } |
并且创建一个在依赖注入容器中注册任务的便捷方法。
1 2 3 4 5 6 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_2" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>ServiceCollectionExtensions</SPAN> { <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> IServiceCollection AddStartupTask<T>(<SPAN class=hljs-keyword>this</SPAN> IServiceCollection services) <SPAN class=hljs-keyword>where</SPAN> T : <SPAN class=hljs-keyword>class</SPAN>, <SPAN class=hljs-title>IStartupTask</SPAN> => services.AddTransient<IStartupTask, T>(); } |
最后,我们添加一个扩展方法,在应用程序启动时找到所有已注册的IStartupTasks,按顺序运行它们,然后启动IWebHost:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_3" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>StartupTaskWebHostExtensions</SPAN> { <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> <SPAN class=hljs-keyword>async</SPAN> Task <SPAN class=hljs-title>RunWithTasksAsync</SPAN>(<SPAN class=hljs-params><SPAN class=hljs-keyword>this</SPAN> IWebHost webHost, CancellationToken cancellationToken = <SPAN class=hljs-keyword>default</SPAN></SPAN>) </SPAN>{ <SPAN class=hljs-keyword>var</SPAN> startupTasks = webHost.Services.GetServices<IStartupTask>(); <SPAN class=hljs-keyword>foreach</SPAN> (<SPAN class=hljs-keyword>var</SPAN> startupTask <SPAN class=hljs-keyword>in</SPAN> startupTasks) { <SPAN class=hljs-keyword>await</SPAN> startupTask.ExecuteAsync(cancellationToken); } <SPAN class=hljs-keyword>await</SPAN> webHost.RunAsync(cancellationToken); } } |
以上就是所有的代码。
下面为了看一下它的实际效果,我将继续使用上一篇中EF Core数据库迁移的例子
例子:异步迁移数据库#
实现
EF Core的数据库迁移启动任务类似以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_4" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>MigratorStartupFilter</SPAN>: <SPAN class=hljs-title>IStartupTask</SPAN> { <SPAN class=hljs-keyword>private</SPAN> <SPAN class=hljs-keyword>readonly</SPAN> IServiceProvider _serviceProvider; <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-title>MigratorStartupFilter</SPAN>(<SPAN class=hljs-params>IServiceProvider serviceProvider</SPAN>) </SPAN>{ _serviceProvider = serviceProvider; } <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> Task <SPAN class=hljs-title>ExecuteAsync</SPAN>(<SPAN class=hljs-params>CancellationToken cancellationToken = <SPAN class=hljs-keyword>default</SPAN></SPAN>) </SPAN>{ <SPAN class=hljs-keyword>using</SPAN>(<SPAN class=hljs-keyword>var</SPAN> scope = _seviceProvider.CreateScope()) { <SPAN class=hljs-keyword>var</SPAN> myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>(); <SPAN class=hljs-keyword>await</SPAN> myDbContext.Database.MigrateAsync(); } } } |
现在,我们可以在
1 2 3 4 5 6 7 8 9 10 11 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_5" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>ConfigureServices</SPAN>(<SPAN class=hljs-params>IServiceCollection services</SPAN>) </SPAN>{ services.MyDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration .GetConnectionString(<SPAN class=hljs-string>"DefaultConnection"</SPAN>))); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddStartupTask<MigrationStartupTask>(); } |
最后我们更新一下
1 2 3 4 5 6 7 8 9 10 11 12 13 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_6" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>Program</SPAN> { <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> <SPAN class=hljs-keyword>async</SPAN> Task <SPAN class=hljs-title>Main</SPAN>(<SPAN class=hljs-params><SPAN class=hljs-keyword>string</SPAN>[] args</SPAN>) </SPAN>{ <SPAN class=hljs-keyword>await</SPAN> CreateWebHostBuilder(args) .Build() .RunWithTasksAsync(); } <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> IWebHostBuilder <SPAN class=hljs-title>CreateWebHostBuilder</SPAN>(<SPAN class=hljs-params><SPAN class=hljs-keyword>string</SPAN>[] args</SPAN>) </SPAN>=> WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); } |
以上代码利用了C# 7.1中引入的异步Task Main的特性。从功能上来说,它与我上一篇博客中的手动代码等同,但是它有一些优点。
- 它的任务实现代码没有放在
Program.cs 中。
- 由于上一条的优点,开发人员可以很容易的添加额外的任务。
- 如果不运行任何任务,它的功能和
RunAsync 是一样的
对于以上方案,有一个问题需要注意。这里我们定义的任务会在
就我个人而言,我不认为这是一个问题,因为我暂时想不出任何可能。到目前为止,我所编写的任务都不依赖于
不幸的是,使用当前的WebHost代码并没有简单的方法(尽管 在.NET Core 3.0中当ASP.NET Core作为IHostedService运行时,这可能会发生变化)。 问题是应用程序是引导(通过配置中间件管道并运行IStartupFilters)和启动在同一个函数中。 当你在Program.cs中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_7" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>virtual</SPAN> <SPAN class=hljs-keyword>async</SPAN> Task <SPAN class=hljs-title>StartAsync</SPAN>(<SPAN class=hljs-params>CancellationToken cancellationToken = <SPAN class=hljs-keyword>default</SPAN></SPAN>) </SPAN>{ _logger = _applicationServices.GetRequiredService<ILogger<WebHost>>(); <SPAN class=hljs-keyword>var</SPAN> application = BuildApplication(); _applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() <SPAN class=hljs-keyword>as</SPAN> ApplicationLifetime; _hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>(); <SPAN class=hljs-keyword>var</SPAN> diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>(); <SPAN class=hljs-keyword>var</SPAN> httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>(); <SPAN class=hljs-keyword>var</SPAN> hostingApp = <SPAN class=hljs-keyword>new</SPAN> HostingApplication(application, _logger, diagnosticSource, httpContextFactory); <SPAN class=hljs-keyword>await</SPAN> Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(<SPAN class=hljs-literal>false</SPAN>); _applicationLifetime?.NotifyStarted(); <SPAN class=hljs-keyword>await</SPAN> _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(<SPAN class=hljs-literal>false</SPAN>); } |
这里问题是我们想要在
我不确定我所给出的解决方案是否优雅,但它可以工作,并为消费者提供更好的体验,因为他们不需要修改Program.cs
使用IServer 的替代方案#
为了实现在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_8" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>TaskExecutingServer</SPAN> : <SPAN class=hljs-title>IServer</SPAN> { <SPAN class=hljs-keyword>private</SPAN> <SPAN class=hljs-keyword>readonly</SPAN> IServer _server; <SPAN class=hljs-keyword>private</SPAN> <SPAN class=hljs-keyword>readonly</SPAN> IEnumerable<IStartupTask> _startupTasks; <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-title>TaskExecutingServer</SPAN>(<SPAN class=hljs-params>IServer server, IEnumerable<IStartupTask> startupTasks</SPAN>) </SPAN>{ _server = server; _startupTasks = startupTasks; } <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>async</SPAN> Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) { <SPAN class=hljs-keyword>foreach</SPAN> (<SPAN class=hljs-keyword>var</SPAN> startupTask <SPAN class=hljs-keyword>in</SPAN> _startupTasks) { <SPAN class=hljs-keyword>await</SPAN> startupTask.ExecuteAsync(cancellationToken); } <SPAN class=hljs-keyword>await</SPAN> _server.StartAsync(application, cancellationToken); } <SPAN class=hljs-keyword>public</SPAN> IFeatureCollection Features => _server.Features; <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>Dispose</SPAN>(<SPAN class=hljs-params></SPAN>) </SPAN>=> _server.Dispose(); <SPAN class=hljs-function><SPAN class=hljs-keyword>public</SPAN> Task <SPAN class=hljs-title>StopAsync</SPAN>(<SPAN class=hljs-params>CancellationToken cancellationToken</SPAN>) </SPAN>=> _server.StopAsync(cancellationToken); } |
这个实现最困难部分是使装饰器正常工作。正如我在上一篇文章中所讨论的那样,使用带有默认ASP.NET Core容器的装饰可能会非常棘手。我通常使用Scrutor来创建装饰器,但是如果你不想依赖另一个库,你总是可以手动进行装饰, 但一定要看看Scrutor是如何做到这一点的!
下面我们添加一个用于添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <DIV class=esa-clipboard-button data-clipboard-target="#copy_target_9" data-tips="复制代码">Copy</DIV><wyn><SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> <SPAN class=hljs-keyword>class</SPAN> <SPAN class=hljs-title>ServiceCollectionExtensions</SPAN> { <SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>static</SPAN> IServiceCollection AddStartupTask<TStartupTask>(<SPAN class=hljs-keyword>this</SPAN> IServiceCollection services) <SPAN class=hljs-keyword>where</SPAN> TStartupTask : <SPAN class=hljs-keyword>class</SPAN>, <SPAN class=hljs-title>IStartupTask</SPAN> => services .AddTransient<IStartupTask, TStartupTask>() .AddTaskExecutingServer(); <SPAN class=hljs-function><SPAN class=hljs-keyword>private</SPAN> <SPAN class=hljs-keyword>static</SPAN> IServiceCollection <SPAN class=hljs-title>AddTaskExecutingServer</SPAN>(<SPAN class=hljs-params><SPAN class=hljs-keyword>this</SPAN> IServiceCollection services</SPAN>) </SPAN>{ <SPAN class=hljs-keyword>var</SPAN> decoratorType = <SPAN class=hljs-keyword>typeof</SPAN>(TaskExecutingServer); <SPAN class=hljs-keyword>if</SPAN> (services.Any(service => service.ImplementationType == decoratorType)) { <SPAN class=hljs-keyword>return</SPAN> services; } <SPAN class=hljs-keyword>return</SPAN> services.Decorate<IServer, TaskExecutingServer>(); } } |
使用这两段代码,我们不再需要再对Program.cs文件进行任何更改,并且我们是在完全构建应用程序后执行我们的任务,这其中也包括IStartupFilters和中间件管道。
启动过程的序列图现在看起来有点像这样:
以上就是这种实现方式全部的内容。它的代码非常少, 以至于我自己都在考虑是否要自己编写一个库。不过最后我还是在GitHub和Nuget上创建了一个库NetEscapades.AspNetCore.StartupTasks
这里我只编写了使用后一种
在GitHub的实现中,我手动构造了装饰器,以避免强制依赖Scrutor。 但最好的方法可能就是将代码复制并粘贴到您自己的项目中。
总结#
在这篇博文中,我展示了两种在ASP.NET Core应用程序启动时异步运行任务的方法。 第一种方法需要稍微修改Program.cs,但是“更安全”,因为它不需要修改像IServer这样的内部实现细节。 第二种方法是装饰IServer,提供更好的用户体验,但感觉更加笨拙。