这一篇是接着前一篇在写的。如果没有看过前一篇文章,建议先去看一下前一篇,这儿是传送门
?
一、前言
前一篇文章,我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点。最后,还提出了一个比较合理的解决方法:通过在Program.cs 里加入代码,来实现IWebHost 启动前运行异步任务。
实现的代码再贴一下:
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(); ????}
????static?IWebHostBuilder?CreateWebHostBuilderstring[]?args)?=> ????????WebHost.CreateDefaultBuilder(args) ????????????.UseStartup<Startup>(); }
这个方法是有效的。但是,也会有一点不足。
从.Net Core的最简规则来说,我们不应该在Program.cs 中加入其它代码。当然,我们可以把这部分代码转到一个外部类中,但最后也必须手动加入到Program.cs 中。尤其是在多个应用中,使用相同的模式时,这种方式会很麻烦。
????为防止非授权转发,这儿给出本文的原文链接:https://www.cnblogs.com/tiger-wang/p/13714679.html
也许,我们可以采用向DI容器中注入启动任务?
二、向DI容器中注入启动任务
这种方式,是基于IStartupFilter 和IHostedService 两个接口,通过这两个接口可以向依赖注入容器中注册类。
?
首先,我们为启动任务创建一个简单接口:
public?interface?IStartupTask { ????Task?ExecuteAsync(CancellationToken?cancellationToken?=?default); }
再建一个扩展方法,用来向DI容器注册启动任务:
static?ServiceCollectionExtensions { ????static?IServiceCollection?AddStartupTask<T>(this?IServiceCollection?services) ????????where?T?:?class,?IStartupTask ????????=>?services.AddTransient<IStartupTask,?T>(); }
最后,再建一个扩展方法,在应用启动时,查找所有已注册的IStartupTask ,按顺序执行他们,然后启动IWebHost :
StartupTaskWebHostExtensions { ????RunWithTasksAsync(this?IHost?webHost,?CancellationToken?cancellationToken?=?default) ????{ ????????var?startupTasks?=?webHost.Services.GetServices<IStartupTask>();
????????foreach?(var?startupTask?in?startupTasks) ????????{ ????????????await?startupTask.ExecuteAsync(cancellationToken); ????????}
????????await?webHost.RunAsync(cancellationToken); ????} }
这样就齐活了。
?
还是用一个例子来看看这个方式的具体应用。
三、示例 - 数据迁移
实现IStartupTask 其实和实现IStartupFilter 很相似,可以从DI容器中注入。如果需要考虑作用域,还可以注入IServiceProvider ,并手动创建作用域。
?
例子中,数据迁移类可以写成这样:
MigratorStartupFilter:?IStartupTask { ????private?readonly?IServiceProvider?_serviceProvider; ????public?MigratorStartupFilter(IServiceProvider?serviceProvider) ????{ ????????_serviceProvider?=?serviceProvider; ????}
????public?async?Task?default) ????{ ????????using(var?scope?=?_seviceProvider.CreateScope()) ????????{ ????????????var?myDbContext?=?scope.ServiceProvider.GetrequiredService<MyDbContext>(); ????????????await?myDbContext.Database.MigrateAsync(); ????????} ????} }
下面,把任务注入到ConfigureServices() 中:
void?ConfigureServices(IServiceCollection?services) { ????services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
????services.AddStartupTask<MigrationStartupTask>(); }
最后,用上一节中的扩展方法RunWithTasksAsync() 来替代Program.cs 中的Run() :
string[]?args) ????{ ???????? ????????await?(args).Build().(); ????}
????string[]?args)?=> ????????WebHost.CreateDefaultBuilder(args) ????????????.UseStartup<Startup>(); }
?
从功能上来说,跟上一篇的代码区别不大,但这样的写法,又多了一些优点:
- 任务代码放到了
Program.cs 之外。这符合微软的建议,也更容易理解;
- 任务放到了DI容器中,这样更容易添加额外的任务;
- 如果没有额外任务,这个代码和标准的
Run() 一样,所以这个代码可以独立成一个模板。
简单来说,使用RunWithTasksAsync() 后,可以轻松地向DI容器添加额外的任务,而不需要任何其它的更改。
?
满意了吗?好像感觉还差一点点…
四、不够完美的地方
如果要照着完美去做,好像还差一点点。
这个一点点是在于:任务现在运行在IConfiguration 和DI容器配置完成后,IStartupFilters 运行和中间件管道配置完成之前。换句话说,如果任务需要依赖于IStartupFilters ,那这个方案行不通。
在大多数情况下,这没什么问题。以我自己的经验来看,好像没有什么功能需要依赖于IStartupFilters 。但作为一个框架类的代码,需要考虑这种情况发生的可能性。
以目前的方案来说,好像还没办法解决。
应用启动时,当调用WebHost.Run() 时,是内部调用WebHost 。看一下StartAsync() 的简化代码:
virtual?async?Task?StartAsyncdefault) { ????_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() 的调用之间,该怎么办?
这段代码能给出答案:我们需要装饰IServer。 ¨K16K 首先,我们替换 IServer的实现: ¨G8G 在这段代码中,我们拦截 StartAsync()调用并注入任务,然后回到内置处理。 下面是对应的扩展代码: ¨G9G 这个扩展代码做了两件事:在DI容器中注册了 IStartupTask,并装饰了之前注册的 IServer实例。装饰方法 Decorate()我略过了,有兴趣的可以去了解一下 - 装饰模式。 Program.cs的代码和第三节的代码相同,略过。   我们终于做到了在应用程序完全构建完成后去执行我们的任务,包括 IStartupFilters`和中间件管道。
现在的流程,类似于下面这个微软官方的图:

(全文完)
?
?
?
|