[ASP.NET Core 3框架揭秘]服務承載系統[4]:總體設計[下篇]

在了解了作為服務宿主的IHost介面之後,我們接著來認識一下作為宿主構建者的IHostBuilder介面。如下面的程式碼片段所示,IHostBuilder介面的核心方法Build用來提供由它構建的IHost對象。除此之外,它還具有一個字典類型的只讀屬性Properties,我們可以將它視為一個共享的數據容器。

public interface IHostBuilder  {      IDictionary<object, object> Properties { get; }      IHost Build();      …  }

作為一個典型的設計模式,Builder模式在最終提供給由它構建的對象之前,一般會允許作相應的前期設置,IHostBuilder針對IHost的構建也不例外。IHostBuilder介面提供了一系列的方法,我們可以利用它們為最終構建的IHost對象作相應的設置,具體的設置主要涵蓋兩個方面:針對配置系統的設置和針對依賴注入框架的設置。

一、針對配置系統的設置

IHostBuilder介面針對配置系統的設置體現在ConfigureHostConfiguration和ConfigureAppConfiguration方法上。通過前面的實例演示,我們知道ConfigureHostConfiguration方法涉及的配置主要是在服務承載過程中使用的,是針對服務宿主的配置;ConfigureAppConfiguration方法設置的則是供承載的IHostedService服務使用的,是針對應用的配置。不過前者最終會合併到後者之中,我們最終得到的配置實際上是兩者合併的結果。

public interface IHostBuilder  {      IHostBuilder ConfigureHostConfiguration( Action<IConfigurationBuilder> configureDelegate);      IHostBuilder ConfigureAppConfiguration( Action<HostBuilderContext, IConfigurationBuilder> configureDelegate);      …  }

從上面的程式碼片段可以看出ConfigureHostConfiguration方法提供一個Action<IConfigurationBuilder>類型的委託作為參數,我們可以利用它註冊不同的配置源或者作相應的設置(比如設置配置文件所在目錄的路徑)。另一個方法ConfigureAppConfiguration的參數類型則是Action<HostBuilderContext, IConfigurationBuilder>,作為第一個參數的HostBuilderContext對象攜帶了與服務承載相關的上下文資訊,我們可以利用該上下文對配置系統作針對性設置。

HostBuilderContext攜帶的上下文主要包含兩個部分:其一,通過調用ConfigureHostConfiguration方法設置的針對宿主的配置;其二,當前的承載環境。這兩部分上下文資訊分別對應著如下所示的Configuration和HostingEnvironment屬性。除此之外,HostBuilderContext同樣具有一個作為共享數據字典的Properties屬性。如果針對配置系統的設置與當前承載上下文無關,我們可以調用如下這個同名的擴展方法,該方法提供的參數依舊是一個Action<IConfigurationBuilder>類型的委託。

public class HostBuilderContext  {      public IConfiguration Configuration { get; set; }      public IHostEnvironment HostingEnvironment { get; set; }      public IDictionary<object, object> Properties { get; }      public HostBuilderContext(IDictionary<object, object> properties);  }    public static class HostingHostBuilderExtensions  {      public static IHostBuilder ConfigureAppConfiguration(this IHostBuilder hostBuilder, Action<IConfigurationBuilder> configureDelegate)      => hostBuilder.ConfigureAppConfiguration((context, builder) =>configureDelegate(builder));  }

二、承載環境

任何一個應用總是針對某個具體的環境進行部署的,我們將承載服務的部署環境稱為承載環境。承載環境通過IHostEnvironment介面表示,HostBuilderContext的HostingEnvironment屬性返回的就是一個IHostEnvironment對象。如下面的程式碼片段所示,除了表示環境名稱的EnvironmentName屬性之外,IHostEnvironment介面還定義了一個表示當前應用名稱的ApplicationName屬性。

public interface IHostEnvironment  {      string EnvironmentName { get; set; }      string ApplicationName { get; set; }      string ContentRootPath { get; set; }      IFileProvider ContentRootFileProvider { get; set; }  }

當我們編譯某個.NET Core項目的時候,提供的程式碼文件(.cs)文件會轉換成元數據和IL指令保存到生成的程式集中,其他一些文件還可以作為程式集的內嵌資源。除了這些面向程式集的文件之外,一些文件還會以靜態文件的形式供應用程式使用,比如Web應用三種典型的靜態文件(JavaScript、CSS和圖片),我們將這些靜態文件稱為內容文件「Content File」。IHostEnvironment介面的ContentRootPath表示的就是存放這些內容文件的根目錄所在的路徑,另一個ContentRootFileProvider屬性對應的則是指向該路徑的IFileProvider對象,我們可以利用它獲取目錄的層次結構,也可以直接利用它來讀取文件的內容。

開發、預發和產品是三種最為典型的承載環境,如果採用「Development」、「Staging」和「Production」來對它們進行命名,我們針對這三種承載環境的判斷就可以利用如下三個擴展方法(IsDevelopment、IsStaging和IsProduction)來完成。如果我們需要判斷指定的IHostEnvironment對象是否屬於某個具體的環境,可以直接調用擴展方法IsEnvironment。從給出的程式碼片段我們不難看出針對環境名稱的比較是不區分大小寫的。

public static class HostEnvironmentEnvExtensions  {      public static bool IsDevelopment(this IHostEnvironment hostEnvironment)          => hostEnvironment.IsEnvironment(Environments.Development);      public static bool IsStaging(this IHostEnvironment hostEnvironment)          => hostEnvironment.IsEnvironment(Environments.Staging);      public static bool IsProduction(this IHostEnvironment hostEnvironment)          => hostEnvironment.IsEnvironment(Environments.Production);        public static bool IsEnvironment(this IHostEnvironment hostEnvironment, string environmentName)          => string.Equals(hostEnvironment.EnvironmentName, environmentName, StringComparison.OrdinalIgnoreCase);  }    public static class Environments  {      public static readonly string Development = "Development";      public static readonly string Production = "Production";      public static readonly string Staging = "Staging";  }

IHostEnvironment對象承載的3個屬性都是通過配置的形式提供的,對應的配置項名稱為「environment」和「contentRoot」和「applicationName」,它們對應著HostDefaults類型中三個靜態只讀欄位。我們可以調用如下這兩個針對IHostBuilder介面的UseEnvironment和UseContentRoot擴展方法來設置環境名稱和內容文件根目錄路徑。從給出的程式碼片段可以看出,該方法依舊是調用的ConfigureHostConfiguration方法。如果沒有對應用名稱做顯示設置,入口程式集名稱會作為當前應用名稱。

public static class HostDefaults  {      public static readonly string EnvironmentKey = "environment";      public static readonly string ContentRootKey = "contentRoot";      public static readonly string ApplicationKey = "applicationName";  }    public static class HostingHostBuilderExtensions  {      public static IHostBuilder UseEnvironment(this IHostBuilder hostBuilder, string environment)      {          return hostBuilder.ConfigureHostConfiguration(configBuilder =>          {              configBuilder.AddInMemoryCollection(new[]              {                  new KeyValuePair<string, string>(HostDefaults.EnvironmentKey,environment)              });          });            public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder, string contentRoot)          {              return hostBuilder.ConfigureHostConfiguration(configBuilder =>              {                  configBuilder.AddInMemoryCollection(new[]                  {                  new KeyValuePair<string, string>(HostDefaults.ContentRootKey,                      contentRoot))                  });          });      }  }

三、針對依賴注入框架的設置

由於包括承載服務在內的所有依賴服務都是由依賴注入框架提供的,所以IHostBuilder介面提供了更多的方法來對完成服務註冊。絕大部分用來註冊服務的方法最終都調用了如下所示的ConfigureServices方法,由於該方法提供的參數是一個Action<HostBuilderContext, IServiceCollection>類型的委託,意味服務可以針對當前的承載上下文進行針對性註冊。如果註冊的服務與當前承載上下文無關,我們可以調用如下所示的這個同名的擴展方法,該方法提供的參數是一個類型為 Action<IServiceCollection>的委託對象。

public interface IHostBuilder  {      IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate);      …  }    public static class HostingHostBuilderExtensions  {      public static IHostBuilder ConfigureServices(this IHostBuilder hostBuilder, Action<IServiceCollection> configureDelegate)          => hostBuilder.ConfigureServices((context, collection) => configureDelegate(collection));  }

在《承載長時間運行的服務[下篇]》針對日誌的演示中,我們調用了IHostBuilder介面的擴展方法ConfigureLogging註冊了針對日誌框架的核心服務,如下的程式碼片段展示了這兩個擴展方法重載的定義。可以看出這兩個方法的背後依舊是調用上面這個ConfigureServices方法,具體的服務是通過調用IServiceCollection介面的AddLogging擴展方法註冊的。

public static class HostingHostBuilderExtensions  {      public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action<HostBuilderContext, ILoggingBuilder> configureLogging)          => hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder)));        public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action<ILoggingBuilder> configureLogging)              => hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(builder)));  }

IHostBuilder介面提供了如下兩個UseServiceProviderFactory<TContainerBuilder>方法重載,我們可以利用它註冊的IServiceProviderFactory<TContainerBuilder>對象實現對第三方依賴注入框架的整合。除此之外,該介面還提供了另一個ConfigureContainer<TContainerBuilder>為註冊IServiceProviderFactory<TContainerBuilder>對象創建的容器作進一步設置。

public interface IHostBuilder  {      IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory);      IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory);      IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate);  }

我個人覺得.NET Core依賴注入框架已經能夠滿足絕大部分應用開發的需求了,所以真正與第三方依賴注入框架的整合其實並沒有太多的必要。我們知道原生的依賴注入框架使用DefaultServiceProviderFactory來提供作為依賴注入容器的IServiceProvider,針對它的註冊由如下這兩個UseDefaultServiceProvider擴展方法來完成。

public static class HostingHostBuilderExtensions  {      public static IHostBuilder UseDefaultServiceProvider(this IHostBuilder hostBuilder, Action<ServiceProviderOptions> configure)      => hostBuilder.UseDefaultServiceProvider((context, options) => configure(options));        public static IHostBuilder UseDefaultServiceProvider(this IHostBuilder hostBuilder, Action<HostBuilderContext, ServiceProviderOptions> configure)      {          return hostBuilder.UseServiceProviderFactory(context =>          {              var options = new ServiceProviderOptions();              configure(context, options);              return new DefaultServiceProviderFactory(options);          });      }  }

定義在IHostBuilder介面的ConfigureContainer<TContainerBuilder>方法提供的參數是一個類型為Action<HostBuilderContext, TContainerBuilder>的委託對象,如果我們針對TContainerBuilder的設置與當前承載上下文無關,我們也可以調用如下的這個簡化的ConfigureContainer<TContainerBuilder>擴展方法,它只需要提供一個Action<TContainerBuilder>對象作為參數就可以了。

public static class HostingHostBuilderExtensions  {      public static IHostBuilder ConfigureContainer<TContainerBuilder>(this IHostBuilder hostBuilder, Action<TContainerBuilder> configureDelegate)      {          return hostBuilder.ConfigureContainer<TContainerBuilder>((context, builder) => configureDelegate(builder));      }  }

四、創建並啟動宿主

IHostBuilder介面還具有如下這個StartAsync擴展方法,它同時完成了針對IHost對象的創建和啟動工作,它的另一個Start方法是StartAsync方法的同步版本。

public static class HostingAbstractionsHostBuilderExtensions  {      public static async Task<IHost> StartAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)      {          var host = hostBuilder.Build();          await host.StartAsync(cancellationToken);          return host;      }        public static IHost Start(this IHostBuilder hostBuilder) => hostBuilder.StartAsync().GetAwaiter().GetResult();  }

服務承載系統[1]: 承載長時間運行的服務[上篇]
服務承載系統[2]: 承載長時間運行的服務[下篇]
服務承載系統[3]: 總體設計[上篇]
服務承載系統[4]: 總體設計[下篇]
服務承載系統[5]: 承載服務啟動流程[上篇]
服務承載系統[6]: 承載服務啟動流程[下篇]

Tags: