這一篇是接著前一篇在寫的,如果沒有看過前一篇文章,建議先去看一下前一篇,這兒是傳送門
?
一、前言
前一篇文章,我們從應用啟動時異步運行任務開始,說到了必要性,也說到了幾種解決方法,及各自的優缺點,最后,還提出了一個比較合理的解決方法:通過在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();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] 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容器注冊啟動任務:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
where T : class, IStartupTask
=> services.AddTransient<IStartupTask, T>();
}
最后,再建一個擴展方法,在應用啟動時,查找所有已注冊的IStartupTask,按順序執行他們,然后啟動IWebHost:
public static class StartupTaskWebHostExtensions
{
public static async Task 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,并手動創建作用域,
?
例子中,資料遷移類可以寫成這樣:
public class MigratorStartupFilter: IStartupTask
{
private readonly IServiceProvider _serviceProvider;
public MigratorStartupFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async 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.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddStartupTask<MigrationStartupTask>();
}
最后,用上一節中的擴展方法RunWithTasksAsync()來替代Program.cs中的Run():
public class Program
{
public static async Task Main(string[] args)
{
// await CreateWebHostBuilder(args).Build().RunAsync();
await CreateWebHostBuilder(args).Build().RunWithTasksAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
?
從功能上來說,跟上一篇的代碼區別不大,但這樣的寫法,又多了一些優點:
- 任務代碼放到了
Program.cs之外,這符合微軟的建議,也更容易理解; - 任務放到了DI容器中,這樣更容易添加額外的任務;
- 如果沒有額外任務,這個代碼和標準的
Run()一樣,所以這個代碼可以獨立成一個模板,
簡單來說,使用RunWithTasksAsync()后,可以輕松地向DI容器添加額外的任務,而不需要任何其它的更改,
?
滿意了嗎?好像感徑訓差一點點…
四、不夠完美的地方
如果要照著完美去做,好像還差一點點,
這個一點點是在于:任務現在運行在IConfiguration和DI容器配置完成后,IStartupFilters運行和中間件管道配置完成之前,換句話說,如果任務需要依賴于IStartupFilters,那這個方案行不通,
在大多數情況下,這沒什么問題,以我自己的經驗來看,好像沒有什么功能需要依賴于IStartupFilters,但作為一個框架類的代碼,需要考慮這種情況發生的可能性,
以目前的方案來說,好像還沒辦法解決,
應用啟動時,當呼叫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()的呼叫之間,該怎么辦?
這段代碼能給出答案:我們需要裝飾IServer, ¨K16K 首先,我們替換IServer的實作: ¨G8G 在這段代碼中,我們攔截StartAsync()呼叫并注入任務,然后回到內置處理, 下面是對應的擴展代碼: ¨G9G 這個擴展代碼做了兩件事:在DI容器中注冊了IStartupTask,并裝飾了之前注冊的IServer實體,裝飾方法Decorate()我略過了,有興趣的可以去了解一下 - 裝飾模式, Program.cs的代碼和第三節的代碼相同,略過,   我們終于做到了在應用程式完全構建完成后去執行我們的任務,包括IStartupFilters`和中間件管道,
現在的流程,類似于下面這個微軟官方的圖:

(全文完)
![]() |
微信公眾號:老王Plus 掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送 本文著作權歸作者所有,轉載請保留此宣告和原文鏈接 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/111706.html
標籤:.NET Core

