原文: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中,让它看起来有点臃肿了,所以这里我们需要将数据库迁移相关的代码移到另外一个类中。

这里更麻烦的问题是,我们必须要手动调用任务。如果你在多个应用程序中使用相同的模式,那么最好能改成自动调用任务。

在依赖注入容器中注册启动任务

这里我将使用基于IStartupFilterIHostService使用的模式。它们允许你在依赖注入容器中注册它们的实现类,并在应用程序启动前获取到这些接口的所有实现类,并依次执行它们。

所以,这里首先我们创建一个简单的接口来启动任务。

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和中间件管道。

启动过程的序列图现在看起来有点像这样:

以上就是这种实现方式全部的内容。它的代码非常少, 以至于我自己都在考虑是否要自己编写一个库。不过最后我还是在GitHubNuget上创建了一个库NetEscapades.AspNetCore.StartupTasks

这里我只编写了使用后一种IServer实现的库,因为它更容易使用,而且Thomas Levesque已经编写针对第一种方法可用的NuGet包。

在GitHub的实现中,我手动构造了装饰器,以避免强制依赖Scrutor。 但最好的方法可能就是将代码复制并粘贴到您自己的项目中。

总结

在这篇博文中,我展示了两种在ASP.NET Core应用程序启动时异步运行任务的方法。 第一种方法需要稍微修改Program.cs,但是“更安全”,因为它不需要修改像IServer这样的内部实现细节。 第二种方法是装饰IServer,提供更好的用户体验,但感觉更加笨拙。

如何在ASP.NET Core程序启动时运行异步任务(2)的更多相关文章

  1. 如何在ASP.NET Core程序启动时运行异步任务(3)

    原文:Running async tasks on app startup in ASP.NET Core (Part 3) 作者:Andrew Lock 译者:Lamond Lu 之前我写了两篇有关 ...

  2. 如何在ASP.NET Core程序启动时运行异步任务(1)

    原文:Running async tasks on app startup in ASP.NET Core (Part 1) 作者:Andrew Lock 译者:Lamond Lu 背景 当我们做项目 ...

  3. 在 ASP.NET Core 程序启动前运行你的代码

    一.前言 在进行 Web 项目开发的过程中,可能会存在一些需要经常访问的静态数据,针对这种在程序运行过程中可能几乎不会发生变化的数据,我们可以尝试在程序运行前写入到缓存中,这样在系统后续使用时就可以直 ...

  4. 探索ASP.Net Core 3.0系列四:在ASP.NET Core 3.0的应用中启动时运行异步任务

    前言:在本文中,我将介绍ASP.NET Core 3.0 WebHost的微小更改如何使使用IHostedService在应用程序启动时更轻松地运行异步任务. 翻译 :Andrew Lock   ht ...

  5. ASP.NET Core 的启动和运行机制

    目录 ASP .NET Core 的运行机制 ASP .NET Core 的启动 ASP .NET Core 的管道和中间件 参考 ASP .NET Core 的运行机制 Web Server: AS ...

  6. 如何在ASP.NET Core中使用JSON Patch

    原文: JSON Patch With ASP.NET Core 作者:.NET Core Tutorials 译文:如何在ASP.NET Core中使用JSON Patch 地址:https://w ...

  7. 如何在ASP.NET Core中自定义Azure Storage File Provider

    文章标题:如何在ASP.NET Core中自定义Azure Storage File Provider 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p ...

  8. Asp.Net Core 程序部署到Linux(centos)生产环境(二):docker部署

    运行环境 照例,先亮环境:软件的话我这里假设你已经批准好了.net core 运行环境,未配置可以看我的这篇[linux(centos)搭建.net core 运行环境] 腾讯云 centos:7.2 ...

  9. Asp.Net Core 程序部署到Linux(centos)生产环境(一):普通部署

    运行环境 照例,先亮底 centos:7.2 cpu:1核 2G内存 1M带宽 辅助工具:xshell xftp 搭建.net core运行环境 .net core 的运行环境我单独写了一篇,请看我的 ...

随机推荐

  1. ReactNative新手学习之路07ListView_ renderHeader使用StaticContainer

    react native新手学习之路07ListView_ renderHeader使用StaticContainer 1.某些特殊场景需要用ScrollView滚动和ListView配合但是不幸运的 ...

  2. For嵌套输出图形

    /*输出此图形    *   * *  * * * * * * ** * * * *  * * * *   * * *   * *     *解析:可以把此图形看成两部分----*---* *--* ...

  3. 自定义圆的半径layout

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:too ...

  4. day3-Python集合、函数、文件操作,python包的概念

    本节大纲: 1 python程序由包(package).模块(module)和函数组成.包是由一系列模块组成的集合.模块是处理某一类问题的函数和类的集合. 2 包就是一个完成特定任务的工具箱. 3 包 ...

  5. Bash Shell字符串操作小结

    装载自:http://my.oschina.net/aiguozhe/blog/41557 1. 取长度 str="abcd" expr length $str # 4 echo ...

  6. nth_element学习

    今天学习到STL中的nth_element,她是一个默认能求第k小的数的方法,需要的头文件为algorithm. 默认为:nth_element(start, start+n, end) 使第n大元素 ...

  7. PHP学习笔记11-表单

    处理GET请求 实现的功能是输入姓名后页面显示“Hello XXX” 创建html文件hello.html: <!DOCTYPE html> <html> <head l ...

  8. 织梦DedeCMS调用二级子栏目或者多级栏目的方法

    图2 当前栏目typeid值为3,所以代码如下: {dede:channelartlist typeid='3,3'} <a href="{dede:field name='typeu ...

  9. Windows上Python2与Python3共存

    首先安装好python2与python3版本 因为安装顺序的不同,所以系统默认的版本也不同.如果先安装的是python,那么系统默认的就是python2 如果根据需求需要使用不同的版本,可以使用py命 ...

  10. WSL(Windows Subsystem for Linux)--Pico Process Overview

    [转载] Windows Subsystem for Linux -- Pico Process Overview Overview This post discusses pico processe ...