ASP.NET Core框架探索 主机搭建与运行(附有思维导图)

前言

我们来结合源码来探究一下ASP.NET Core Web框架的运行原理。

可以先整体看一下下面这张基于源码分析过程的一个总结大纲

包含各环节完成的关键步骤:.

ASP.NET Core框架探索 主机搭建与运行(附有思维导图)

下面我们将一起来结合源码探索启动一个ASP.NET CORE的Web项目时框架是怎么运行起来的,以及各个环节框架底层的源码大致做了哪些事情!

一、初始化与框架配置

首先我们聚焦于Host.CreateDefaultBuilder

public static IHostBuilder CreateDefaultBuilder(string[] args)
{
    //使用默认的配置初始化一个HostBuilder对象
    var builder = new HostBuilder();

    builder.UseContentRoot(Directory.GetCurrentDirectory());
    builder.ConfigureHostConfiguration(config =>
    {
        config.AddEnvironmentVariables(prefix: "DOTNET_");
        if (args != null)
        {
            config.AddCommandLine(args);
        }
    });

    builder.ConfigureAppConfiguration((hostingContext, config) =>
    {
        var env = hostingContext.HostingEnvironment;

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

        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((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();
        }
    })
    .UseDefaultServiceProvider((context, options) =>
    {
        var isDevelopment = context.HostingEnvironment.IsDevelopment();
        options.ValidateScopes = isDevelopment;
        options.ValidateOnBuild = isDevelopment;
    });

    return builder;
}

该方法首先会创建一个IHostBuilder,并使用一些默认的配置进行初始化:

UseContentRoot 设置主机项目根目录

ConfigureHostConfiguration配置环境变量和命令行参数添加到Configure对象,便于程序在以后的运行中可以从Configure对象获取到来源于环境变量和命令行的参数

ConfigureAppConfiguration设置对配置文件和用户机密文件的加载,并且可以再次使用该方法来定义App应用的配置信息,对于Configure的讲解可查看我的这篇文章《浅析.netcore中的Configuration》

ConfigureLogging用来配置系统的日志

UseDefaultServiceProvider用来配置框架使用默认的IOC容器

然后程序来到了ConfigureWebHostDefaults方法

程序会先使用GenericWebHostBuilder 创建和初始化一个IWebhostBuilder 对象,然后调用WebHost.ConfigureWebDefaults方法,在方法中通过调用UseKestrel告诉框架使用Kestrel作为web服务器用于接受请求,完成Kestrel服务器的相关配置。

ASP.NET Core框架探索 主机搭建与运行(附有思维导图)

接着程序会调用webBuilder.UseStartup< Startup >(),该方法会找出Startup中的ConfigureServicesConfigureContainerConfigure方法,将其方法的实际执行动作调用IHostBuilder中的ConfigureServices方法以委托的形式保存起来。下面为UseStartUp执行的部分源代码

public IWebHostBuilder UseStartup(Type startupType)
{
    // UseStartup can be called multiple times. Only run the last one.
    _builder.Properties["UseStartup.StartupType"] = startupType;
    _builder.ConfigureServices((context, services) =>
    {
        if (_builder.Properties.TryGetValue("UseStartup.StartupType", out var cachedType) && (Type)cachedType == startupType)
        {
            UseStartup(startupType, context, services);
        }
    });

    return this;
}

private void UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
{
    var webHostBuilderContext = GetWebHostBuilderContext(context);
    var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];

    ...
    try
    {
        //1.完成对StartUp类的合法性进行校验
        // We cannot support methods that return IServiceProvider as that is terminal and we need ConfigureServices to compose
        if (typeof(IStartup).IsAssignableFrom(startupType))
        {
            throw new NotSupportedException($"{typeof(IStartup)} isn't supported");
        }
        if (StartupLoader.HasConfigureServicesIServiceProviderDelegate(startupType, context.HostingEnvironment.EnvironmentName))
        {
            throw new NotSupportedException($"ConfigureServices returning an {typeof(IServiceProvider)} isn't supported.");
        }

        instance = ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);
        context.Properties[_startupKey] = instance;

        //2.查找并校验StartUp类中的ConfigureServices方法
        // Startup.ConfigureServices
        var configureServicesBuilder = StartupLoader.FindConfigureServicesDelegate(startupType, context.HostingEnvironment.EnvironmentName);
        var configureServices = configureServicesBuilder.Build(instance);

        //3.执行StartUp类中的ConfigureServices
        configureServices(services);

        //4.查找并校验StartUp类中的ConfigureContainer方法
        // REVIEW: We're doing this in the callback so that we have access to the hosting environment
        // Startup.ConfigureContainer
        var configureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName);
        ...

        //5.查找并校验StartUp类中的Configure方法
        // Resolve Configure after calling ConfigureServices and ConfigureContainer
        configureBuilder = StartupLoader.FindConfigureDelegate(startupType, context.HostingEnvironment.EnvironmentName);
    }
    catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
    {
        startupError = ExceptionDispatchInfo.Capture(ex);
    }

    ...
}

有些同学可能已经发现,我们可以使用UseStartup的时候可以不指定具体的StartUp类,而是指定一个程序集便可实现通过定义不同的环境去加载不同的StartUp类。如下图我们定义了多个StartUp类

ASP.NET Core框架探索 主机搭建与运行(附有思维导图)

然后将webBuilder.UseStartup< Startup >()改为webBuilder.UseStartup(typeof(Startup).GetTypeInfo().Assembly.FullName),通过设置不同的环境配置项便可执行对应的StartUp

ASP.NET Core框架探索 主机搭建与运行(附有思维导图)

这个又是怎么实现的呢?这便要说起前面我们使用GenericWebHostBuilder构造IWebHostBuilder对象的时候,构造函数中会拿到传递到程序集字符串集合当前的主机环境找到对应的StartUp类,最后的执行与直接使用webBuilder.UseStartup< Startup >()最后调用的逻辑是一样的。所以看到源码之后,就会对于这样的写法和用途醍醐灌顶。

public GenericWebHostBuilder(IHostBuilder builder)
{
    _builder = builder;

    ...

    _builder.ConfigureServices((context, services) =>
    {
        var webhostContext = GetWebHostBuilderContext(context);
        var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];

        ...

        // webHostOptions.StartupAssembly拿到的就是UseStartUp传递的程序集字符串
        // Support UseStartup(assemblyName)
        if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
        {
            try
            {
                //根据Environment找到对应的StartUp类
                var startupType = StartupLoader.FindStartupType(webHostOptions.StartupAssembly, webhostContext.HostingEnvironment.EnvironmentName);
                //webBuilder.UseStartup<Startup>()调用过程中也会执行此方法,实现的作用在上面有进行说明
                UseStartup(startupType, context, services);
            }
            catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
            {
            }
        }
    });
}

二、IHostBuilder.Build()构建装载框架的处理能力

到此为止,前面的动作都是使用默认的配置完成了一些对象的初始化,并且将一些执行动作保存为委托,并没有实际执行,等到来到Build方法,会执行方法内部的三个核心动作。

public IHost Build()
{
    ...
    BuildHostConfiguration();
    ...
    BuildAppConfiguration();
    CreateServiceProvider();

    ...
}

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 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()
{
    var services = new ServiceCollection();
    .... //注册框架需要的服务实例到容器

    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>();
}

BuildHostConfiguration会执行前面调用ConfigureHostConfiguration保存的委托方法完成对Host的配置

BuildAppConfiguration会执行前面调用ConfigureAppConfiguration保存的委托方法完成对App应用的配置

CreateServiceProvider首先会初始化ServiceCollection的容器实例,然后将部分框架需要的实例对象添加到容器中,完成主机环境、上下文、生命周期等相关对象的容器注册,随后会执行前面调用ConfigureServices保存的委托方法,所以StartUp类中的ConfigureServices其实就是在这个环节执行的。

三、IHost.Run()组装请求处理流程并启动服务器监听

最后来到IHost.Run去运行主机

//Run最终会调用该方法
public async Task StartAsync(CancellationToken cancellationToken)
{
    .....

    RequestDelegate application = null;

    try
    {
        //这边是前面拿到的StartUp中的Configure方法
        Action<IApplicationBuilder> configure = Options.ConfigureApplication;

        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);

        ....

        configure(builder);

        // Build the request pipeline
        application = builder.Build();
    }
    catch (Exception ex)
    {
    }

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

    await Server.StartAsync(httpApplication, cancellationToken);
    ...
}

该过程中框架会拿到执行前面查询StartUp找到并保存起来的Configure方法,此方法会定义并添加对请求进行处理的管道中间件以及中间间的处理顺序,然后会通过ApplicationBuilderFactory创建一个IApplicationBuilder对象,使用IApplicationBuilder.build去组装管道处理流程,最后生成一个RequestDelegate,该对象就是我们的处理管道对象。

然后程序会把RequestDelegate进行包装后作为参数传递到Server.StartAsync方法中,Server对象即为Kestrel服务器,调用Server.StartAsync便会启动Kestrel服务器,实现站点对请求的监听,同时通过传递过来的RequestDelegate对象,便把Kestrel与管道中间件连接起来了,从而实现了从请求监听到请求处理的全过程。