1. 程式人生 > >ASP.NET Core管道詳解[6]: ASP.NET Core應用是如何啟動的?[下篇]

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