[ASP.NET Core 3框架揭祕]服務承載系統[6]: 承載服務啟動流程[下篇]
實際上HostBuilder物件並沒有在實現的Build方法中呼叫建構函式來建立Host物件,該物件利用作為依賴注入容器的IServiceProvider物件建立的。為了可以採用依賴注入框架來提供構建的Host物件,HostBuilder必須完成前期的服務註冊工作。總地來說,HostBuilder針對Host物件的構建大體可以劃分為如下5個步驟:
- 建立HostBuilderContext上下文:建立針對宿主配置的IConfiguration物件和表示承載環境的IHostEnvironment物件,然後利用二者創建出代表承載上下文的HostBuilderContext物件。
- 建立針對應用的配置:建立針對應用配置的IConfiguration物件,並用它替換HostBuilderContext物件承載的配置。
- 註冊依賴服務:註冊所需的依賴服務,包括應用程式通過呼叫ConfigureServices方法提供的服務註冊和其他一些確保服務承載正常執行的預設服務註冊。
- 建立IServiceProvider:利用註冊的IServiceProviderFactory<TContainerBuilder>工廠(系統預設註冊或者應用程式顯式註冊)創建出用來提供所有依賴服務的IServiceProvider物件。
- 建立Host物件:利用IServiceProvider物件提供作為宿主的Host物件。
步驟一、建立HostBuilderContext
由於很多依賴服務都是針對當前承載上下文進行註冊的,所以Build方法首要的任務就是創建出作為承載上下文的HostBuilderContext物件。一個HostBuilderContext物件由承載針對宿主配置的IConfiguration物件和描述當前承載環境的IHostEnvironment物件組成,但是後者提供的環境名稱、應用名稱和內容檔案根目錄路徑可以通過前者來指定,具體配置項名稱定義在如下這個靜態型別HostDefaults中。
public static class HostDefaults { public static readonly string EnvironmentKey = "environment"; public static readonly string ContentRootKey = "contentRoot"; public static readonly string ApplicationKey = "applicationName"; }
接下來我們通過一個簡單的例項來演示如何利用配置的方式來指定上述三個與承載環境相關的屬性。我們定義瞭如下一個名為FakeHostedService的承載服務,並在建構函式中注入IHostEnvironment物件。在實現的StartAsync方法中,我們將與承載環境相關的環境名稱、應用名稱和內容檔案根目錄路徑輸出到控制檯上。
public class FakeHostedService : IHostedService { private readonly IHostEnvironment _environment; public FakeHostedService(IHostEnvironment environment) => _environment = environment; public Task StartAsync(CancellationToken cancellationToken) { Console.WriteLine("{0,-15}:{1}", nameof(_environment.EnvironmentName), _environment.EnvironmentName); Console.WriteLine("{0,-15}:{1}", nameof(_environment.ApplicationName), _environment.ApplicationName); Console.WriteLine("{0,-15}:{1}", nameof(_environment.ContentRootPath), _environment.ContentRootPath); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
FakeHostedService採用如下的形式承載於當前應用程式中。如下面的程式碼片段所示,在建立作為宿主構建者的HostBuilder之後,我們呼叫了它的ConfigureHostConfiguration方法註冊了基於命令列引數作為配置源,意味著我們可以利用命令列引數的形式來初始化相應的配置。
class Program { static void Main(string[] args) { new HostBuilder() .ConfigureHostConfiguration(builder => builder.AddCommandLine(args)) .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>()) .Build() .Run(); } }
我們採用命令列的方式啟動這個演示程式,並利用傳入的命令列引數指定環境名稱、應用名稱和內容檔案根目錄路徑(確保路徑確實存在)。從如圖10-11所示的輸出結果表明應用程式當前的承載環境確實與基於宿主的配置一致。(S1009)
HostBuilder針對HostBuilderContext物件的建立體現在如下所示的CreateBuilderContext方法中。如下面的程式碼片段所示,該方法建立了一個ConfigurationBuilder物件並呼叫AddInMemoryCollection擴充套件方法註冊了針對記憶體變數的配置源。HostBuilder接下來會將這個ConfigurationBuilder物件作為引數呼叫ConfigureHostConfiguration方法註冊的所有Action<IConfigurationBuilder>委託。這個ConfigurationBuilder物件生成的IConfiguration物件將會作為HostBuilderContext上下文物件的配置。
public class HostBuilder : IHostBuilder { private List<Action<IConfigurationBuilder>> _configureHostConfigActions; public IHost Build() { var buildContext = CreateBuilderContext(); … } private HostBuilderContext CreateBuilderContext() { //Create Configuration var configBuilder = new ConfigurationBuilder().AddInMemoryCollection(); foreach (var buildAction in _configureHostConfigActions) { buildAction(configBuilder); } var hostConfig = configBuilder.Build(); //Create HostingEnvironment var contentRoot = hostConfig[HostDefaults.ContentRootKey]; var contentRootPath = string.IsNullOrEmpty(contentRoot) ? AppContext.BaseDirectory : Path.IsPathRooted(contentRoot) ? contentRoot : Path.Combine(Path.GetFullPath(AppContext.BaseDirectory), contentRoot); var hostingEnvironment = new HostingEnvironment() { ApplicationName = hostConfig[HostDefaults.ApplicationKey], EnvironmentName = hostConfig[HostDefaults.EnvironmentKey] ?? Environments.Production, ContentRootPath = contentRootPath, }; if (string.IsNullOrEmpty(hostingEnvironment.ApplicationName)) { hostingEnvironment.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name; } hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath); //Create HostBuilderContext return new HostBuilderContext(Properties) { HostingEnvironment = hostingEnvironment, Configuration = hostConfig }; } … }
在創建出HostBuilderContext物件的配置之後,HostBuilder會根據配置創建出代表承載環境的HostingEnvironment物件。如果不存在針對應用名稱的配置項,應用名稱將會設定為當前入口程式集的名稱。如果內容檔案根目錄路徑對應的配置項不存在,當前應用的基礎路徑(AppContext.BaseDirectory)將會作為內容檔案根目錄路徑。如果指定的是一個相對路徑,HostBuilder會根據基礎路徑生成一個絕對路徑作為內容檔案根目錄路徑。CreateBuilderContext方法最終會根據建立的這個HostingEnvironment物件和之前建立的IConfiguration創建出代表承載上下文的BuilderContext物件。
步驟二、構建針對應用的配置
到目前為止,作為承載上下文的BuilderContext物件攜帶的是通過呼叫ConfigureHostConfiguration方法初始化的配置,接下來通過呼叫ConfigureAppConfiguration方法初始化的配置將會與之合併,具體的邏輯體現在如下所示的BuildAppConfiguration方法上。
如下面的程式碼片段所示,BuildAppConfigration方法會建立一個ConfigurationBuilder物件,並呼叫其AddConfiguration方法將現有的配置合併進來。於此同時,內容檔案根目錄的路徑將會作為配置檔案所在目錄的基礎路徑。HostBuilder最後會將之前建立的HostBuilderContext 物件和這個ConfigurationBuilder物件作為引數呼叫在ConfigureAppConfiguration方法註冊的每一個Action<HostBuilderContext, IConfigurationBuilder>委託。通過這個ConfigurationBuilder物件建立的IConfiguration物件將會重新賦值給HostBuilderContext物件的Configuration屬性,我們自此就可以從承載上下文中得到完整的配置了。
public class HostBuilder: IHostBuilder { private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions; public IHost Build() { var buildContext = CreateBuilderContext(); buildContext.Configuration = BuildAppConfigration(buildContext); … } private IConfiguration BuildAppConfigration(HostBuilderContext buildContext) { var configBuilder = new ConfigurationBuilder() .SetBasePath(buildContext.HostingEnvironment.ContentRootPath) .AddConfiguration(buildContext.Configuration,true); foreach (var action in _configureAppConfigActions) { action(_hostBuilderContext, configBuilder); } return configBuilder.Build(); } }
步驟三、依賴服務註冊
當作為承載上下文的HostBuilderContext物件創建出來並完成被初始化後,HostBuilder需要完成服務註冊工作,這一實現體現在如下所示的ConfigureAllServices方法中。如下面的程式碼片段所示,ConfigureAllServices方法在將代表承載上下文的HostBuilderContext物件和建立的ServiceCollection物件作為引數呼叫ConfigureServices方法中註冊的每一個Action<HostBuilderContext, IServiceCollection>委託物件之前,它會註冊一些額外的系統服務。ConfigureAllServices方法最終返回包含所有服務註冊的IServiceCollection物件。
public class HostBuilder: IHostBuilder { private List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions; public IHost Build() { var buildContext = CreateBuilderContext(); buildContext.Configuration = BuildAppConfigration(buildContext); var services = ConfigureAllServices (buildContext); … } private IServiceCollection ConfigureAllServices(HostBuilderContext buildContext) { var services = new ServiceCollection(); services.AddSingleton(buildContext); services.AddSingleton(buildContext.HostingEnvironment); services.AddSingleton(_ => buildContext.Configuration); services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>(); services.AddSingleton<IHostLifetime, ConsoleLifetime>(); services.AddSingleton<IHost,Host>(); services.AddOptions(); services.AddLogging(); foreach (var configureServicesAction in _configureServicesActions) { configureServicesAction(_hostBuilderContext, services); } return services; } }
對於ConfigureAllServices方法預設註冊的這些服務,如果我們自定義的承載服務需要使用到它們,可以直接採用構造器注入的方式對它們進行消費。由於其中包含了針對Host的服務註冊,所有由所有服務註冊構建的IServiceProvider物件可以提供最終構建的Host物件。
步驟四、建立IServiceProvider物件
目前我們已經擁有了所有的服務註冊,接下來的任務就是利用它創建出作為依賴注入容器的IServiceProvider物件並利用它提供構建的Host物件。針對IServiceProvider的建立體現在如下所示的CreateServiceProvider方法中。如下面的程式碼片段所示,CreateServiceProvider方法會先得到_serviceProviderFactory欄位表示的IServiceFactoryAdapter物件,該物件是根據UseServiceProviderFactory<TContainerBuilder>方法註冊的IServiceProviderFactory<TContainerBuilder>物件建立的,我們呼叫它的CreateBuilder方法可以得到由註冊的IServiceProviderFactory<TContainerBuilder>物件建立的TContainerBuilder物件。
public class HostBuilder : IHostBuilder { private List<IConfigureContainerAdapter> _configureContainerActions; private IServiceFactoryAdapter _serviceProviderFactory public IHost Build() { var buildContext = CreateBuilderContext(); buildContext.Configuration = BuildAppConfigration(buildContext); var services = ConfigureServices(buildContext); var serviceProvider = CreateServiceProvider(buildContext, services); return serviceProvider.GetRequiredService<IHost>(); } private IServiceProvider CreateServiceProvider(HostBuilderContext builderContext, IServiceCollection services) { var containerBuilder = _serviceProviderFactory.CreateBuilder(services); foreach (var containerAction in _configureContainerActions) { containerAction.ConfigureContainer(builderContext, containerBuilder); } return _serviceProviderFactory.CreateServiceProvider(containerBuilder); } }
接下來我們將這個TContainerBuilder物件作為引數呼叫_configureContainerActions欄位中的每個IConfigureContainerAdapter物件的ConfigureContainer方法,這裡的每個IConfigureContainerAdapter物件都是根據ConfigureContainer<TContainerBuilder>方法提供的Action<HostBuilderContext, TContainerBuilder>物件建立的。在完成了使用者針對TContainerBuilder物件的設定之後,CreateServiceProvider會將該物件會作為引數呼叫 IServiceFactoryAdapter的CreateServiceProvider創建出代表依賴注入容器的IServiceProvider物件,Build方法正是利用它來提供構建的Host物件。
靜態型別Host
當目前為止,我們演示的例項都是直接建立HostBuilder物件來建立作為服務宿主的IHost物件。如果直接利用模板來建立一個ASP.NET Core應用,我們會發現生成的程式會採用如下的服務承載方式。具體來說,用來建立宿主的IHostBuilder物件是間接地呼叫靜態型別Host的CreateDefaultBuilder方法創建出來的,那麼這個方法究竟會提供建立一個IHostBuilder物件呢。
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); }
如下所示的是定義在靜態型別Host中的兩個CreateDefaultBuilder方法過載的定義的,我們會發現它們最終提供的仍舊是一個HostBuilder物件,但是在返回該物件之前,該方法會幫助我們做一些初始化工作。如下面的程式碼片段所示,當CreateDefaultBuilder方法創建出HostBuilder物件之後,它會自動將當前目錄所在的路徑作為內容檔案根目錄的路徑。接下來,該方法還會呼叫HostBuilder物件的ConfigureHostConfiguration方法註冊針對環境變數的配置源,對應環境變數名稱字首被設定為“DOTNET_”。如果提供了代表命令列引數的字串陣列,CreateDefaultBuilder方法還會註冊針對命令列引數的配置源。
public static class Host { public static IHostBuilder CreateDefaultBuilder() => CreateDefaultBuilder(args: null); public static IHostBuilder CreateDefaultBuilder(string[] args) { var builder = new HostBuilder(); builder.UseContentRoot(Directory.GetCurrentDirectory()); builder.ConfigureHostConfiguration(config => { config.AddEnvironmentVariables(prefix: "DOTNET_"); if (args != null) { config.AddCommandLine(args); } }); builder.ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; config .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName)) { var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null) { config.AddUserSecrets(appAssembly, optional: true); } } config.AddEnvironmentVariables(); if (args != null) { config.AddCommandLine(args); } }) .ConfigureLogging((hostingContext, logging) => { logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); logging.AddDebug(); logging.AddEventSourceLogger(); }) .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }); return builder; } }
在設定了針對宿主的配置之後,CreateDefaultBuilder呼叫了HostBuilder的ConfigureAppConfiguration方法設定針對應用的配置,具體的配置源包括針對Json檔案“appsettings.json”和“appsettings.{environment}.json”、環境變數(沒有字首限制)和命令列引數(如果提供了表示命令航引數的字串陣列)。
在完成了針對配置的設定之後,CreateDefaultBuilder方法還會呼叫HostBuilder的ConfigureLogging擴充套件方法作一些與日誌相關的設定,其中包括應用日誌相關的配置(對應配置節名稱為“Logging”)和註冊針對控制檯、偵錯程式和EventSource的日誌輸出渠道。在此之後,它還會呼叫UseDefaultServiceProvider方法讓針對服務範圍的驗證在開發環境下被自動開啟。
服務承載系統[1]: 承載長時間執行的服務[上篇]
服務承載系統[2]: 承載長時間執行的服務[下篇]
服務承載系統[3]: 總體設計[上篇]
服務承載系統[4]: 總體設計[下篇]
服務承載系統[5]: 承載服務啟動流程[上篇]
服務承載系統[6]: 承載服務啟動流程[下篇]