asp.net core 源碼每日讀 -- WebHostBuilder

開篇

之前寫過Asp.net core 中的Startup類是啥?dotnet core 打造干凈的Web API服務(wù). 相信讀過的朋友可以對asp.net core的啟動機(jī)制有一定的理解. 可我們終究不能滿足于此, 對于一些沒有充足時間通讀源碼卻對源碼有深深渴望的朋友, 我希望通過這個系列幫助大家用很短的時間對asp.net core的源碼有較為深入的理解.

瀏覽指南

作為這個系列的開篇, 希望為此系列定個調(diào)調(diào), 也方便您瀏覽和查閱. Asp.net core 的源碼地址為https://github.com/aspnet 如果您有興趣可以自行查閱.
身為一個老碼農(nóng), 我習(xí)慣讀代碼的方式是先對代碼整體有一個概覽, 根據(jù)各個模塊和類的命名對程序結(jié)構(gòu)有一個大體了解. 之后從程序入口開始, 層層推進(jìn)(哪里不懂讀哪里) . 最后如果實(shí)在理解有困難就采取各種方式Debug.
另外, asp.net core的源碼我還沒有通讀, 只是比較膚淺的有一些理解, 也是通過寫文章, 促使自己更加深入的了解這個框架(每天讓游戲消磨我寶貴的時間深感罪惡, 我要洗心革面重新做人). 利人利己, 這個事我做!

提醒一點(diǎn), 這個系列我自己也不清楚能不能完成, 讀到哪寫到哪, 寫到哪算到哪, 如果給您帶跑偏概不負(fù)責(zé)啊.

從WebHostBuilder入手

當(dāng)你新建一個asp.net core的工程, 項目模板為您生成的代碼結(jié)構(gòu)大概是這樣的


很俗套的MVC. 你會發(fā)現(xiàn)Program.cs文件中有這樣一段代碼

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

        host.Run();
    }

對的, 就是程序入口的Main函數(shù), 我們就從這里看起.
這段代碼的意圖很明顯, 就是要生成的Http Server的宿主. WebHostBuilder負(fù)責(zé)建造這個宿主. 它被包含于Hosting這個工程中, 我們根據(jù)這個鏈接可以得到相關(guān)的源碼.

打開源碼, 我們看到WebHostBuilder繼承于IWebHostBuilder接口, IWebHostBuilder的結(jié)構(gòu)是這樣的

能夠看出Build函數(shù)是最終生成宿主的方法, 除了Build函數(shù), 大部分的函數(shù)都返回IWebHostBuilder, 也就是返回它本身, 這也意味著這些函數(shù)是在對將要生成的宿主進(jìn)行配置. 我們轉(zhuǎn)到具體的實(shí)現(xiàn)邏輯來看看配置項都有哪些.

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

先來看這個UseSetting, 代碼很簡單, 而且根據(jù)命名可以知道這個函數(shù)就是進(jìn)行配置的, 其中的_config是IConfiguration類型, 是一個配置項的集合. 我們可以在WebHostBuilder的構(gòu)造函數(shù)中找到_config的實(shí)例化代碼


其中AddEnvironmentVariables函數(shù)的意圖是將環(huán)境變量中的配置加入到這個服務(wù)的配置中來.

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

        _createLoggerFactoryDelegate = _ => loggerFactory;
        return this;
    }

這個函數(shù)很好理解, 用于配置一個Logger工廠的實(shí)例.

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

        _configureServicesDelegates.Add(configureServices);
        return this;
    }

這個ConfigureServices函數(shù)看代碼的意思是把一個匿名函數(shù)加入到一個匿名函數(shù)集合, 也就是_configureServicesDelegates中, 那么問題來了, 這個匿名函數(shù)是干啥的? 我們帶著問題繼續(xù)往下看.

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

        _configureLoggingDelegates.Add(configureLogging);
        return this;
    }

這個函數(shù)也是把一個匿名函數(shù)加入到一個匿名函數(shù)的集合_configureLoggingDelegates, 類似的函數(shù)還有ConfigureConfiguration(里面有一個集合_configureConfigurationBuilderDelegates), 我們后面看看它的用途.
最后, 最重要的Build函數(shù)是我們也要重點(diǎn)了解的, 它里面定義了具體的實(shí)現(xiàn)邏輯, 代碼比較長, 我們分步來看

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

這段很好理解, 對于一個Builder有一個宿主實(shí)例. 后面這幾句則是Build函數(shù)中的核心邏輯, 也是WebHostBuilder這個類的核心了.

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

        AddApplicationServices(applicationServices, hostingServiceProvider);

先來看這個函數(shù)BuildCommonServices, 這是一個WebHostBuilder類的私有函數(shù), 從命名我們可以看出, 它是要把所有Common的Services配置到將來的宿主中, 我們來看看實(shí)現(xiàn)

        _options = new WebHostOptions(_config);

這句把之前配置好的_config對象轉(zhuǎn)換為了WebHostOptions對象, 萬變不離其宗, 它還是個配置表. 后面一段代碼還是跟配置相關(guān), 包括環(huán)境變量, 根目錄, 應(yīng)用名等等.

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

上面兩句代碼定義了一個依賴注入的服務(wù)容器, 并把_hostingEnvironment對象作為一個服務(wù)注冊到容器中. 用于在其他地方訪問環(huán)境的一些信息(包括應(yīng)用名, 環(huán)境名, 根路徑等).

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

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

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

這段代碼主要生成了configuration對象, 并把它加入到依賴注入的容器中. 其中用到了_configureConfigurationBuilderDelegates這個匿名函數(shù)集合的對象. 一個foreach把_configureConfigurationBuilderDelegates中的所有匿名函數(shù)執(zhí)行了一遍, 使用hostingContext和builder作為參數(shù). 意圖很明顯, 用戶自定義的一些作用于hostingContext和builder的匿名函數(shù)在這里被執(zhí)行了, 起到了應(yīng)有的作用. 說白了就是為了修改,刪除或新增了某些配置項.

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

上面這段創(chuàng)建了一個loggerFactory實(shí)例, 并把它加入到依賴注入的容器中.

        var exceptions = new List<Exception>();

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

這段說明用戶可以在配置文件中配置啟動程序集對宿主進(jìn)行一定的配置, 沒有特別需要的話這塊估計很少會用到.

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

這段明顯就是通過一些自定義的匿名函數(shù)對Logger工廠進(jìn)行配置. 具體的大家實(shí)踐一下吧, 后面計劃單獨(dú)一篇講講Logger的機(jī)制.

        // 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<DiagnosticListener>(listener);
        services.AddSingleton<DiagnosticSource>(listener);

        services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>();
        services.AddTransient<IHttpContextFactory, HttpContextFactory>();
        services.AddScoped<IMiddlewareFactory, MiddlewareFactory>();
        services.AddOptions();

        // Conjure up a RequestServices
        services.AddTransient<IStartupFilter, AutoRequestServicesStartupFilter>();
        services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>();

        // Ensure object pooling is available everywhere.
        services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

        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<IHostingEnvironment>();
                        var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
                        return new ConventionBasedStartup(methods);
                    });
                }
            }
            catch (Exception ex)
            {
                var capture = ExceptionDispatchInfo.Capture(ex);
                services.AddSingleton<IStartup>(_ =>
                {
                    capture.Throw();
                    return null;
                });
            }
        }

這一段又注入了很多服務(wù), 不再一一細(xì)講, 后面用到的時候我們再回過頭來看. 其中有一點(diǎn)比較特殊的是_options.StartupAssembly, 說明對于Startup也可以在配置文件中進(jìn)行配置, 則系統(tǒng)回自動反射為對象, 并把此對象作為一個依賴注入的服務(wù).

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

這段能回答上面的問題了, _configureServicesDelegates用戶保存一些匿名函數(shù), 這些匿名函數(shù)將在Build宿主的時候被執(zhí)行, 并且這些函數(shù)有一個共同的參數(shù)類型IServiceCollection, 很顯然, 這是一個依賴注入的對外暴露的接口. 最后把services這個對象, 這個依賴注入的容器返回.
我們回到Build函數(shù), 后面的操作很簡單, 將返回的services對象克隆一份賦值給applicationServices, 并調(diào)用了AddApplicationServices函數(shù).

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

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

這個AddApplicationServices函數(shù)的邏輯也很簡單, 將幾個服務(wù)的生命周期轉(zhuǎn)換為單例模式.

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

        host.Initialize();

        return host;

最終用上面得出的幾個對象創(chuàng)建了一個WebHost對象.

總結(jié)

總的看來WebHostBuilder做了兩件事:
1. 定義了一些WebHost的配置項
2. 創(chuàng)建依賴注入的容器, 并注入一些service

作為開篇, 川酷盡量做到嚴(yán)謹(jǐn)和詳細(xì), 如果有不明白的地方歡迎討論. 也歡迎您的批評指正. 下一篇計劃寫一篇關(guān)于WebHost的內(nèi)容, 謝謝您的關(guān)注!

--------------------------華麗麗的分割-----------------------------------

愿dotnet社區(qū)日益強(qiáng)大, 愿dotnet生態(tài)日趨完善. dotnet core技術(shù)交流群歡迎你, 掃描下面二維碼進(jìn)群.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漓糙,一起剝皮案震驚了整個濱河市铣缠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昆禽,老刑警劉巖蝗蛙,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異醉鳖,居然都是意外死亡捡硅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門盗棵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來壮韭,“玉大人,你說我怎么就攤上這事纹因∨缥荩” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵瞭恰,是天一觀的道長屯曹。 經(jīng)常有香客問我,道長惊畏,這世上最難降的妖魔是什么恶耽? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮颜启,結(jié)果婚禮上偷俭,老公的妹妹穿的比我還像新娘。我一直安慰自己农曲,他們只是感情好社搅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乳规,像睡著了一般形葬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上暮的,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天笙以,我揣著相機(jī)與錄音,去河邊找鬼冻辩。 笑死猖腕,一個胖子當(dāng)著我的面吹牛拆祈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播倘感,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼放坏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了老玛?” 一聲冷哼從身側(cè)響起淤年,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜡豹,沒想到半個月后麸粮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镜廉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年弄诲,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娇唯。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡齐遵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出视乐,到底是詐尸還是另有隱情洛搀,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布佑淀,位于F島的核電站留美,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏伸刃。R本人自食惡果不足惜谎砾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望捧颅。 院中可真熱鬧景图,春花似錦、人聲如沸碉哑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扣典。三九已至妆毕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贮尖,已是汗流浹背笛粘。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人薪前。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓润努,卻偏偏與公主長得像,于是被迫代替她去往敵國和親示括。 傳聞我的和親對象是個殘疾皇子铺浇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容