[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]: 承载服务启动流程[下篇]