[ASP.NET Core 3框架揭秘]服務承載系統[6]: 承載服務啟動流程[下篇]

  • 2020 年 3 月 12 日
  • 筆記

實際上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)

10-11

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]: 承載服務啟動流程[下篇]