理解ASP.NET Core - [04] Host

xiaoxiaotank發表於2021-09-16

注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄

本文會涉及部分 Host 相關的原始碼,並會附上 github 原始碼地址,不過為了降低篇幅,我會刪除一些不涉及的程式碼。

為了方便,還是建議你將原始碼(.net5)runtimeaspnetcore 下載下來,通過VS等工具閱讀

請耐心閱讀!

Generic Host & WebHost

在.NET Core 2.x時,ASP.NET Core 預設使用的是WebHost

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

而到了.NET Core 3.x,ASP.NET Core 預設選擇使用Generic Host

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

那麼,為什麼.NET團隊要將Web主機(Web Host)替換為通用主機(Generic Host)呢?

參考 What is the difference between Host and WebHost class in asp.net core

Generic Host在.NET Core 2.1就已經存在了,並且它就是按照.NET Core未來版本的通用標準來實現的。不過由於當時的Generic Host只能用於非HTTP工作負載,所以.NET Core 2.x仍然使用的是 Web Host。不過到了.NET Core 3.x,Generic Host已經可以同時支援HTTP和非HTTP工作負載了。

為什麼要使用Generic Host呢?那是因為Web Host與HTTP請求緊密關聯,且用於Web應用。然而,隨著微服務和Docker的出現,.NET團隊認為需要一個更加通用的主機,不僅能夠服務於Web應用,還能服務於控制檯等其他型別的應用。所以就實現了Generic Host

在我們的ASP.NET Core應用中,需要建立一個Generic Host,並通過ConfigureWebHostDefaults等擴充套件方法針對Web Host進行配置。

所以,我們應該在所有型別的應用中始終使用通用主機

因此,接下來我們們就聊一下通用主機。

Generic Host——通用主機

先上兩張Host的啟動流程圖:

請大家就著上面這張兩圖食用以下內容。

ConfigureXXX

在深入之前,大家要先了解一下ConfigureHostConfigurationConfigureAppConfigurationConfigureServices等方法到底做了什麼。其實,很簡單,就是將委託暫存到了一個臨時變數裡。

public class HostBuilder : IHostBuilder
{
    private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
    private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>();
    private List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<HostBuilderContext, IServiceCollection>>();

    public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
    {
        _configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
        return this;
    }
    
    public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
    {
        _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
        return this;
    }
        
    public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
    {
        _configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
        return this;
    }
}

Host.CreateDefaultBuilder(args)

public static class Host
{
    public static IHostBuilder CreateDefaultBuilder(string[] args)
    {
        var builder = new HostBuilder();
    
        // 將 Content Root(專案根目錄)設定為 Directory.GetCurrentDirectory (當前工作目錄)
        builder.UseContentRoot(Directory.GetCurrentDirectory());
        builder.ConfigureHostConfiguration(config =>
        {
            // 新增以 DOTNET_ 為字首的環境變數(會將字首刪除作為環境變數的Key)
            config.AddEnvironmentVariables(prefix: "DOTNET_");
            if (args != null)
            {
                // 新增命令列引數 args
                config.AddCommandLine(args);
            }
        });
    
        builder.ConfigureAppConfiguration((hostingContext, config) =>
        {
            IHostEnvironment env = hostingContext.HostingEnvironment;
    
            // 預設當配置發生更改時,過載配置
            bool reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
    
            // appsettings.json、appsettings.{Environment}.json
            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
                  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
    
            // 啟用 User Secrets(僅當執行在 Development 環境時)
            if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
            {
                var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                if (appAssembly != null)
                {
                    config.AddUserSecrets(appAssembly, optional: true);
                }
            }
    
            // 新增環境變數(未限定字首)
            // 目的是當應用(App)配置載入完畢後(注意是載入完畢後),允許讀取所有環境變數,且優先順序更高
            // 即若存在多個同名的環境變數,不帶字首的比帶字首的優先順序更高
            config.AddEnvironmentVariables();
    
            if (args != null)
            {
                // 新增命令列引數 args
                config.AddCommandLine(args);
            }
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
    
            if (isWindows)
            {
                logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
            }
    
            // 新增 Logging 配置
            logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
            
            logging.AddConsole();
            logging.AddDebug();
            logging.AddEventSourceLogger();
    
            if (isWindows)
            {
                // 在Windows平臺上,新增 EventLogLoggerProvider
                logging.AddEventLog();
            }
    
            logging.Configure(options =>
            {
                options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
                                                    | ActivityTrackingOptions.TraceId
                                                    | ActivityTrackingOptions.ParentId;
            });
    
        })
        .UseDefaultServiceProvider((context, options) =>
        {
            // 啟用範圍驗證 scope validation 和依賴關係驗證 dependency validation(僅當執行在 Development 環境時)
            bool isDevelopment = context.HostingEnvironment.IsDevelopment();
            options.ValidateScopes = isDevelopment;
            options.ValidateOnBuild = isDevelopment;
        });
    
        return builder;
    }
}

ConfigureWebHostDefaults

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

public static class GenericHostWebHostBuilderExtensions
{
    public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
    {
        return builder.ConfigureWebHost(configure, _ => { });
    }
    
    public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
    {
        var webHostBuilderOptions = new WebHostBuilderOptions();
        configureWebHostBuilder(webHostBuilderOptions);
        
        // 重點1: GenericWebHostBuilder
        var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);
        configure(webhostBuilder);
        
        // 重點2:GenericWebHostService
        builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
        return builder;
    }
}

上面這段程式碼重點有兩個:

  • 一個是GenericWebHostBuilder這個類,記住它,ConfigureWebHostDefaults委託中的webBuilder引數就是它!
  • 另一個是GenericWebHostService

下面,我們先看一下GenericWebHostBuilder的建構函式:

internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
    public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
    {
        _builder = builder;
        var configBuilder = new ConfigurationBuilder()
            .AddInMemoryCollection();
    
        if (!options.SuppressEnvironmentConfiguration)
        {
            // 新增以 ASPNETCORE_ 為字首的環境變數(會將字首刪除作為環境變數的Key)
            configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_");
        }
    
        _config = configBuilder.Build();
    
        _builder.ConfigureHostConfiguration(config =>
        {
            // 新增到主機(Host)配置
            config.AddConfiguration(_config);
            
            // 執行 HostingStartups,詳見下方的 ExecuteHostingStartups 方法
            ExecuteHostingStartups();
        });
    
        _builder.ConfigureAppConfiguration((context, configurationBuilder) =>
        {
            // 在 ExecuteHostingStartups 方法中,該欄位通常會被初始化
            if (_hostingStartupWebHostBuilder != null)
            {
                var webhostContext = GetWebHostBuilderContext(context);
                // 載入 HostingStartups 中新增的應用(App)配置
                _hostingStartupWebHostBuilder.ConfigureAppConfiguration(webhostContext, configurationBuilder);
            }
        });
    
        _builder.ConfigureServices((context, services) =>
        {
            var webhostContext = GetWebHostBuilderContext(context);
            var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];
    
            // 註冊 IWebHostEnvironment
            services.AddSingleton(webhostContext.HostingEnvironment);
            services.AddSingleton((AspNetCore.Hosting.IHostingEnvironment)webhostContext.HostingEnvironment);
            services.AddSingleton<IApplicationLifetime, GenericWebHostApplicationLifetime>();
    
            services.Configure<GenericWebHostServiceOptions>(options =>
            {
                options.WebHostOptions = webHostOptions;
                options.HostingStartupExceptions = _hostingStartupErrors;
            });
    
            var listener = new DiagnosticListener("Microsoft.AspNetCore");
            services.TryAddSingleton<DiagnosticListener>(listener);
            services.TryAddSingleton<DiagnosticSource>(listener);
    
            services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
            services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
            services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();
    
            // 註冊 IHostingStartup 中配置的服務
            _hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);
    
            if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
            {
                try
                {
                    var startupType = StartupLoader.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 void ExecuteHostingStartups()
    {
        var webHostOptions = new WebHostOptions(_config, Assembly.GetEntryAssembly()?.GetName().Name);
    
        if (webHostOptions.PreventHostingStartup)
        {
            return;
        }
    
        var exceptions = new List<Exception>();
        // 注意這裡對 _hostingStartupWebHostBuilder 進行了初始化
        _hostingStartupWebHostBuilder = new HostingStartupWebHostBuilder(this);
    
        // 從當前程式集和環境變數`ASPNETCORE_HOSTINGSTARTUPASSEMBLIES`配置的程式集列表(排除`ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES`中配置的程式集列表)中尋找特性`HostingStartupAttribute`,
        // 並通過反射的方式建立特性所標識的`IHostingStartup`實現的例項,並呼叫其`Configure`方法。
        foreach (var assemblyName in webHostOptions.GetFinalHostingStartupAssemblies().Distinct(StringComparer.OrdinalIgnoreCase))
        {
            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);
        }
    }
}

接著來看WebHost.ConfigureWebDefaults

public static class WebHost
{
    internal static void ConfigureWebDefaults(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration((ctx, cb) =>
        {
            if (ctx.HostingEnvironment.IsDevelopment())
            {
                StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
            }
        });
        // 將 Kestrel 伺服器設定為 Web 伺服器,並新增配置
        builder.UseKestrel((builderContext, options) =>
        {
            options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
        })
        .ConfigureServices((hostingContext, services) =>
        {
            // 配置主機過濾中介軟體(Host Filtering)
            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>();
    
            // 當環境變數 ASPNETCORE_FORWARDEDHEADERS_ENABLED 為 true 時,新增轉接頭中介軟體(Forwarded Headers)
            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();
        })
        // 啟用IIS整合
        .UseIIS()
        .UseIISIntegration();
    }
}

我們通常會在ConfigureWebHostDefaults擴充套件方法的委託中呼叫UseStartup來指定Startup類,下面我們就來看一下UseStartup到底做了什麼:將Startup.ConfigureServices中要註冊的服務新增到ConfigureServices的委託中

public static class WebHostBuilderExtensions
{
    public static IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)]TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class
    {
        return hostBuilder.UseStartup(typeof(TStartup));
    }
    
    public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, [DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType)
    {
        // ...刪除了一些程式碼
    
        // 會進入該條件分支
        // 不知道為什麼進入該分支?上面讓你牢記的 GenericWebHostBuilder 還記得嗎?快去看看它實現了哪些介面
        if (hostBuilder is ISupportsStartup supportsStartup)
        {
            return supportsStartup.UseStartup(startupType);
        }
    
        // ...刪除了一些程式碼
    }
}

internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
    public IWebHostBuilder UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType)
    {
        // 可以看到,雖然 UseStartup 可以呼叫多次,但是隻有最後一次才有效
        _startupObject = startupType;
    
        // 將 Startup.ConfigureServices 中要註冊的服務新增進來
        // 好了,暫時看到這裡就ok了
        _builder.ConfigureServices((context, services) =>
        {
            if (object.ReferenceEquals(_startupObject, startupType))
            {
                UseStartup(startupType, context, services);
            }
        });
    
        return this;
    }
}

最後,看一下上面提到的第二個重點GenericWebHostService:用於後續Run方法時執行Configure(包括StartupFilters.ConfigureStartup.Configure等)

internal class GenericWebHostService : IHostedService
{
    // 建構函式注入
    public GenericWebHostServiceOptions Options { get; }
    // 建構函式注入
    public IEnumerable<IStartupFilter> StartupFilters { get; }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // ...刪除了一些程式碼

        RequestDelegate application = null;

        try
        {
            // 這裡取到了 Startup.Configure
            // 可能你不知道為什麼這裡可以取到,彆著急,文章後面會為你解釋的
            Action<IApplicationBuilder> configure = Options.ConfigureApplication;

            // 要求 Startup 必須包含 Configure 方法,或必須呼叫 IWebHostBuilder.Configure
            if (configure == null)
            {
                throw new InvalidOperationException($"No application configured. Please specify an application via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration.");
            }

            var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);
            
            // 注意:這裡來執行 StartupFilters.Configure 與 Startup.Configure 
            // 將 Startup.Configure 與 StartupFilters.Configure 連線成中介軟體管道
            // 為什麼 Reverse?因為要先執行 StartupFilters.Configure,最後才執行 Startup.Configure,
            // 所以用類似鏈條的方式,從尾巴開始向頭部牽手,這樣,最終得到的 configure 指向的就是頭部
            // 當執行 configure 時,就可以從頭部流轉到尾巴
            foreach (var filter in StartupFilters.Reverse())
            {
                configure = filter.Configure(configure);
            }

            // 執行 Configure 方法
            configure(builder);

            // Build HTTP 請求管道
            application = builder.Build();
        }
        catch (Exception ex)
        {
            Logger.ApplicationError(ex);

            if (!Options.WebHostOptions.CaptureStartupErrors)
            {
                throw;
            }

            application = BuildErrorPageApplication(ex);
        }

        var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, HttpContextFactory);

        await Server.StartAsync(httpApplication, cancellationToken);

        // ...刪除了一些程式碼
    }
}

Build

public class HostBuilder : IHostBuilder
{
    public IHost Build()
    {
        // 載入主機(Host)配置
        BuildHostConfiguration();
        // 例項化 HostingEnvironment
        CreateHostingEnvironment();
        // 例項化 HostBuilderContext
        CreateHostBuilderContext();
        // 載入應用(App)配置
        BuildAppConfiguration();
        // 註冊服務並建立 Service Provider
        CreateServiceProvider();
    
        // 生成 IHost 例項並返回
        return _appServices.GetRequiredService<IHost>();
    }
}

BuildHostConfiguration

public class HostBuilder : IHostBuilder
{
    private void BuildHostConfiguration()
    {
        IConfigurationBuilder configBuilder = new ConfigurationBuilder()
            .AddInMemoryCollection(); 
    
        // 載入主機(Host)配置(同時會執行上面所說的 IHostingStartup.Configure)
        foreach (Action<IConfigurationBuilder> buildAction in _configureHostConfigActions)
        {
            buildAction(configBuilder);
        }
        _hostConfiguration = configBuilder.Build();
    }
}

CreateHostingEnvironment

public class HostBuilder : IHostBuilder
{
    private void CreateHostingEnvironment()
    {
        _hostingEnvironment = new HostingEnvironment()
        {
            ApplicationName = _hostConfiguration[HostDefaults.ApplicationKey],
            EnvironmentName = _hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production,
            ContentRootPath = ResolveContentRootPath(_hostConfiguration[HostDefaults.ContentRootKey], AppContext.BaseDirectory),
        };
    
        if (string.IsNullOrEmpty(_hostingEnvironment.ApplicationName))
        {
            _hostingEnvironment.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name;
        }
    
        _hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(_hostingEnvironment.ContentRootPath);
    }
}

CreateHostBuilderContext

public class HostBuilder : IHostBuilder
{
    private void CreateHostBuilderContext()
    {
        _hostBuilderContext = new HostBuilderContext(Properties)
        {
            HostingEnvironment = _hostingEnvironment,
            Configuration = _hostConfiguration
        };
    }
}

BuildAppConfiguration

public class HostBuilder : IHostBuilder
{
    private void BuildAppConfiguration()
    {
        IConfigurationBuilder configBuilder = new ConfigurationBuilder()
            .SetBasePath(_hostingEnvironment.ContentRootPath)
            .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);
    
        foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
        {
            buildAction(_hostBuilderContext, configBuilder);
        }
        _appConfiguration = configBuilder.Build();
        _hostBuilderContext.Configuration = _appConfiguration;
    }
}

CreateServiceProvider

public class HostBuilder : IHostBuilder
{
    private void CreateServiceProvider()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
        // 註冊 IHostEnvironment
        services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
        // 註冊 HostBuilderContext
        services.AddSingleton(_hostBuilderContext);
        // 註冊 IConfiguration,所以能在 Startup 中進行建構函式注入
        services.AddSingleton(_ => _appConfiguration);
        services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
        services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
        // 注意這裡註冊了 IHostLifetime 服務的例項 ConsoleLifetime
        services.AddSingleton<IHostLifetime, ConsoleLifetime>();
        // 註冊 IHost 例項
        services.AddSingleton<IHost, Internal.Host>();
        services.AddOptions();
        services.AddLogging();
    
        // 執行 ConfigureServices 方法中的委託進行服務註冊
        // 包括使用擴充套件方法 ConfigureServices、 Startup.ConfigureServices 等設定的委託
        foreach (Action<HostBuilderContext, IServiceCollection> configureServicesAction in _configureServicesActions)
        {
            configureServicesAction(_hostBuilderContext, services);
        }
    
        object containerBuilder = _serviceProviderFactory.CreateBuilder(services);
    
        // 載入容器配置
        foreach (IConfigureContainerAdapter containerAction in _configureContainerActions)
        {
            containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
        }
    
        // 建立 Service Provider
        _appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);
    
        if (_appServices == null)
        {
            throw new InvalidOperationException($"The IServiceProviderFactory returned a null IServiceProvider.");
        }
    
        _ = _appServices.GetService<IConfiguration>();
    }
}

Run

public static class HostingAbstractionsHostExtensions
{
    public static void Run(this IHost host)
    {
        host.RunAsync().GetAwaiter().GetResult();
    }

    public static async Task RunAsync(this IHost host, CancellationToken token = default)
    {
        try
        {
            await host.StartAsync(token).ConfigureAwait(false);
    
            await host.WaitForShutdownAsync(token).ConfigureAwait(false);
        }
        finally
        {
            if (host is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync().ConfigureAwait(false);
            }
            else
            {
                host.Dispose();
            }
    
        }
    }
}

StartAsync

internal class Host : IHost, IAsyncDisposable
{
    public async Task StartAsync(CancellationToken cancellationToken = default)
    {
        _logger.Starting();
    
        using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
        CancellationToken combinedCancellationToken = combinedCancellationTokenSource.Token;
    
        // _hostLifetime 是在建構函式注入的
        // 還記得嗎?在上面的 CreateServiceProvider 方法中,注入了該服務的預設例項 ConsoleLifetime,在下方你可以看到 ConsoleLifetime 的部分實現
        await _hostLifetime.WaitForStartAsync(combinedCancellationToken).ConfigureAwait(false);
    
        combinedCancellationToken.ThrowIfCancellationRequested();
        
        // 這裡面就包含我們上面提到的重點 GenericWebHostService
        _hostedServices = Services.GetService<IEnumerable<IHostedService>>();
    
        foreach (IHostedService hostedService in _hostedServices)
        {
            // 啟用 IHostedService.StartAsync
            await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
        }
    
        // 啟用 IHostApplicationLifetime.Started
        _applicationLifetime.NotifyStarted();
    
        _logger.Started();
    }
}

public class ConsoleLifetime : IHostLifetime, IDisposable
{
    public Task WaitForStartAsync(CancellationToken cancellationToken)
    {
        // ...刪除了一些程式碼
    
        // 註冊了程式退出回撥
        AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
        // 註冊了 Ctrl + C 回撥(這下你知道為啥執行了 Ctrl + C 程式就退出了吧?)
        Console.CancelKeyPress += OnCancelKeyPress;
    
        // 立即啟動 Console applications
        return Task.CompletedTask;
    }
    
    private void OnProcessExit(object sender, EventArgs e)
    {
        ApplicationLifetime.StopApplication();
        if (!_shutdownBlock.WaitOne(HostOptions.ShutdownTimeout))
        {
            Logger.LogInformation("Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks.");
        }
        _shutdownBlock.WaitOne();

        System.Environment.ExitCode = 0;
    }
    
    private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs e)
    {
        e.Cancel = true;
        ApplicationLifetime.StopApplication();
    }
}

WaitForShutdownAsync

public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default)
{
    IHostApplicationLifetime applicationLifetime = host.Services.GetService<IHostApplicationLifetime>();

    token.Register(state =>
    {
        ((IHostApplicationLifetime)state).StopApplication();
    },
    applicationLifetime);

    var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
    applicationLifetime.ApplicationStopping.Register(obj =>
    {
        var tcs = (TaskCompletionSource<object>)obj;
        tcs.TrySetResult(null);
    }, waitForStop);

    // 正是由於此處,程式 Run 起來後,在 applicationLifetime.ApplicationStopping 被觸發前,能夠一直保持執行狀態
    await waitForStop.Task.ConfigureAwait(false);

    await host.StopAsync(CancellationToken.None).ConfigureAwait(false);
}

Host的整個啟動流程,就差不多說完了。

服務介面

接下來我們們就從上面註冊的預設服務中,挑幾個詳細聊一下。

IHostedService

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);

    Task StopAsync(CancellationToken cancellationToken);
}

IHostedService用於在應用啟動和關閉時,執行一些額外的操作。可以新增多個,都會被執行。

程式碼例項請檢視接下來的IHostApplicationLifetime

IHostApplicationLifetime

通過該服務,可以針對程式啟動後、正常關閉前和正常關閉後指定要執行的操作。

該服務生命週期被註冊為Singleton,所以可以將該服務註冊到任何類中。

該服務所擁有的3個屬性ApplicationStartedApplicationStoppingApplicationStopped型別均為CancellationToken,當程式執行到某個生命週期節點時,就會觸發對應屬性的Cancel命令,進而執行註冊的委託。

該服務的預設註冊實現是Microsoft.Extensions.Hosting.Internal.ApplicationLifetime,程式碼很簡單,就是在程式啟動後、正常關閉前和正常關閉後觸發對應的3個屬性。

另外,該服務還擁有StopApplication方法,用於請求停止當前應用程式的執行。

需要注意的是,IHostApplicationLifetime不允許註冊自己的實現,只能使用微軟提供的預設實現。

接下來就舉個例子吧(配合IHostedService):

/// <summary>
/// 通用主機服務的生命週期事件
/// </summary>
public class LifetimeEventsHostedService : IHostedService
{
    private readonly ILogger _logger;
    private readonly IHostApplicationLifetime _appLifetime;

    public LifetimeEventsHostedService(
        ILogger<LifetimeEventsHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;
        _appLifetime = appLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _appLifetime.ApplicationStarted.Register(OnStarted);
        _appLifetime.ApplicationStopping.Register(OnStopping);
        _appLifetime.ApplicationStopped.Register(OnStopped);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("App Started");
    }

    private void OnStopping()
    {
        _logger.LogInformation("App Stopping");
    }

    private void OnStopped()
    {
        _logger.LogInformation("App Stopped");
    }
}

// 注入服務
public void ConfigureServices(IServiceCollection services)
{
    services.AddHostedService<LifetimeEventsHostedService>();
}

IHostLifetime

該服務生命週期被註冊為Singleton,以最後一個註冊的實現為準。

預設註冊的實現是Microsoft.Extensions.Hosting.Internal.ConsoleLifetime,該實現:

  • 監聽Ctrl + C指令,並呼叫IHostApplicationLifetime.StopApplication方法來關閉程式。
  • 解除RunAsyncWaitForShutdownAsync等擴充套件方法的阻塞呼叫。

IHostEnvironment & IWebHostEnvironment

這兩個服務生命週期均被註冊為Singleton

通過IHostEnvironment,我們可以獲取到:

  • ApplicationName
  • EnvironmentName
  • ContentRootPath
  • ContentRootFileProvider

IWebHostEnvironment繼承於IHostEnvironment,在其基礎上,又增加了:

  • WebRootPath
  • WebRootFileProvider

[01] Startup 中,我留下了一個問題,就是Startup類的建構函式中,IHostEnvironmentIWebHostEnvironment是同一個例項,這是為什麼呢?接下來就來解開大家的疑惑:

或許你還會疑惑,明明我們使用的 Service Provider 要在 Startup.ConfigureServices 執行完畢後,才會被建立,為啥 Startup 的建構函式中卻還能進行依賴注入呢?下面也會解答你得疑惑!

上面解讀UseStartup時,看到一半就停下了,那是因為我要在這裡和大家一起來更深入的理解:

internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
    private void UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType, HostBuilderContext context, IServiceCollection services, object instance = null)
    {
        var webHostBuilderContext = GetWebHostBuilderContext(context);
        var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];
    
        ExceptionDispatchInfo startupError = null;
        ConfigureBuilder configureBuilder = null;
    
        try
        {
            // 建立 Startup 例項
            // 注意,這裡使用的 Service Provider 是 HostServiceProvider (不是我們經常使用的那個 service provider,此時它還沒被建立),解決問題的核心就在這個類裡面
            instance ??= ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);
            context.Properties[_startupKey] = instance;
    
            // Startup.ConfigureServices
            var configureServicesBuilder = StartupLoader.FindConfigureServicesDelegate(startupType, context.HostingEnvironment.EnvironmentName);
            var configureServices = configureServicesBuilder.Build(instance);
    
            // 呼叫 Startup.ConfigureServices
            configureServices(services);
    
            // 將 Startup.ConfigureContainer 新增到 IHostBuilder.ConfigureContainer 中
            // 這個方法熟悉嗎?你在使用 Autofac 的時候是不是會有一個這個方法?
            var configureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName);
            if (configureContainerBuilder.MethodInfo != null)
            {
                var containerType = configureContainerBuilder.GetContainerType();
                _builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder;
    
                var actionType = typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType);
    
                var configureCallback = typeof(GenericWebHostBuilder).GetMethod(nameof(ConfigureContainerImpl), BindingFlags.NonPublic | BindingFlags.Instance)
                                                 .MakeGenericMethod(containerType)
                                                 .CreateDelegate(actionType, this);
    
                // _builder.ConfigureContainer<T>(ConfigureContainer);
                typeof(IHostBuilder).GetMethod(nameof(IHostBuilder.ConfigureContainer))
                    .MakeGenericMethod(containerType)
                    .InvokeWithoutWrappingExceptions(_builder, new object[] { configureCallback });
            }
    
            // 注意,當執行完 ConfigureServices 和 ConfigureContainer 方法後,
            // 會將 Configure 方法解析出來
            configureBuilder = StartupLoader.FindConfigureDelegate(startupType, context.HostingEnvironment.EnvironmentName);
        }
        catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
        {
            startupError = ExceptionDispatchInfo.Capture(ex);
        }
    
        // Startup.Configure
        services.Configure<GenericWebHostServiceOptions>(options =>
        {
            options.ConfigureApplication = app =>
            {
                // Throw if there was any errors initializing startup
                startupError?.Throw();
    
                // 執行 Startup.Configure
                // 這下,你明白為什麼之前可以通過 Options.ConfigureApplication 獲取到 Startup.Configure 了吧?
                if (instance != null && configureBuilder != null)
                {
                    configureBuilder.Build(instance)(app);
                }
            };
        });
    }
    
    private class HostServiceProvider : IServiceProvider
    {
        private readonly WebHostBuilderContext _context;

        public HostServiceProvider(WebHostBuilderContext context)
        {
            _context = context;
        }

        // 該 ServieceProvider 中,僅提供了 IConfiguration、IHostEnvironment、IWebHostEnvironment 三種服務
        // 所以,在Startup的建構函式中,只能注入這三種服務
        public object GetService(Type serviceType)
        {
            // 很顯然,IWebHostEnvironment 和 IHostEnvironment 返回的都是同一例項
            if (serviceType == typeof(Microsoft.Extensions.Hosting.IHostingEnvironment)
                || serviceType == typeof(Microsoft.AspNetCore.Hosting.IHostingEnvironment)
                || serviceType == typeof(IWebHostEnvironment)
                || serviceType == typeof(IHostEnvironment)
                )
            {
                return _context.HostingEnvironment;
            }

            if (serviceType == typeof(IConfiguration))
            {
                return _context.Configuration;
            }

            return null;
        }
    }
}

還有一個要點是:Startup構造方法中注入的IHostEnvironment和在Startup.Configure等方法中通過常規 Service Provider 解析出來的IHostEnvironment例項是不同的。 原因就是Startup構造方法中的依賴注入 Service Provider 和後面我們用的不是同一個,它們解析的服務例項也不是同一個。

配置

ConfigureHostConfiguration—主機配置

我們可以在HostBuilder.ConfigureHostConfiguration方法中新增主機配置,多次呼叫該方法也沒關係,最終會將這些配置聚合起來

.ConfigureHostConfiguration(config =>
{
    config.SetBasePath(Directory.GetCurrentDirectory());
    config.AddEnvironmentVariables("MYAPPENVPREFIX_");
})

我們可以通過IHostEnvironment服務實現的屬性來獲取部分主機配置。

還可以在HostBuilder.ConfigureAppConfiguration方法中呼叫HostBuilderContext.Configuration來獲取主機配置。在執行完ConfigureAppConfiguration中的委託之後,在其他委託中通過HostBuilderContext.Configuration獲取的就不再針對主機的配置了,而是針對應用的配置。

ConfigureAppConfiguration—應用配置

通過HostBuilder.ConfigureAppConfiguration方法,可以新增應用配置。同樣的,該方法也可以多次進行呼叫,最終會對配置進行聚合。

.ConfigureAppConfiguration((hostingContext, config) =>
{
    // 獲取主機配置
    var hostingConfig = hostingContext.Configuration;

    var env = hostingContext.HostingEnvironment;

    config.AddJsonFile("mysettings.json", optional: true, reloadOnChange: true)
          .AddJsonFile($"mysettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
})

結語

  • 一些常用的配置項解釋可以訪問官方文件

  • 由於預設只能在Development環境時才會啟用範圍驗證(scope validation)和依賴關係驗證(dependency validation),所以,如果想要手動進行配置,可以通過UseDefaultServiceProvider(其實預設邏輯的原始碼裡面也是使用的該擴充套件方法)

.UseDefaultServiceProvider((context, options) =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;
});

相信你讀完本篇文章,一定對ASP.NET Core主機的啟動流程,有了新的認識!

相關文章