[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接口针对配置系统的设置体现在ConfigureHostConfigurationConfigureAppConfiguration方法上。通过前面的实例演示,我们知道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]: 承载服务启动流程[下篇]