想寫好中間件,這是基礎。
一、前言
今天這個內容,基於於ASP.NET Core 3.x。
從3.x開始,ASP.NET Core使用了通用主機模式。它將WebHostBuilder放到了通用的IHost之上,這樣可以確保Kestrel可以運行在IHostedService中。
我們今天就來研究一下這個啟動方式和啟動順序。
為了防止不提供原網址的轉載,特在這里加上原文鏈接:https://www.cnblogs.com/tiger-wang/p/13636641.html
二、通常的啟動次序
通常情況下,IHostedService的任何實現在添加到Startup.ConfigureServices()后,都會在GenericWebHostService之前啟動。

這是微軟官方給出的圖。
這個圖展示了在IHost上調用RunAsync()時的啟動順序(后者又調用StartAsync())。對我們來說,最重要的部分是啟動的IHostedServices。從圖上也可以看到,自定義IHostedServices先於GenericWebHostSevice啟動。
我們來看一個簡單的例子:
public class StartupHostedService : IHostedService
{
private readonly ILogger _logger;
public StartupHostedService(ILogger<StartupHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting IHostedService registered in Startup");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping IHostedService registered in Startup");
return Task.CompletedTask;
}
}
我們做一個簡單的IHostedService。希望加到Startup.cs中:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<StartupHostedService>();
}
}
運行代碼:
info: demo.StartupHostedService[0] # 這是上邊的StartupHostedService
Starting IHostedService registered in Startup
info: Microsoft.Hosting.Lifetime[0] # 這是GenericWebHostSevice
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
正如預期的那樣,IHostedService首先執行,然后是GenericWebHostSevice。ApplicationLifetime事件在所有IHostedServices執行之后觸發。無論在什么地方注冊了Startup.ConfigureServices()中的IHostedService, GenericWebHostSevice都在最后啟動。
那么問題來了,為什么GenericWebHostSevice在最后啟動?
三、為什么`GenericWebHostSevice`在最后啟動?
先看看多個IHostedService的情況。
當有多個IHostedService的實現加入到Startup.ConfigureServices()時,運行次序取決於它被加入的次序。
看例子:
public class Service1 : IHostedService
{
private readonly ILogger _logger;
public Service1(ILogger<Service1> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting Service1");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stoping Service1");
return Task.CompletedTask;
}
}
public class Service2 : IHostedService
{
private readonly ILogger _logger;
public Service2(ILogger<Service2> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting Service2");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stoping Service2");
return Task.CompletedTask;
}
}
Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<Service1>();
services.AddHostedService<Service2>();
}
}
運行:
info: demo.Service1[0] # 這是Service1
Starting Service1
info: demo.Service2[0] # 這是Service2
Starting Service2
info: Microsoft.Hosting.Lifetime[0] # 這是GenericWebHostSevice
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
那么,GenericWebHostSevice是什么時候注冊的?
我們看看另一個文件Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => # 這是GenericWebHostSevice注冊的位置
{
webBuilder.UseStartup<Startup>();
});
}
ConfigureWebHostDefaults擴展方法調用ConfigureWebHost方法,該方法執行Startup.ConfigureServices(),然后注冊GenericWebHostService。整理一下代碼,就是下面這個樣子:
public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
var webhostBuilder = new GenericWebHostBuilder(builder);
configure(webhostBuilder);
builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
return builder;
}
這樣可以確保GenericWebHostService總是最后運行,以保持通用主機實現和WebHost(已棄用)實現之間的行為一致。
因此,可以采用同樣的方式,讓IHostedService在GenericWebHostService后面啟動。
四、讓`IHostedService`在`GenericWebHostService`后面啟動
在大多數情況下,在GenericWebHostService之前啟動IHostedServices就可以滿足常規的應用。但是,GenericWebHostService還負責構建應用程序的中間件管道。如果IHostedService依賴於中間件管道或路由,那么就需要將它的啟動延遲到GenericWebHostService完成之后。
根據上面的說明,在GenericWebHostService之后執行IHostedService的唯一方法是將它添加到GenericWebHostService之后的DI容器中。這意味着你必須跳出Startup.ConfigureServices(),在調用ConfigureWebHostDefaults之后,直接在IHostBuilder上調用ConfigureServices():
public class ProgramHostedService : IHostedService
{
private readonly ILogger _logger;
public ProgramHostedService(ILogger<ProgramHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting ProgramHostedService registered in Program");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping ProgramHostedService registered in Program");
return Task.CompletedTask;
}
}
加到Program.cs中:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => # 這是GenericWebHostSevice注冊的位置
{
webBuilder.UseStartup<Startup>();
})
.ConfigureServices(services =>
services.AddHostedService<ProgramHostedService>()); # 這是ProgramHostedService注冊的位置
}
看輸出:
info: demo.StartupHostedService[0] # 這是StartupHostedService
Starting IHostedService registered in Startup
info: Microsoft.Hosting.Lifetime[0] # 這是GenericWebHostSevice
Now listening on: https://localhost:5001
info: demo.ProgramHostedService[0] # 這是ProgramHostedService
Starting ProgramHostedService registered in Program
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
同樣,在關閉應用時,IHostedServices被反向停止,所以ProgramHostedService首先停止,接着是GenericWebHostSevice,最后是StartupHostedService:
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
info: demo.ProgramHostedService[0]
Stopping ProgramHostedService registered in Program
info: demo.StartupHostedService[0]
Stopping IHostedService registered in Startup
五、總結
最后總結一下:
IHostedServices的執行順序與它們在Startup.configureservices()中添加到DI容器中的順序相同。運行偵聽HTTP請求的Kestrel服務器的GenericWebHostSevice總是注冊的IHostedServices之后運行。
要在GenericWebHostSevice之后啟動IHostedService,需要在Program.cs中的IHostBuilder上的ConfigureServices()擴展方法中進行注冊。
(全文完)
本文的代碼在:https://github.com/humornif/Demo-Code/tree/master/0024/demo
![]() |
微信公眾號:老王Plus 掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送 本文版權歸作者所有,轉載請保留此聲明和原文鏈接 |

