1. 程式人生 > >[ASP.NET Core 3框架揭祕]服務承載系統[4]:總體設計[下篇]

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