原文: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 3.x启动时运行异步任务(一)

    这是一个大的题目,需要用几篇文章来说清楚.这是第一篇.   一.前言 在我们的项目中,有时候我们需要在应用程序启动前执行一些一次性的逻辑.比方说:验证配置的正确性.填充缓存.或者运行数据库清理/迁移等 ...

  6. ASP.NET Core 3.x启动时运行异步任务(二)

    这一篇是接着前一篇在写的.如果没有看过前一篇文章,建议先去看一下前一篇,这儿是传送门   一.前言 前一篇文章,我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点.最 ...

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

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

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

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

  9. 如何在 asp.net core 3.x 的 startup.cs 文件中获取注入的服务

    一.前言 从 18 年开始接触 .NET Core 开始,在私底下.工作中也开始慢慢从传统的 mvc 前后端一把梭,开始转向 web api + vue,之前自己有个半成品的 asp.net core ...

随机推荐

  1. 转载:selenium的wait.until()

    package com.test.elementwait; import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;imp ...

  2. nsqlookup_protocol_v1.go

    , atomic.LoadInt64(&client.peerInfo.lastUpdate))         now := time.Now()         p.ctx.nsqlook ...

  3. uoj123 【NOI2013】小Q的修炼

    搞了一下午+半晚上.其实不是很难. 提答题重要的是要发现数据的特殊性质,然后根据不同数据写出不同的算法获得其对应的分数. 首先前两个测试点我们发现可以直接暴搜通过,事实上对于每个数据都暴搜加上一定的次 ...

  4. BZOJ2751 [HAOI2012]容易题

    Description 为了使得大家高兴,小Q特意出个自认为的简单题(easy)来满足大家,这道简单题是描述如下: 有一个数列A已知对于所有的A[i]都是1~n的自然数,并且知道对于一些A[i]不能取 ...

  5. 【JVM虚拟机】(8)--深入理解Class中--方法、属性表集合

    #[JVM虚拟机](8)--深入理解Class中--方法.属性表集合 之前有关class文件已经写了两篇博客: 1.[JVM虚拟机](5)---深入理解JVM-Class中常量池 2.[JVM虚拟机] ...

  6. C#之Socket的简单使用

    Socket是一种通信TCP/IP的通讯接口,也就是HTTP的抽象层,就是Socket在Http之上,Socket也就是发动机.实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又 ...

  7. redis基本类型以及优点特性

    1.什么是redis? redis是一个基于内存的高性能key-value数据库 2.redis基本数据类型及应用场景  支持多种数据类型: string(字符串)   String数据结构是简单的k ...

  8. 邀您参加 | BigData & Alluxio 交流会-成都站

    4月27日,在天府之国,与你共享大数据与Alluxio的技术魅力. 活动介绍 本期技术沙龙将会聚焦在大数据.存储.数据库以及Alluxio应用实践等领域,邀请腾讯技术专家和业界技术专家现场分享关于Al ...

  9. 如何手写JDK锁

    手写JDK锁 需要三个步骤: 手写一个类MyLock,实现Lock接口 重写lock()方法 重写unlock()方法 代码: public class MyLock implements Lock ...

  10. Spring Boot入门(六):使用MyBatis访问MySql数据库(注解方式)

    本系列博客记录自己学习Spring Boot的历程,如帮助到你,不胜荣幸,如有错误,欢迎指正! 本篇博客我们讲解下在Spring Boot中使用MyBatis访问MySql数据库的简单用法. 1.前期 ...