asp.net core 源码每日读 -- WebHostBuilder

开篇

之前写过Asp.net core 中的Startup类是啥?和dotnet core 打造干净的Web API服务. 相信读过的朋友可以对asp.net core的启动机制有一定的理解. 可我们终究不能满足于此, 对于一些没有充足时间通读源码却对源码有深深渴望的朋友, 我希望通过这个系列帮助大家用很短的时间对asp.net core的源码有较为深入的理解.

浏览指南

作为这个系列的开篇, 希望为此系列定个调调, 也方便您浏览和查阅. Asp.net core 的源码地址为https://github.com/aspnet 如果您有兴趣可以自行查阅.
身为一个老码农, 我习惯读代码的方式是先对代码整体有一个概览, 根据各个模块和类的命名对程序结构有一个大体了解. 之后从程序入口开始, 层层推进(哪里不懂读哪里) . 最后如果实在理解有困难就采取各种方式Debug.
另外, asp.net core的源码我还没有通读, 只是比较肤浅的有一些理解, 也是通过写文章, 促使自己更加深入的了解这个框架(每天让游戏消磨我宝贵的时间深感罪恶, 我要洗心革面重新做人). 利人利己, 这个事我做!

提醒一点, 这个系列我自己也不清楚能不能完成, 读到哪写到哪, 写到哪算到哪, 如果给您带跑偏概不负责啊.

asp.net core 源码每日读 -- WebHostBuilder_第1张图片

从WebHostBuilder入手

当你新建一个asp.net core的工程, 项目模板为您生成的代码结构大概是这样的

asp.net core 源码每日读 -- WebHostBuilder_第2张图片

很俗套的MVC. 你会发现Program.cs文件中有这样一段代码

    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup()
            .UseApplicationInsights()
            .UseUrls("http://*:5000")
            .Build();

        host.Run();
    }

对的, 就是程序入口的Main函数, 我们就从这里看起.
这段代码的意图很明显, 就是要生成的Http Server的宿主. WebHostBuilder负责建造这个宿主. 它被包含于Hosting这个工程中, 我们根据这个链接可以得到相关的源码.

打开源码, 我们看到WebHostBuilder继承于IWebHostBuilder接口, IWebHostBuilder的结构是这样的

asp.net core 源码每日读 -- WebHostBuilder_第3张图片

能够看出Build函数是最终生成宿主的方法, 除了Build函数, 大部分的函数都返回IWebHostBuilder, 也就是返回它本身, 这也意味着这些函数是在对将要生成的宿主进行配置. 我们转到具体的实现逻辑来看看配置项都有哪些.

    public IWebHostBuilder UseSetting(string key, string value)
    {
        _config[key] = value;
        return this;
    }

先来看这个UseSetting, 代码很简单, 而且根据命名可以知道这个函数就是进行配置的, 其中的_config是IConfiguration类型, 是一个配置项的集合. 我们可以在WebHostBuilder的构造函数中找到_config的实例化代码


其中AddEnvironmentVariables函数的意图是将环境变量中的配置加入到这个服务的配置中来.

    public IWebHostBuilder UseLoggerFactory(ILoggerFactory loggerFactory)
    {
        if (loggerFactory == null)
        {
            throw new ArgumentNullException(nameof(loggerFactory));
        }

        _createLoggerFactoryDelegate = _ => loggerFactory;
        return this;
    }

这个函数很好理解, 用于配置一个Logger工厂的实例.

    public IWebHostBuilder ConfigureServices(Action configureServices)
    {
        if (configureServices == null)
        {
            throw new ArgumentNullException(nameof(configureServices));
        }

        _configureServicesDelegates.Add(configureServices);
        return this;
    }

这个ConfigureServices函数看代码的意思是把一个匿名函数加入到一个匿名函数集合, 也就是_configureServicesDelegates中, 那么问题来了, 这个匿名函数是干啥的? 我们带着问题继续往下看.

    public IWebHostBuilder ConfigureLogging(Action configureLogging)
    {
        if (configureLogging == null)
        {
            throw new ArgumentNullException(nameof(configureLogging));
        }

        _configureLoggingDelegates.Add(configureLogging);
        return this;
    }

这个函数也是把一个匿名函数加入到一个匿名函数的集合_configureLoggingDelegates, 类似的函数还有ConfigureConfiguration(里面有一个集合_configureConfigurationBuilderDelegates), 我们后面看看它的用途.
最后, 最重要的Build函数是我们也要重点了解的, 它里面定义了具体的实现逻辑, 代码比较长, 我们分步来看

        if (_webHostBuilt)
        {
            throw new InvalidOperationException(Resources.WebHostBuilder_SingleInstance);
        }
        _webHostBuilt = true;

这段很好理解, 对于一个Builder有一个宿主实例. 后面这几句则是Build函数中的核心逻辑, 也是WebHostBuilder这个类的核心了.

        var hostingServices = BuildCommonServices(out var hostingStartupErrors);
        var applicationServices = hostingServices.Clone();
        var hostingServiceProvider = hostingServices.BuildServiceProvider();

        AddApplicationServices(applicationServices, hostingServiceProvider);

先来看这个函数BuildCommonServices, 这是一个WebHostBuilder类的私有函数, 从命名我们可以看出, 它是要把所有Common的Services配置到将来的宿主中, 我们来看看实现

        _options = new WebHostOptions(_config);

这句把之前配置好的_config对象转换为了WebHostOptions对象, 万变不离其宗, 它还是个配置表. 后面一段代码还是跟配置相关, 包括环境变量, 根目录, 应用名等等.

        var appEnvironment = PlatformServices.Default.Application;
        var contentRootPath = ResolveContentRootPath(_options.ContentRootPath, appEnvironment.ApplicationBasePath);
        var applicationName = _options.ApplicationName ?? appEnvironment.ApplicationName;
        var hostingContext = new WebHostBuilderContext
        {
            Configuration = _config
        };

后面两句初始化了_hostingEnvironment对象并把它指给hostingContext对象

        // Initialize the hosting environment
        _hostingEnvironment.Initialize(applicationName, contentRootPath, _options);
        hostingContext.HostingEnvironment = _hostingEnvironment;

_hostingEnvironment和hostingContext对象在后面也都有用到.

        var services = new ServiceCollection();
        services.AddSingleton(_hostingEnvironment);

上面两句代码定义了一个依赖注入的服务容器, 并把_hostingEnvironment对象作为一个服务注册到容器中. 用于在其他地方访问环境的一些信息(包括应用名, 环境名, 根路径等).

        var builder = new ConfigurationBuilder()
            .SetBasePath(_hostingEnvironment.ContentRootPath)
            .AddInMemoryCollection(_config.AsEnumerable());

        foreach (var configureConfiguration in _configureConfigurationBuilderDelegates)
        {
            configureConfiguration(hostingContext, builder);
        }

        var configuration = builder.Build();
        services.AddSingleton(configuration);
        hostingContext.Configuration = configuration;

这段代码主要生成了configuration对象, 并把它加入到依赖注入的容器中. 其中用到了_configureConfigurationBuilderDelegates这个匿名函数集合的对象. 一个foreach把_configureConfigurationBuilderDelegates中的所有匿名函数执行了一遍, 使用hostingContext和builder作为参数. 意图很明显, 用户自定义的一些作用于hostingContext和builder的匿名函数在这里被执行了, 起到了应有的作用. 说白了就是为了修改,删除或新增了某些配置项.

        // The configured ILoggerFactory is added as a singleton here. AddLogging below will not add an additional one.
        var loggerFactory = _createLoggerFactoryDelegate?.Invoke(hostingContext) ?? new LoggerFactory(configuration.GetSection("Logging"));
        services.AddSingleton(loggerFactory);
        hostingContext.LoggerFactory = loggerFactory;

上面这段创建了一个loggerFactory实例, 并把它加入到依赖注入的容器中.

        var exceptions = new List();

        // Execute the hosting startup assemblies
        foreach (var assemblyName in _options.HostingStartupAssemblies)
        {
            try
            {
                var assembly = Assembly.Load(new AssemblyName(assemblyName));

                foreach (var attribute in assembly.GetCustomAttributes())
                {
                    var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType);
                    hostingStartup.Configure(this);
                }
            }
            catch (Exception ex)
            {
                // Capture any errors that happen during startup
                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);

            // Throw directly if we're not capturing startup errors
            if (!_options.CaptureStartupErrors)
            {
                throw hostingStartupErrors;
            }
        }

这段说明用户可以在配置文件中配置启动程序集对宿主进行一定的配置, 没有特别需要的话这块估计很少会用到.

         // Kept for back-compat, will remove once ConfigureLogging is removed.
        foreach (var configureLogging in _configureLoggingDelegates)
        {
            configureLogging(loggerFactory);
        }

这段明显就是通过一些自定义的匿名函数对Logger工厂进行配置. 具体的大家实践一下吧, 后面计划单独一篇讲讲Logger的机制.

        // Kept for back-compat, will remove once ConfigureLogging is removed.
        foreach (var configureLogging in _configureLoggingDelegates)
        {
            configureLogging(loggerFactory);
        }

        //This is required to add ILogger of T.
        services.AddLogging();

        var listener = new DiagnosticListener("Microsoft.AspNetCore");
        services.AddSingleton(listener);
        services.AddSingleton(listener);

        services.AddTransient();
        services.AddTransient();
        services.AddScoped();
        services.AddOptions();

        // Conjure up a RequestServices
        services.AddTransient();
        services.AddTransient, DefaultServiceProviderFactory>();

        // Ensure object pooling is available everywhere.
        services.AddSingleton();

        if (!string.IsNullOrEmpty(_options.StartupAssembly))
        {
            try
            {
                var startupType = StartupLoader.FindStartupType(_options.StartupAssembly, _hostingEnvironment.EnvironmentName);

                if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
                {
                    services.AddSingleton(typeof(IStartup), startupType);
                }
                else
                {
                    services.AddSingleton(typeof(IStartup), sp =>
                    {
                        var hostingEnvironment = sp.GetRequiredService();
                        var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
                        return new ConventionBasedStartup(methods);
                    });
                }
            }
            catch (Exception ex)
            {
                var capture = ExceptionDispatchInfo.Capture(ex);
                services.AddSingleton(_ =>
                {
                    capture.Throw();
                    return null;
                });
            }
        }

这一段又注入了很多服务, 不再一一细讲, 后面用到的时候我们再回过头来看. 其中有一点比较特殊的是_options.StartupAssembly, 说明对于Startup也可以在配置文件中进行配置, 则系统回自动反射为对象, 并把此对象作为一个依赖注入的服务.

        foreach (var configureServices in _configureServicesDelegates)
        {
            configureServices(services);
        }
        return services;

这段能回答上面的问题了, _configureServicesDelegates用户保存一些匿名函数, 这些匿名函数将在Build宿主的时候被执行, 并且这些函数有一个共同的参数类型IServiceCollection, 很显然, 这是一个依赖注入的对外暴露的接口. 最后把services这个对象, 这个依赖注入的容器返回.
我们回到Build函数, 后面的操作很简单, 将返回的services对象克隆一份赋值给applicationServices, 并调用了AddApplicationServices函数.

    private void AddApplicationServices(IServiceCollection services, IServiceProvider hostingServiceProvider)
    {
        var loggerFactory = hostingServiceProvider.GetService();
        services.Replace(ServiceDescriptor.Singleton(typeof(ILoggerFactory), loggerFactory));

        var listener = hostingServiceProvider.GetService();
        services.Replace(ServiceDescriptor.Singleton(typeof(DiagnosticListener), listener));
        services.Replace(ServiceDescriptor.Singleton(typeof(DiagnosticSource), listener));
    }

这个AddApplicationServices函数的逻辑也很简单, 将几个服务的生命周期转换为单例模式.

        var host = new WebHost(
            applicationServices,
            hostingServiceProvider,
            _options,
            _config,
            hostingStartupErrors);

        host.Initialize();

        return host;

最终用上面得出的几个对象创建了一个WebHost对象.

总结

总的看来WebHostBuilder做了两件事:
1. 定义了一些WebHost的配置项
2. 创建依赖注入的容器, 并注入一些service

作为开篇, 川酷尽量做到严谨和详细, 如果有不明白的地方欢迎讨论. 也欢迎您的批评指正. 下一篇计划写一篇关于WebHost的内容, 谢谢您的关注!

--------------------------华丽丽的分割-----------------------------------

愿dotnet社区日益强大, 愿dotnet生态日趋完善. dotnet core技术交流群欢迎你, 扫描下面二维码进群.

你可能感兴趣的:(asp.net core 源码每日读 -- WebHostBuilder)