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]: 應用承載[下篇]