Asp.Net Core启动流程

起因

昨天修改博客程序,发现有部分代码是Asp.Net Core 2.x的用法,博客程序从Mono+Nancy时期到Asp.Net Core,只是简单调整了代码.早期为了调试方便,使用不少黑科技.比如预处理.

在改为Asp.Net Core 3.x的时候,还是没有调整启动相关的代码.后来使用Jexus代理.有问题不好调试,便修改默认的启动代码,改为从配置文件读取端口.正式这一块的代码才想起来看Asp.Net Core启动流程相关的代码.

在C#中使用预处理,不过这种方式还是少使用

#if RELEASE
            //正式环境
            IWebHostBuilder hostBuilder = null;
            hostBuilder = new WebHostBuilder()
                .UseKestrel()
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    //读取配置文件
                    var env = hostingContext.HostingEnvironment;
                    config.AddJsonFile("appsettings.json", true)
                          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true);
                })
                .ConfigureKestrel((context, options) =>
                {
                    //读取配置文件端口
                    var basePort = context.Configuration.GetValue<int?>("BASE_PORT") ?? 5000;
                    options.Listen(IPAddress.Any, basePort, listenOptions =>
                    {
                        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
                    });
                })
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseStartup<Startup>();
            hostBuilder.Build().Run();
#elif DEBUG
            //本地环境,方便调试
            var hostBuilder = CreateHostBuilder(args);
            hostBuilder.Build().Run();
#endif

Asp.Net Core和Asp.Net宿主区别

在Asp.Net时代,宿主在Windows上首选便是IIS,在Linux上使用Jexus,当然还有XSP,这个没有用过.

换到Asp.Net Core自带的宿主便是Kestrel,所以在流程便改为:
xxx.exe运行→Kestrel→Startup→中间件→MVC/Web API
在创建项目后看不到Kestrel相关的代码,是因为又封装了,早期是可以看到的.在上方代码中是可以调用UseKestrel,这是配置Kestrel,紧接着ConfigureKestrel配置Kestrel监听端口,如果不指定端口,会默认使用5000(http)和5001(https).我们项目用IIS/Jexus代理后,其实没有必要使用5001端口,或者开启https的.Kestrel只需要做一个安静处理请求的美男子就好了.
我们在看一下默认生成的代码并看一下代码简易流程图:
public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

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

Asp.Net Core 启动过程

下面我们去找相关实现的代码:

Host.CreateHostBuilder代码:

//代码没有在Asp.Net Core项目中, 而是在Microsoft.Extensions.Hosting(在Dotnet/Runtime/libraries //github地址:https://github.com/dotnet/runtime/Microsoft.Extensions.Hosting)中//剔除一些代码注释
namespace Microsoft.Extensions.Hosting
{
    public static class Host
    {
        public static IHostBuilder CreateDefaultBuilder() =>
            CreateDefaultBuilder(args: null);

        public static IHostBuilder CreateDefaultBuilder(string[] args)
        {
            //创建HostBuilder
            var builder = new HostBuilder();

            //获取当前程序所在目录
            builder.UseContentRoot(Directory.GetCurrentDirectory());

            //配置host
            builder.ConfigureHostConfiguration(config =>
            {
                config.AddEnvironmentVariables(prefix: "DOTNET_");
                if (args != null)
                {
                    config.AddCommandLine(args);
                }
            });

            //获取程序所需的配置,如appsettings.json
            builder.ConfigureAppConfiguration((hostingContext, config) =>
            {
                var env = hostingContext.HostingEnvironment;

                var reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);

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

                if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
                {
                    var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                    if (appAssembly != null)
                    {
                        config.AddUserSecrets(appAssembly, optional: true);
                    }
                }

                config.AddEnvironmentVariables();

                if (args != null)
                {                    //获取命令行启动参数
                    config.AddCommandLine(args);
                }
            })

            //配置日志相关的,在外部调用ConfigureLogging,会将内部的配置覆盖
            .ConfigureLogging((hostingContext, logging) =>
            {
                var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

            // IMPORTANT: This needs to be added *before* configuration is loaded, this lets
            // the defaults be overridden by the configuration.
            if (isWindows)
                {
                // Default the EventLogLoggerProvider to warning or above
                logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
                }

                logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                logging.AddConsole();
                logging.AddDebug();
                logging.AddEventSourceLogger();

                if (isWindows)
                {
                // Add the EventLogLoggerProvider on windows machines
                logging.AddEventLog();
                }
            })
            //使用默认的服务,初步怀疑是容器相关的,内部实例化ServiceProviderOptions和DefaultServiceProviderFactory(serviceProviderOptions)            //在CreateServiceProvider内部调用BuildServiceProvider(IServiceCollection扩展方法),            .UseDefaultServiceProvider((context, options) =>
            {
                var isDevelopment = context.HostingEnvironment.IsDevelopment();
                options.ValidateScopes = isDevelopment;
                options.ValidateOnBuild = isDevelopment;
            });

            return builder;
        }
    }
}

ConfigureWebHostDefaults源码:

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

ConfigureWebHost代码:

public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
    var webhostBuilder = new GenericWebHostBuilder(builder);  //GenericWebHostBuilder实现IWebHostBuilder
    configure(webhostBuilder);    //在GenericWebHostService注册到容器中,在实例化的时候会实例化KestrelServer
    builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());  
    return builder;
}

WebHost.ConfigureWebDefaults代码:

internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
    builder.ConfigureAppConfiguration((ctx, cb) =>
    {
        if (ctx.HostingEnvironment.IsDevelopment())
        {
            StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
        }
    });
    //终于找到你了,在UseKestrel内部把SocketTransportFactory/KestrelServerOptionsSetup/KestrelServer添加到容器中
    builder.UseKestrel((builderContext, options) =>
    {
        options.Configure(builderContext.Configuration.GetSection("Kestrel"));
    })
    .ConfigureServices((hostingContext, services) =>
    {
        // Fallback
        services.PostConfigure<HostFilteringOptions>(options =>
        {
            if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
            {
                // "AllowedHosts": "localhost;127.0.0.1;[::1]"
                var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                // Fall back to "*" to disable.
                options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
            }
        });
        // Change notification
        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;
                // Only loopback proxies are allowed by default. Clear that restriction because forwarders are
                // being enabled by explicit configuration.
                options.KnownNetworks.Clear();
                options.KnownProxies.Clear();
            });

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

        services.AddRouting();
    })
    .UseIIS()               // IIS相关的中间件
    .UseIISIntegration();   // 与IIS集成的中间件}

接下来我们Build方法中都有什么.

public IHost Build()
{
    if (_hostBuilt)
    {
        throw new InvalidOperationException("Build can only be called once.");
    }
    _hostBuilt = true;

    BuildHostConfiguration();  
    CreateHostingEnvironment();
    CreateHostBuilderContext();
    BuildAppConfiguration();
    CreateServiceProvider();  //这里创建了 di的容器

    return _appServices.GetRequiredService<IHost>(); //从di的容器获取Host实例,在Run函数内部调用
}
private void BuildHostConfiguration()
{
    var configBuilder = new ConfigurationBuilder()
        .AddInMemoryCollection(); // Make sure there's some default storage since there are no default providers

    foreach (var buildAction in _configureHostConfigActions)
    {
        buildAction(configBuilder);
    }
    _hostConfiguration = configBuilder.Build();
}

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))
    {
        // Note GetEntryAssembly returns null for the net4x console test runner.
        _hostingEnvironment.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name;
    }

    _hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(_hostingEnvironment.ContentRootPath);
}

private string ResolveContentRootPath(string contentRootPath, string basePath)
{
    if (string.IsNullOrEmpty(contentRootPath))
    {
        return basePath;
    }
    if (Path.IsPathRooted(contentRootPath))
    {
        return contentRootPath;
    }
    return Path.Combine(Path.GetFullPath(basePath), contentRootPath);
}

private void CreateHostBuilderContext()
{
    _hostBuilderContext = new HostBuilderContext(Properties)
    {
        HostingEnvironment = _hostingEnvironment,
        Configuration = _hostConfiguration
    };
}

private void BuildAppConfiguration()
{
    var configBuilder = new ConfigurationBuilder()
        .SetBasePath(_hostingEnvironment.ContentRootPath)
        .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);

    foreach (var buildAction in _configureAppConfigActions)
    {
        buildAction(_hostBuilderContext, configBuilder);
    }
    _appConfiguration = configBuilder.Build();
    _hostBuilderContext.Configuration = _appConfiguration;
}

private void CreateServiceProvider()
{    // Asp.Net Core 自带的容器在这里初始化 new ServiceCollection
    var services = new ServiceCollection();
#pragma warning disable CS0618 // Type or member is obsolete
    services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
#pragma warning restore CS0618 // Type or member is obsolete
    services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
    services.AddSingleton(_hostBuilderContext);
    // register configuration as factory to make it dispose with the service provider
    services.AddSingleton(_ => _appConfiguration);
#pragma warning disable CS0618 // Type or member is obsolete
    services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
    services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
    services.AddSingleton<IHostLifetime, ConsoleLifetime>();
    services.AddSingleton<IHost, Internal.Host>();   // 在这里将Host添加到容器 在Main方法Build函数调用返回的,会获取容器中获取Host实例
    services.AddOptions();
    services.AddLogging();

    foreach (var configureServicesAction in _configureServicesActions)
    {
        configureServicesAction(_hostBuilderContext, services);
    }

    var containerBuilder = _serviceProviderFactory.CreateBuilder(services);

    foreach (var containerAction in _configureContainerActions)
    {
        containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
    }

    _appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);

    if (_appServices == null)
    {
        throw new InvalidOperationException($"The IServiceProviderFactory returned a null IServiceProvider.");
    }

    // resolve configuration explicitly once to mark it as resolved within the
    // service provider, ensuring it will be properly disposed with the provider
    _ = _appServices.GetService<IConfiguration>();
}

最后看一下Run内部都有什么黑科技.

Run函数在HostingAbstractionsHostExtensions.cs文件中,可以看到又是通过扩展方法往Host对象添加函数.

/// <summary>
/// Runs an application and block the calling thread until host shutdown.
/// </summary>
/// <param name="host">The <see cref="IHost"/> to run.</param>
public static void Run(this IHost host)
{
    host.RunAsync().GetAwaiter().GetResult();
}

/// <summary>
/// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered.
/// </summary>
/// <param name="host">The <see cref="IHost"/> to run.</param>
/// <param name="token">The token to trigger shutdown.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public static async Task RunAsync(this IHost host, CancellationToken token = default)
{
    try
    {
        await host.StartAsync(token);

        await host.WaitForShutdownAsync(token);
    }
    finally
    {
        if (host is IAsyncDisposable asyncDisposable)
        {
            await asyncDisposable.DisposeAsync();
        }
        else
        {
            host.Dispose();
        }

    }
}

/// <summary>
/// Returns a Task that completes when shutdown is triggered via the given token.
/// </summary>
/// <param name="host">The running <see cref="IHost"/>.</param>
/// <param name="token">The token to trigger shutdown.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default)
{
    var 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);

    await waitForStop.Task;

    // Host will use its default ShutdownTimeout if none is specified.
    await host.StopAsync();
}

Host.cs文件代码:

internal class Host : IHost, IAsyncDisposable
{
    private readonly ILogger<Host> _logger;
    private readonly IHostLifetime _hostLifetime;
    private readonly ApplicationLifetime _applicationLifetime;
    private readonly HostOptions _options;
    private IEnumerable<IHostedService> _hostedServices;

    public Host(IServiceProvider services, IHostApplicationLifetime applicationLifetime, ILogger<Host> logger,
        IHostLifetime hostLifetime, IOptions<HostOptions> options)
    {
        Services = services ?? throw new ArgumentNullException(nameof(services));
        _applicationLifetime = (applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime))) as ApplicationLifetime;
        if (_applicationLifetime is null)
        {
            throw new ArgumentException("Replacing IHostApplicationLifetime is not supported.", nameof(applicationLifetime));
        }
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _hostLifetime = hostLifetime ?? throw new ArgumentNullException(nameof(hostLifetime));
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
    }

    public IServiceProvider Services { get; }

    public async Task StartAsync(CancellationToken cancellationToken = default)
    {
        _logger.Starting();

        using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
        var combinedCancellationToken = combinedCancellationTokenSource.Token;

        await _hostLifetime.WaitForStartAsync(combinedCancellationToken);

        combinedCancellationToken.ThrowIfCancellationRequested();
        _hostedServices = Services.GetService<IEnumerable<IHostedService>>(); // 获取GenericWebHostService

        foreach (var hostedService in _hostedServices)
        {
            // 
            await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
        }

        // Fire IHostApplicationLifetime.Started
        _applicationLifetime.NotifyStarted();

        _logger.Started();
    }

    public async Task StopAsync(CancellationToken cancellationToken = default)
    {
        _logger.Stopping();

        using (var cts = new CancellationTokenSource(_options.ShutdownTimeout))
        using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
        {
            var token = linkedCts.Token;
            // Trigger IHostApplicationLifetime.ApplicationStopping
            _applicationLifetime.StopApplication();

            IList<Exception> exceptions = new List<Exception>();
            if (_hostedServices != null) // Started?
            {
                foreach (var hostedService in _hostedServices.Reverse())
                {
                    token.ThrowIfCancellationRequested();
                    try
                    {
                        await hostedService.StopAsync(token).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        exceptions.Add(ex);
                    }
                }
            }

            token.ThrowIfCancellationRequested();
            await _hostLifetime.StopAsync(token);

            // Fire IHostApplicationLifetime.Stopped
            _applicationLifetime.NotifyStopped();

            if (exceptions.Count > 0)
            {
                var ex = new AggregateException("One or more hosted services failed to stop.", exceptions);
                _logger.StoppedWithException(ex);
                throw ex;
            }
        }

        _logger.Stopped();
    }

    public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();

    public async ValueTask DisposeAsync()
    {
        switch (Services)
        {
            case IAsyncDisposable asyncDisposable:
                await asyncDisposable.DisposeAsync();
                break;
            case IDisposable disposable:
                disposable.Dispose();
                break;
        }
    }
}

先说Kestrel,UseKestrel中间件先往容器注册,在ConfigureWebHost中往容器注册GenericWebHostService,在Host中的RunAsync从容器中获取GenericWebHostService,调用StartAsync

秋风 2020-08-24