原文:Running async tasks on app startup in ASP.NET Core (Part 2) 做者:Andrew Lock 译者:Lamond Luhtml
在个人上一篇博客中,我介绍了如何在ASP.NET Core应用程序启动时运行一些一次性异步任务。本篇博客将继续讨论上一篇的内容,若是你尚未读过,我建议你先读一下前一篇。git
在本篇博客中,我将展现上一篇博文中提出的“在<code>Program.cs</code>中手动运行异步任务”的实现方法。该实现会使用一些简单的接口和类来封装应用程序启动时的运行任务逻辑。我还会展现一个替代方法,这个替代方法是在Kestral服务器启动时,使用<code>IServer</code>接口。github
这里咱们先回顾一下上一遍博客内容,在上一篇中,咱们试图寻找一种方案,容许咱们在ASP.NET Core应用程序启动时执行一些异步任务。这些任务应该是在ASP.NET Core应用程序启动以前执行,可是因为这些任务可能须要读取配置或者使用服务,因此它们只能在ASP.NET Core的依赖注入容器配置完成后执行。数据库迁移,填充缓存均可以这种异步任务的使用场景。web
咱们在一篇文章的末尾提出了一个相对完善的解决方案,这个方案是在<code>Program.cs</code>中“手动”运行任务。运行任务的时机是在<code>IWebHostBuilder.Build()</code>和<code>IWebHost.RunAsync()</code>之间。数据库
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>(); }
这种实现方式是可行的,可是有点乱。这里咱们将许多不该该属于<code>Program.cs</code>职责的代码放在了<code>Program.cs</code>中,让它看起来有点臃肿了,因此这里咱们须要将数据库迁移相关的代码移到另一个类中。c#
这里更麻烦的问题是,咱们必需要手动调用任务。若是你在多个应用程序中使用相同的模式,那么最好能改为自动调用任务。缓存
这里我将使用基于<code>IStartupFilter</code>和<code>IHostService</code>使用的模式。它们容许你在依赖注入容器中注册它们的实现类,并在应用程序启动前获取到这些接口的全部实现类,并依次执行它们。安全
因此,这里首先咱们建立一个简单的接口来启动任务。服务器
public interface IStartupTask { Task ExecuteAsync(CancellationToken cancellationToken = default); }
而且建立一个在依赖注入容器中注册任务的便捷方法。app
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数据库迁移的例子
实现<code>IStartupTask</code>和实现<code>IStartupFilter</code>很是的类似。你能够从依赖注入容器中注入服务。为了使用依赖注入容器中的服务,这里咱们须要手动注入一个<code>IServiceProvider</code>对象,并手动建立一个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(); } } }
如今,咱们能够在<code>ConfigureServices</code>方法中使用依赖注入容器添加启动任务了。
public void ConfigureServices(IServiceCollection services) { services.MyDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration .GetConnectionString("DefaultConnection"))); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddStartupTask<MigrationStartupTask>(); }
最后咱们更新一下<code>Program.cs</code>, 使用<code>RunWithTasksAsync()</code>方法替换<code>Run()</code>方法。
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的特性。从功能上来讲,它与我上一篇博客中的手动代码等同,可是它有一些优势。
对于以上方案,有一个问题须要注意。这里咱们定义的任务会在<code>IConfiguration</code>和依赖注入容器配置完成以后运行,这也就意味着,当任务执行时,全部的<code>IStartupFilter</code>都没有运行,中间件管道也没有配置。
就我我的而言,我不认为这是一个问题,由于我暂时想不出任何可能。到目前为止,我所编写的任务都不依赖于<code>IStartupFilter</code>和中间件管道。但这也并不意味着没有这种可能。
不幸的是,使用当前的WebHost代码并无简单的方法(尽管 在.NET Core 3.0中当ASP.NET Core做为IHostedService运行时,这可能会发生变化)。 问题是应用程序是引导(经过配置中间件管道并运行IStartupFilters)和启动在同一个函数中。 当你在Program.cs中调用<code>WebHost.Run()</code>时,在内部程序会调用<code>WebHost.StartAsync</code>,以下所示,为简洁起见,其中只包含了日志记录和一些其余次要代码:
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); }
这里问题是咱们想要在<code>BuildApplication()</code>和<code>Server.StartAsync</code>之间插入代码,可是如今没有这样作的机制。
我不肯定我所给出的解决方案是否优雅,但它能够工做,并为消费者提供更好的体验,由于他们不须要修改Program.cs
为了实如今<code>BuildApplication()</code>和<code>Server.StartAsync()</code>之间运行异步代码,我能想到的惟一办法是咱们本身的实现一个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); }
<code>TaskExecutingServer</code>在其构造函数中获取了一个<code>IServer</code>实例 - 这是<code>ASP.NET Core</code>注册的原始Kestral服务器。咱们将大部分<code>IServer</code>的接口实现直接委托给Kestrel,咱们只是拦截对<code>StartAsync</code>的调用并首先运行注入的任务。
这个实现最困难部分是使装饰器正常工做。正如我在上一篇文章中所讨论的那样,使用带有默认ASP.NET Core容器的装饰可能会很是棘手。我一般使用Scrutor来建立装饰器,可是若是你不想依赖另外一个库,你老是能够手动进行装饰, 但必定要看看Scrutor是如何作到这一点的!
下面咱们添加一个用于添加<code>IStartupTask</code>的扩展方法, 这个扩展方法作了两件事,一是将<code>IStartupTask</code>注册到依赖注入容器中,二是装饰了以前注册的<code>IServer</code>实例(这里为了简洁,我省略了<code>Decorate</code>方法的实现)。若是它发现<code>IServer</code>已经被装饰,它会跳过第二步,这样你就能够安全的屡次调用<code>AddStartupTask<T></code>方法。
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
这里我只编写了使用后一种<code>IServer</code>实现的库,由于它更容易使用,并且Thomas Levesque已经编写针对第一种方法可用的NuGet包。
在GitHub的实现中,我手动构造了装饰器,以免强制依赖Scrutor。 但最好的方法可能就是将代码复制并粘贴到您本身的项目中。
在这篇博文中,我展现了两种在ASP.NET Core应用程序启动时异步运行任务的方法。 第一种方法须要稍微修改Program.cs,可是“更安全”,由于它不须要修改像IServer这样的内部实现细节。 第二种方法是装饰IServer,提供更好的用户体验,但感受更加笨拙。
原文出处:https://www.cnblogs.com/lwqlun/p/10354149.html