要了解一個程式的運行,一定要先了解程式的入口以及執行的邏輯與順序,只要抓住了主軸後面若要變化與應用就容易得多了。這篇文章想要簡單描述一下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初始化ConfigurationConfigureHostConfiguration
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系列文章