ASP.NET Core管道详解[6]: ASP.NET Core应用是如何启动的?[下篇]

要承载一个ASP.NET Core应用,只需要将GenericWebHostService服务注册到承载系统中即可。但GenericWebHostService服务具有针对其他一系列服务的依赖,所以在注册该承载服务之前需要先完成对这些依赖服务的注册。针对GenericWebHostService及其依赖服务的注册是借助GenericWebHostBuilder对象来完成的。

在传统的基于IWebHost/IWebHostBuilder的承载系统中,IWebHost对象表示承载Web应用的宿主,它由对应的IWebHostBuilder对象构建而成,IWebHostBuilder针对IWebHost对象的构建体现在它的Build方法上。由于通过该方法构建IWebHost对象是利用依赖注入框架提供的,所以IWebHostBuilder接口定义了两个ConfigureServices方法重载来注册这些依赖服务。如果注册的服务与通过WebHostBuilderContext对象表示的承载上下文(承载环境和配置)无关,我们一般调用第一个ConfigureServices方法重载,第二个方法可以帮助我们完成基于承载上下文的服务注册,我们也可以根据当前承载环境和提供的配置来动态地注册所需的服务。

public interface IWebHostBuilder
{
    IWebHost Build();

    string GetSetting(string key);
    IWebHostBuilder UseSetting(string key, string value);
    IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate);

    IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices);
    IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices);
}

目录

一、ISupportsStartup & ISupportsUseDefaultServiceProvider

二、服务注册

三、配置的读写

四、默认依赖注入框架配置

五、Startup

六、Hosting Startup

七、ConfigureWebHostDefaults

一、ISupportsStartup & ISupportsUseDefaultServiceProvider

在基于IWebHost/IWebHostBuilder的承载系统中,WebHostBuilder是对IWebHostBuilder接口的默认实现。如果采用基于IHost/IHostBuilder的承载系统,默认实现的IWebHostBuilder类型为GenericWebHostBuilder。这个内部类型除了实现IWebHostBuilder接口,还实现了如下所示的两个内部接口(ISupportsStartup和ISupportsUseDefaultServiceProvider)。前面介绍Startup时提到过ISupportsStartup接口,它定义了一个用于注册中间件的Configure方法和一个用来注册Startup类型的UseStartup方法。ISupportsUseDefaultServiceProvider接口则定义了唯一的UseDefaultServiceProvider方法,该方法用来对默认使用的依赖注入容器进行设置。

internal interface ISupportsStartup
{
    IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuilder> configure);
    IWebHostBuilder UseStartup(Type startupType);
}

internal interface ISupportsUseDefaultServiceProvider
{
    IWebHostBuilder UseDefaultServiceProvider(Action<WebHostBuilderContext, ServiceProviderOptions> configure);
}

二、服务注册

下面通过简单的代码来模拟GenericWebHostBuilder针对IWebHostBuilder接口的实现。首先介绍用来注册依赖服务的ConfigureServices方法的实现。如下面的代码片段所示,GenericWebHostBuilder实际上是对一个IHostBuilder对象的封装,针对依赖服务的注册是通过调用IHostBuilder接口的ConfigureServices方法实现的。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    private readonly IHostBuilder _builder;

    public GenericWebHostBuilder(IHostBuilder builder)
    {
        _builder = builder;
        ...
    }

    public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices) => ConfigureServices((_, services) => configureServices(services));

    public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices)
    {
        _builder.ConfigureServices((context, services) => configureServices(GetWebHostBuilderContext(context), services));
        return this;
    }

    private WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context)
    {
        if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var value))
        {
            var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name);
            var webHostBuilderContext = new WebHostBuilderContext
            {
                Configuration = context.Configuration,
                HostingEnvironment  = new HostingEnvironment(),
            };
            webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options);
            context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext;
            context.Properties[typeof(WebHostOptions)] = options;
            return webHostBuilderContext;
        }

        var webHostContext = (WebHostBuilderContext)value;
        webHostContext.Configuration = context.Configuration;
        return webHostContext;
    }
}

IHostBuilder接口的ConfigureServices方法提供针对当前承载上下文的服务注册,通过HostBuilderContext对象表示的承载上下文包含两个元素,分别是表示配置的IConfiguration对象和表示承载环境的IHostEnvironment对象。而ASP.NET Core应用下的承载上下文是通过WebHostBuilderContext对象表示的,两个上下文之间的不同之处体现在针对承载环境的描述上,WebHostBuilderContext上下文中的承载环境是通过IWebHostEnvironment对象表示的。GenericWebHostBuilder在调用IHostBuilder对象的ConfigureServices方法注册依赖服务时,需要调用GetWebHostBuilderContext方法将提供的WebHostBuilderContext上下文转换成HostBuilderContext类型。

GenericWebHostBuilder对象在构建时会以如下方式调用ConfigureServices方法注册一系列默认的依赖服务,其中包括表示承载环境的IWebHostEnvironment服务、用来发送诊断日志事件的DiagnosticSource服务和DiagnosticListener服务(它们返回同一个服务实例)、用来创建HttpContext上下文的IHttpContextFactory工厂、用来创建中间件的IMiddlewareFactory工厂、用来创建IApplicationBuilder对象的IApplicationBuilderFactory工厂等。除此之外,GenericWeb
HostBuilder构造函数中还完成了针对GenericWebHostServiceOptions配置选项的设置,承载ASP.NET Core应用的GenericWebHostService服务也是在这里注册的。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    private readonly IHostBuilder  _builder;
    private AggregateException  _hostingStartupErrors;

    public GenericWebHostBuilder(IHostBuilder builder)
    {
        _builder = builder;
        _builder.ConfigureServices((context,  services)=>
        {
            var webHostBuilderContext = GetWebHostBuilderContext(context);
            services.AddSingleton(webHostBuilderContext.HostingEnvironment);
            services.AddHostedService<GenericWebHostService>();
            DiagnosticListener instance = new DiagnosticListener("Microsoft.AspNetCore");
            services.TryAddSingleton(instance);
            services.TryAddSingleton<DiagnosticSource>(instance);
            services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
            services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
            services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();

            var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];
            services.Configure<GenericWebHostServiceOptions>(options=>
            {
                options.WebHostOptions = webHostOptions;
                options.HostingStartupExceptions = _hostingStartupErrors;
            });
        });
        ...
    }
}

三、配置的读写

除了两个ConfigureServices方法重载,IWebHostBuilder接口的其他方法均与配置有关。基于IHost/IHostBuilder的承载系统涉及两种类型的配置:一种是在服务承载过程中供作为宿主的IHost对象使用的配置,另一种是供承载的服务或者应用消费的配置,前者是后者的子集。这两种类型的配置分别由IHostBuilder接口的ConfigureHostConfiguration方法和ConfigureAppConfiguration方法进行设置,GenericWebHostBuilder针对配置的设置最终会利用这两个方法来完成。

GenericWebHostBuilder提供的配置体现在它的字段_config上,以键值对形式设置和读取配置的UseSetting方法与GetSetting方法操作的是_config字段表示的IConfiguration对象。静态Host类型的CreateDefaultBuilder方法创建的HostBuilder对象会默认将前缀为“DOTNET_”的环境变量作为配置源,ASP.NET Core应用则选择将前缀为“ASPNETCORE_”的环境变量作为配置源,这一点体现在如下所示的代码片段中。

internal class GenericWebHostBuilder :
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    private readonly IHostBuilder _builder;
    private readonly IConfiguration _config;
    
    public GenericWebHostBuilder(IHostBuilder builder)
    {
        _builder = builder;
        _config = new ConfigurationBuilder()
            .AddEnvironmentVariables(prefix: "ASPNETCORE_")
            .Build();
        _builder.ConfigureHostConfiguration(config => config.AddConfiguration(_config));
        ...
    }
    public string GetSetting(string key) => _config[key];

    public IWebHostBuilder UseSetting(string key, string value)
    {
        _config[key] = value;
        return this;
    }
}

如上面的代码片段所示,GenericWebHostBuilder对象在构造过程中会创建一个ConfigurationBuilder对象,并将前缀为“ASPNETCORE_”的环境变量作为配置源。在利用ConfigurationBuilder对象创建IConfiguration对象之后,该对象体现的配置通过调用IHostBuilder对象的ConfigureHostConfiguration方法被合并到承载系统的配置中。由于IHostBuilder接口和IWebHostBuilder接口的ConfigureAppConfiguration方法具有相同的目的,所以GenericWebHostBuilder类型的ConfigureAppConfiguration方法直接调用IHostBuilder的同名方法。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    private readonly IHostBuilder _builder;

    public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate)
    {
        _builder.ConfigureAppConfiguration((context, builder) => configureDelegate(GetWebHostBuilderContext(context), builder));
        return this;
    }
}

四、默认依赖注入框架配置

原生的依赖注入框架被直接整合到ASP.NET Core应用中,源于GenericWebHostBuilder类型ISupportsUseDefaultServiceProvider接口的实现。如下面的代码片段所示,在实现的UseDefaultServiceProvider方法中,GenericWebHostBuilder会根据ServiceProviderOptions对象承载的配置选项完成对DefaultServiceProviderFactory工厂的注册。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    public IWebHostBuilder UseDefaultServiceProvider(Action<WebHostBuilderContext, ServiceProviderOptions> configure)
    {
        _builder.UseServiceProviderFactory(context =>
        {
            var webHostBuilderContext = GetWebHostBuilderContext(context);
            var options = new ServiceProviderOptions();
            configure(webHostBuilderContext, options);
            return new DefaultServiceProviderFactory(options);
        });

        return this;
    }
}

五、Startup

在大部分应用开发场景下,通常将应用启动时需要完成的初始化操作定义在注册的Startup中,按照约定定义的Startup类型旨在完成如下3个任务。

  • 利用Configure方法或者Configure{EnvironmentName}方法注册中间件。
  • 利用ConfigureServices方法或者Configure{EnvironmentName}Services方法注册依赖服务。
  • 利用ConfigureContainer方法或者Configure{EnvironmentName}Container方法对第三方依赖注入容器做相关设置。

上述3个针对Startup的设置最终都需要应用到基于IHost/IHostBuilder的承载系统上。由于Startup类型是注册到GenericWebHostBuilder对象上的,而GenericWebHostBuilder对象本质上是对IHostBuilder对象的封装,这些设置可以借助这个被封装的IHostBuilder对象被应用到承载系统上,具体的实现体现在如下几点。

  • 将针对中间件的注册转移到GenericWebHostServiceOptions这个配置选项的Configure
    Application属性上。
  • 调用IHostBuilder对象的ConfigureServices方法来完成真正的服务注册。
  • 调用IHostBuilder对象的ConfigureContainer<TContainerBuilder>方法完成对依赖注入容器的设置。

上述3个在启动过程执行的初始化操作由3个对应的Builder对象(ConfigureBuilder、ConfigureServicesBuilder和ConfigureContainerBuilder)辅助完成,其中Startup类型的Configure方法或者Configure{EnvironmentName}方法对应如下所示的ConfigureBuilder类型。ConfigureBuilder对象由Configure方法或者Configure{EnvironmentName}方法对应的MethodInfo对象创建而成,最终赋值给GenericWebHostServiceOptions配置选项ConfigureApplication属性的就是这个委托对象。如下所示的代码片段是ConfigureBuilder类型简化后的定义。

public class ConfigureBuilder
{
    public MethodInfo MethodInfo { get; }
    public ConfigureBuilder(MethodInfo configure) => MethodInfo = configure;
    
    public Action<IApplicationBuilder> Build(object instance) => builder => Invoke(instance, builder);

    private void Invoke(object instance, IApplicationBuilder builder)
    {           
        using (var scope = builder.ApplicationServices.CreateScope())
        {
            var serviceProvider = scope.ServiceProvider;
            var parameterInfos = MethodInfo.GetParameters();
            var parameters = new object[parameterInfos.Length];
            for (var index = 0; index < parameterInfos.Length; index++)
            {
                var parameterInfo = parameterInfos[index];
                parameters[index] = 
                    parameterInfo.ParameterType == typeof(IApplicationBuilder)
                    ? builder
                    : serviceProvider.GetRequiredService(parameterInfo.ParameterType);
            }
            MethodInfo.InvokeWithoutWrappingExceptions(instance, parameters);
        }
    }
}

如下所示的ConfigureServicesBuilder和ConfigureContainerBuilder类型是简化后的版本,前者对应Startup类型的ConfigureServices/Configure{EnvironmentName}Services方法,后者对应ConfigureContainer方法或者Configure{EnvironmentName}Container方法。针对对应方法的调用会反映在Build方法返回的委托对象上。

public class ConfigureServicesBuilder
{
    public MethodInfo MethodInfo { get; }
    public ConfigureServicesBuilder2(MethodInfo configureServices) => MethodInfo = configureServices;    
    public Func<IServiceCollection, IServiceProvider> Build(object instance) => services => Invoke(instance, services);
    private IServiceProvider Invoke(object instance, IServiceCollection services) => MethodInfo.InvokeWithoutWrappingExceptions(instance, new object[] { services }) as IServiceProvider;
}

public class ConfigureContainerBuilder
{
    public MethodInfo MethodInfo { get; }
    public ConfigureContainerBuilder(MethodInfo configureContainerMethod) => MethodInfo = configureContainerMethod;
    public Action<object> Build(object instance) => container => Invoke(instance, container);
    private void Invoke(object instance, object container) => MethodInfo.InvokeWithoutWrappingExceptions(instance, new object[] { container });
}

Startup类型的构造函数中是可以注入依赖服务的,但是可以在这里注入的依赖服务仅限于组成当前承载上下文的两个元素,即表示承载环境的IHostEnvironment对象或者IWebHostEnvironment对象和表示配置的IConfiguration对象。这一个特性是如下这个特殊的IServiceProvider实现类型决定的。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    private class HostServiceProvider : IServiceProvider
    {
        private readonly WebHostBuilderContext _context;
        public HostServiceProvider(WebHostBuilderContext context)
        {
            _context = context;
        }

        public object GetService(Type serviceType)
        {
            if (serviceType == typeof(IWebHostEnvironment)|| serviceType == typeof(IHostEnvironment))
            {
                return _context.HostingEnvironment;
            }
            if (serviceType == typeof(IConfiguration))
            {
                return _context.Configuration;
            }
            return null;
        }
    }
}

如下所示的代码片段是GenericWebHostBuilder的UseStartup方法简化版本的定义。可以看出,Startup类型是通过调用IHostBuilder对象的ConfigureServices方法来注册的。如下面的代码片段所示,GenericWebHostBuilder对象会根据指定Startup类型创建出3个对应的Builder对象,然后利用上面的HostServiceProvider创建出Startup对象,并将该对象作为参数调用对应的3个Builder对象的Build方法构建的委托对象完成对应的初始化任务。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    private readonly IHostBuilder _builder;
    private readonly object _startupKey = new object();      

    public IWebHostBuilder UseStartup(Type startupType)
    {
        _builder.Properties["UseStartup.StartupType"] = startupType;
        _builder.ConfigureServices((context, services) =>
        {
            if (_builder.Properties.TryGetValue("UseStartup.StartupType", out var cachedType) && (Type)cachedType == startupType)
            {
                UseStartup(startupType, context, services);
            }
        });

        return this;
    }

    private void UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
    {
        var webHostBuilderContext = GetWebHostBuilderContext(context);
        var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];

        ExceptionDispatchInfo startupError = null;
        object instance = null;
        ConfigureBuilder configureBuilder = null;

        try
        {
            instance = ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);
            context.Properties[_startupKey] = instance;
            var environmentName = context.HostingEnvironment.EnvironmentName;
            BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;

            //ConfigureServices
            var configureServicesMethod = startupType.GetMethod($"Configure{environmentName}Services", bindingFlags)?? startupType.GetMethod("ConfigureServices", bindingFlags);
            if (configureServicesMethod != null)
            {
                var configureServicesBuilder = new ConfigureServicesBuilder(configureServicesMethod);
                var configureServices = configureServicesBuilder.Build(instance);
                configureServices(services);
            }

            //ConfigureContainer
            var configureContainerMethod = startupType.GetMethod($"Configure{environmentName}Container", bindingFlags)?? startupType.GetMethod("ConfigureContainer", bindingFlags);
            if (configureContainerMethod != null)
            {
                var configureContainerBuilder = new ConfigureBuilder(configureServicesMethod);
                _builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder;
                var containerType = configureContainerBuilder.MethodInfo.GetParameters()[0].ParameterType;
                var actionType = typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType);
                var configure = GetType().GetMethod(nameof(ConfigureContainer), BindingFlags.NonPublic | BindingFlags.Instance).MakeGenericMethod(containerType).CreateDelegate(actionType, this);

                //IHostBuilder.ConfigureContainer<TContainerBuilder>(
                //Action<HostBuilderContext, TContainerBuilder> configureDelegate)
                typeof(IHostBuilder).GetMethods().First(m => m.Name == nameof(IHostBuilder.ConfigureContainer)).MakeGenericMethod(containerType).Invoke(_builder, BindingFlags.DoNotWrapExceptions, null, new object[] { configure },null);
            }

            var configureMethod = startupType.GetMethod($"Configure{environmentName}", bindingFlags)?? startupType.GetMethod("Configure", bindingFlags);
            configureBuilder = new ConfigureBuilder(configureMethod);
        }
        catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
        {
            startupError = ExceptionDispatchInfo.Capture(ex);
        }

        //Configure
        services.Configure<GenericWebHostServiceOptions>(options =>
        {
            options.ConfigureApplication = app =>
            {                   
                startupError?.Throw();
                if (instance != null && configureBuilder != null)
                {
                    configureBuilder.Build(instance)(app);
                }
            };
        });
    }

    //用于创建IHostBuilder.ConfigureContainer<TContainerBuilder>(
    //Action<HostBuilderContext, TContainerBuilder> configureDelegate)方法的参数
    private void ConfigureContainer<TContainer>(HostBuilderContext context, TContainer container)
    {
        var instance = context.Properties[_startupKey];
        var builder = (ConfigureContainerBuilder)context.Properties[typeof(ConfigureContainerBuilder)];
        builder.Build(instance)(container);
    }
}

除了上面的UseStartup方法,GenericWebHostBuilder还实现了ISupportsStartup接口的Configure方法。如下面的代码片段所示,该方法会将指定的用于注册中间件的Action<WebHostBuilderContext, IApplicationBuilder>复制给作为配置选项的GenericWebHostServiceOptions对象的ConfigureApplication属性。由于注册Startup类型的目的也是通过设置GenericWebHostServiceOptions对象的ConfigureApplication属性来注册中间件,如果在调用了Configure方法时又注册了一个Startup类型,系统会采用“后来居上”的原则。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    public IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuilder> configure)
    {
        _builder.ConfigureServices((context, services) =>
        {
            services.Configure<GenericWebHostServiceOptions>(options =>
            {
                var webhostBuilderContext = GetWebHostBuilderContext(context);
                options.ConfigureApplication = app => configure(webhostBuilderContext, app);
            });
        });
        return this;
    }
}

除了直接调用UseStartup方法注册一个Startup类型,还可以利用配置注册Startup类型所在的程序集。GenericWebHostBuilder对象在初始化过程中会按照约定的规则定位和加载Startup类型。通过第11章的介绍可知,GenericWebHostBuilder对象会按照如下顺序从指定的程序集类型列表中筛选Startup类型。

  • Startup{EnvironmentName}(全名匹配)。
  • Startup(全名匹配)。
  • {StartupAssembly}.Startup{EnvironmentName}(全名匹配)。
  • {StartupAssembly}.Startup(全名匹配)。
  • Startup{EnvironmentName}(任意命名空间)。
  • Startup(任意命名空间)。

从指定启动程序集中加载Startup类型的逻辑体现在如下所示的FindStartupType方法中。在执行构造函数的最后阶段,如果WebHostOptions选项的StartupAssembly属性被设置了一个启动程序集,定义在该程序集中的Startup类型会被加载出来,并作为参数调用上面定义的UseStartup方法完成对它的注册。如果在此过程中抛出异常,并且将WebHostOptions选项的CaptureStartupErrors属性设置为True,那么捕捉到的异常会通过设置GenericWebHostServiceOptions对象的ConfigureApplication属性的方式重新抛出来。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    public GenericWebHostBuilder(IHostBuilder builder)
    {
        ...
        if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
        {
            try
            {
                var startupType = FindStartupType(webHostOptions.StartupAssembly, webhostContext.HostingEnvironment.EnvironmentName);
                UseStartup(startupType, context, services);
            }
            catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
            {
                var capture = ExceptionDispatchInfo.Capture(ex);
                services.Configure<GenericWebHostServiceOptions>(options =>
                {
                    options.ConfigureApplication = app => capture.Throw();
                });
            }
        }    
   }

   private static Type FindStartupType(string startupAssemblyName, string environmentName)
   {
        var assembly = Assembly.Load(new AssemblyName(startupAssemblyName))?? throw new InvalidOperationException(string.Format("The assembly '{0}' failed to load.", startupAssemblyName));
        var startupNameWithEnv = $"Startup{environmentName}";
        var startupNameWithoutEnv = "Startup";

        var type =
            assembly.GetType(startupNameWithEnv) ??
            assembly.GetType(startupAssemblyName + "." + startupNameWithEnv) ??
            assembly.GetType(startupNameWithoutEnv) ??
            assembly.GetType(startupAssemblyName + "." + startupNameWithoutEnv);
        if (null != type)
        {
            return type;
        }

        var types = assembly.DefinedTypes.ToList();
        type = types.Where(info => info.Name.Equals(startupNameWithEnv, StringComparison.OrdinalIgnoreCase)).FirstOrDefault()
            ?? types.Where(info => info.Name.Equals(startupNameWithoutEnv, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
        return type??  throw new InvalidOperationException(string.Format("A type named '{0}' or '{1}' could not be found in assembly '{2}'.",startupNameWithEnv,startupNameWithoutEnv,startupAssemblyName));
    }
}

六、Hosting Startup

Startup类型可定义在任意程序集中,并通过配置的方式注册到ASP.NET Core应用中。Hosting Startup与之类似,我们可以将一些初始化操作定义在任意程序集中,在无须修改应用程序任何代码的情况下利用配置的方式实现对它们的注册。两者的不同之处在于:整个应用最终只会使用到一个Startup类型,但是采用Hosting Startup注册的初始化操作都是有效的。Hosting Startup类型提供的方式将一些工具“附加”到一个ASP.NET Core应用中。以Hosting Startup方法实现的初始化操作必须实现在IHostingStartup接口的实现类型中,该类型最终以HostingStartupAttribute特性的方式进行注册。Hosting Startup相关的配置最终体现在WebHostOptions如下的3个属性上。

public class WebHostOptions
{
    public bool PreventHostingStartup { get; set; }
    public IReadOnlyList<string> HostingStartupAssemblies { get; set; }
    public IReadOnlyList<string> HostingStartupExcludeAssemblies { get; set; }
}

定义在IHostingStartup实现类型上的初始化操作会作用在一个IWebHostBuilder对象上,但最终对象并不是GenericWebHostBuilder,而是如下所示的HostingStartupWebHostBuilder对象。从给出的代码片段可以看出,HostingStartupWebHostBuilder对象实际上是对GenericWebHostBuilder对象的进一步封装,针对它的方法调用最终还是会转移到封装的GenericWebHostBuilder对象上。

internal class HostingStartupWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
    private readonly GenericWebHostBuilder _builder;
    private Action<WebHostBuilderContext, IConfigurationBuilder> _configureConfiguration;
    private Action<WebHostBuilderContext, IServiceCollection> _configureServices;

    public HostingStartupWebHostBuilder(GenericWebHostBuilder builder) =>_builder = builder;

    public IWebHost Build() => throw new NotSupportedException();

    public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate)
    {
        _configureConfiguration += configureDelegate;
        return this;
    }

    public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices) => ConfigureServices((context, services) => configureServices(services));

    public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices)
    {
        _configureServices += configureServices;
        return this;
    }
    public string GetSetting(string key) => _builder.GetSetting(key);

    public IWebHostBuilder UseSetting(string key, string value)
    {
        _builder.UseSetting(key, value);
        return this;
    }

    public void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) => _configureServices?.Invoke(context, services);

    public void ConfigureAppConfiguration(WebHostBuilderContext context, IConfigurationBuilder builder)=> _configureConfiguration?.Invoke(context, builder);

    public IWebHostBuilder UseDefaultServiceProvider(Action<WebHostBuilderContext, ServiceProviderOptions> configure)=> _builder.UseDefaultServiceProvider(configure);

    public IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuilder> configure) => _builder.Configure(configure);

    public IWebHostBuilder UseStartup(Type startupType)=> _builder.UseStartup(startupType);
}

Hosting Startup的实现体现在如下所示的ExecuteHostingStartups方法中,该方法会根据当前的配置和作为应用名称的入口程序集名称创建一个新的WebHostOptions对象,如果这个配置选项的PreventHostingStartup属性返回True,就意味着人为关闭了Hosting Startup特性。在Hosting Startup特性没有被显式关闭的情况下,该方法会利用配置选项的HostingStartupAssemblies属性和HostingStartupExcludeAssemblies属性解析出启动程序集名称,并从中解析出注册的IHostingStartup类型。在通过反射的方式创建出对应的IHostingStartup对象之后,上面介绍的HostingStartupWebHostBuilder对象会被创建出来,并作为参数调用这些IHostingStartup对象的Configure方法。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    private readonly IHostBuilder_builder;
    private readonly IConfiguration _config;

    public GenericWebHostBuilder(IHostBuilder builder)
    {
        _builder = builder;
        _config = new ConfigurationBuilder().AddEnvironmentVariables(prefix: "ASPNETCORE_").Build();

        _builder.ConfigureHostConfiguration(config =>
        {
            config.AddConfiguration(_config);            
            ExecuteHostingStartups();
        });
   }

    private void ExecuteHostingStartups()
    {
        var options = new WebHostOptions(_config, Assembly.GetEntryAssembly()?.GetName().Name);
        if (options.PreventHostingStartup)
        {
            return;
        }

        var exceptions = new List<Exception>();
        _hostingStartupWebHostBuilder = new HostingStartupWebHostBuilder(this);

        var assemblyNames = options.HostingStartupAssemblies.Except(options.HostingStartupExcludeAssemblies, StringComparer.OrdinalIgnoreCase).Distinct(StringComparer.OrdinalIgnoreCase);
        foreach (var assemblyName in assemblyNames)
        {
            try
            {
                var assembly = Assembly.Load(new AssemblyName(assemblyName));
                foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>())
                {
                    var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType);
                    hostingStartup.Configure(_hostingStartupWebHostBuilder);
                }
            }
            catch (Exception ex)
            {
                exceptions.Add(new InvalidOperationException($"Startup assembly {assemblyName} failed to execute. See the inner exception for more details.", ex));
            }
        }
        if (exceptions.Count > 0)
        {
            _hostingStartupErrors = new AggregateException(exceptions);
        }
    }
}

由于调用IHostingStartup对象的Configure方法传入的HostingStartupWebHostBuilder对象是对当前GenericWebHostBuilder对象的封装,而这个GenericWebHostBuilder对象又是对IHostBuilder的封装,所以以Hosting Startup注册的初始化操作最终还是应用到了以IHost/IHostBuilder为核心的承载系统中。虽然GenericWebHostBuilder类型实现了IWebHostBuilder接口,但它仅仅是IHostBuilder对象的代理,其自身针对IWebHost对象的构建需求不复存在,所以它的Build方法会直接抛出异常。

internal class GenericWebHostBuilder : 
    IWebHostBuilder, 
    ISupportsStartup, 
    ISupportsUseDefaultServiceProvider
{
    public IWebHost Build()=> throw new NotSupportedException($"Building this implementation of {nameof(IWebHostBuilder)} is not supported.");
    ...
}

七、ConfigureWebHostDefaults

演示实例中针对IWebHostBuilder对象的应用都体现在IHostBuilder接口的ConfigureWebHostDefaults扩展方法上,它最终调用的其实是如下所示的ConfigureWebHost扩展方法。如下面的代码片段所示,ConfigureWebHost方法会将当前IHostBuilder对象创建的GenericWebHostBuilder对象作为参数调用指定的Action<IWebHostBuilder>委托对象。由于GenericWebHostBuilder对象相当于IHostBuilder对象的代理,所以这个委托中完成的所有操作最终都会转移到IHostBuilder对象上。

public static class GenericHostWebHostBuilderExtensions
{
    public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
    {
        var webhostBuilder = new GenericWebHostBuilder(builder);
        configure(webhostBuilder);
        return builder;
    }
}

顾名思义,ConfigureWebHostDefaults方法会帮助我们做默认设置,这些设置实现在静态类型WebHost的ConfigureWebDefaults方法中。如下所示的ConfigureWebDefaults方法的实现,该方法提供的默认设置包括将定义在Microsoft.AspNetCore.StaticWebAssets.xml文件(物理文件或者内嵌文件)作为默认的Web资源、注册KestrelServer、配置关于主机过滤(Host Filter)和Http Overrides相关选项、注册路由中间件,以及对用于集成IIS的AspNetCoreModule模块的配置。

public static class GenericHostBuilderExtensions
{
    public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure) => builder.ConfigureWebHost(webHostBuilder =>
        {
            WebHost.ConfigureWebDefaults(webHostBuilder);
            configure(webHostBuilder);
        });
}

public static class WebHost
{
    internal static void ConfigureWebDefaults(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration((ctx, cb) =>
        {
            if (ctx.HostingEnvironment.IsDevelopment())
            {
                StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment);
            }
        });
        builder.UseKestrel((builderContext, options) =>
        {
            options.Configure(builderContext.Configuration.GetSection("Kestrel"));
        })
        .ConfigureServices((hostingContext, services) =>
        {            
            services.PostConfigure<HostFilteringOptions>(options =>
            {
                if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
                {
                    var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                    options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
                }
            });
            services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));

            services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();

            if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
            {
                services.Configure<ForwardedHeadersOptions>(options =>
                {
                    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor|ForwardedHeaders.XForwardedProto;
                    
                    options.KnownNetworks.Clear();
                    options.KnownProxies.Clear();
                });

                services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
            }

            services.AddRouting();
        })
        .UseIIS()
        .UseIISIntegration();
    }
}

请求处理管道[1]: 模拟管道实现
请求处理管道[2]: HttpContext本质论
请求处理管道[3]: Pipeline = IServer +  IHttpApplication<TContext
请求处理管道[4]: 中间件委托链
请求处理管道[5]: 应用承载[上篇
请求处理管道[6]: 应用承载[下篇]

posted @ 2020-12-03 09:00  Artech  阅读(625)  评论(0编辑  收藏  举报