要了解一個程式的運行,一定要先了解程式的入口以及執行的邏輯與順序,只要抓住了主軸後面若要變化與應用就容易得多了。這篇文章想要簡單描述一下Dontnet Core的啟動流程,可能有些人覺得不重要,但我覺得只有掌握了核心流程才能說真的了解Dotnet Core。
談談啟動順序
相信許多人在寫Dotnet Core的時候,就是把範例中Program.cs複製貼上,或是簡單改改就上路了。並不知道Dotnet Core的啟動順序是如何,網路上敘述這些的文章不多,且有些不太正確或是片面。我想試著用簡單的方式讓大家更了解。 以下是Dotnet Core 3以後官方建議的啟動方法:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
很明顯以上這段程式是利用建構者模式來創建整個應用服務。在Dotnet Core 3以後多加了一個更廣泛的介面IHost,比之前2的版本用IWebHost只能使用http服務更為廣泛。所以整體的思路是先在Program裡面使用一個Host的靜態方法CreateDefaultBuilder去建立IHostBuilder,並設置一些基本的預設值。接者,使用擴充方法ConfigureWebHostDefaults來增強IHostBuilder中的設定,也就是整合之前版本的WebHost,說得更直白點就是使用Kestrel跟其他Http相關配置,並使用委派方法將個人的服務元件依序注入,在此使用Startup類來實現(後續文章會再說明)。當IHostBuilder中該設定的都設定好了,就會去執行Build方法。至此就會產生Host的實例。最後,執行Run方法把Host實例跑起來。
因此,我認為只要了解四個部分,大致上就可以了解啟動順序:
第一部分:IHost Build 做了什麼?
關鍵程式碼如下:
public IHost Build()
{
...
BuildHostConfiguration();
CreateHostingEnvironment();
CreateHostBuilderContext();
BuildAppConfiguration();
CreateServiceProvider();
return _appServices.GetRequiredService<IHost>();
}
這個流程就是整個Donte Core主要啟動流程喔,非常關鍵,最好簡單記憶一下。這整個流程還可以搭配一些委派方法做些客製化的設定。
以下做一張表來敘述各個流程所做的事情以及這些委派方法。
流程 | 說明 | 委派方法名稱 |
---|---|---|
BuildHostConfiguration | 初始化Configuration | ConfigureHostConfiguration |
CreateHostingEnvironment | 初始化HostingEnvironment | 同上 |
CreateHostBuilderContext | 初始化HostBuilderContext(包含HostingEnvironment Configuration) | 無 |
BuildAppConfiguration | 構建應用程式配置 | ConfigureAppConfiguration |
CreateServiceProvider | 建立依賴注入服務提供程式 | ConfigureServices |
其中最關鍵流程就是CreateServiceProvider,因為一些Dotnet Core預設注入的項目就在這裡發生的,例如:IHostingEnvironment、IApplicationLifetime、Options、Logging…。以上程式碼的最後一行,就是去DI容器中把IHost實例拿出來返回。
補充:當然還有幾個客製化方法
UseServiceProviderFactory:這是可以覆寫掉預設的DI容器。
ConfigureContainer:針對DI容器做配置上的修改。
第二部分:IHost Run 做了什麼?
此方法是一個擴充方法,事實上他就是呼叫RunAsync的阻塞方法。
public static void Run(this IHost host)
{
host.RunAsync().GetAwaiter().GetResult();
}
而host.RunAsync實際上就是呼叫StartAsync加上WaitForShutdownAsync。
public static async Task RunAsync(this IHost host, CancellationToken token = default)
{
try
{
await host.StartAsync(token);
await host.WaitForShutdownAsync(token);
}
finally
{
...
}
}
而真正的StartAsync具體實現是在Host:
public async Task StartAsync(CancellationToken cancellationToken = default)
{
...
_hostedServices = Services.GetService<IEnumerable<IHostedService>>();
foreach (var hostedService in _hostedServices)
{
await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
}
...
}
這邊只提我認為最重要的部分,就是它去容器中拿IHostedService的實例,並依序執行StartAsync方法。所以一個Dotnet Core是可以包含多個Host服務的。
第三部分:CreateDefaultBuilder 做了什麼?
接下來兩個步驟就是Builder設定的部分,首先先看CreateDefaultBuilder。以下直接上部分重要源碼,並加上註解解釋。
public static IHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new HostBuilder();
// 設定專案路徑
builder.UseContentRoot(Directory.GetCurrentDirectory());
// 設定環境變量與輸入參數
builder.ConfigureHostConfiguration(config =>
{
...
});
// 設定appsettings.json
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
...
})
// 設定console log
.ConfigureLogging((hostingContext, logging) =>
{
...
})
// 設定DI容器參數(預設開發模式要開啟scope檢查)
.UseDefaultServiceProvider((context, options) =>
{
...
});
return builder;
}
第四部分:ConfigureWebHostDefaults 做了什麼?
最後介紹最複雜的部分,就是如何把Http服務器注入進去。其中用了許多擴充方法,鏈式調用委派方法,因為程式碼太多且網路上也有探討細部內容的文章,況且也可以上Dotnet Core的Github上直接看原碼。在此,我就整理關鍵的程式碼,並標出是哪個類以方便大家自行探索即可,並以最簡單列點的方式來說明就好。
# GenericHostBuilderExtensions.cs
public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
...
return builder.ConfigureWebHost(webHostBuilder =>
{
WebHost.ConfigureWebDefaults(webHostBuilder);
configure(webHostBuilder);
});
}
# GenericHostWebHostBuilderExtensions.cs
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;
}
# GenericWebHostBuilder.cs 將HostBuilder提升為WebHostBuilder
public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
{
_builder = builder;
...
_builder.ConfigureServices((context, services) =>
{
...
services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();
// IMPORTANT: This needs to run *before* direct calls on the builder (like UseStartup)
_hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);
// Support UseStartup(assemblyName)
if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
{
try
{
var startupType = StartupLoader.FindStartupType(webHostOptions.StartupAssembly, webhostContext.HostingEnvironment.EnvironmentName);
UseStartup(startupType, context, services);
}
catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
{
...
}
}
});
}
# WebHost.cs 對WebHostBuilder做設定
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
...
builder.UseKestrel((builderContext, options) =>
...
.UseIIS()
.UseIISIntegration();
}
- 首先,經過多次擴充方法直到創建GenericWebHostBuilder這個實例。並且可發現裡面使用Startup的邏輯。
- 接者,用委派事件的方式去把GenericWebHostBuilder這個實例當作參數,讓WebHost可以去設定需要注入的相關元件。例如:環境變量,Kestrel,整合IIS等等。
- 接者回到ConfigureWebHost,注入GenericWebHostService,因為GenericWebHostService是IHostedService的實現,所以根據上一部分所說的,可以在最終用Run方法跑起來。
- 最後回到Program.cs裡,執行我們客製化的委派,最上面的範例就是使用UseStartup,根據我們自己服務去設置需要注入的元件與中介軟體。
見樹又見林
本篇文章的宗旨,就是希望大家見樹又見林,但是又不要深入的叢林,以免迷路出不來。小結一下,讀完這篇你應該要對Dotnet Core的啟動,注入服務的流程,以及服務是如何運行起來,有一個基本的概念。這對於往後如何查找問題以及擴展上都相當有幫助。大家加油。
.Net系列文章