理解ASP.NET Core - 主機(Host)


注:本文隸屬於《理解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主機的啟動流程,有了新的認識!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM