ASP.NET Core使用HostingStartup加強啓動操做

概念
javascript

    在ASP.NET Core中咱們可使用一種機制來加強啓動時的操做,它就是HostingStartup。如何叫"加強"操做,相信瞭解過AOP概念的同窗應該都很是的熟悉。咱們常說AOP使用了關注點分離的方式,加強了對現有邏輯的操做。而咱們今天要說的HostingStartup就是爲了"加強"啓動操做,這種"加強"的操做甚至能夠對對現有的程序能夠作到無改動的操做。例如,外部程序集可經過HostingStartup實現爲應用提供配置服務、註冊服務或中間件管道操做等。java

使用方式

    HostingStartup屬性表示要在運行時激活的承載啓動程序集。大體分爲兩種狀況,一種是自動掃描當前Web程序集中經過HostingStartup指定的類,另外一種是手動添加配置hostingstartupassembles指定外部的程序集中經過HostingStartup指定的類。第一種方式相對簡單,可是對Web程序自己有入侵,第二種方式稍微複雜一點點,可是能夠作到對現有代碼無入侵操做,接下來咱們分別演示這兩種使用方式。web

ASP.NET Core中直接定義

首先是在ASP.NET Core程序中直接使用HostingStartup,這種方式比較簡單首先在Web程序中隨便定義一個類,而後實現IHostingStartup接口,最後別忘了在程序集中添加HostingStartupAttribute指定要啓動的類的類型,具體代碼以下所示typescript

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
//經過HostingStartup指定要啓動的類型
[assembly: HostingStartup(typeof(HostStartupWeb.HostingStartupInWeb))]
namespace HostStartupWeb
{
    public class HostingStartupInWeb : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            //程序啓動時打印依據話,表明執行到了這裏
            Debug.WriteLine("Web程序中HostingStartupInWeb類啓動");


            //能夠添加配置
            builder.ConfigureAppConfiguration(config => {
                //模擬添加一個一個內存配置
                var datas = new List<KeyValuePair<string, string>>
                {
                    new KeyValuePair<string, string>("ServiceName", "HostStartupWeb")
                };
                config.AddInMemoryCollection(datas);
            });


            //能夠添加ConfigureServices
            builder.ConfigureServices(services=> {
                //模擬註冊一個PersonDto
                services.AddScoped(provider=>new PersonDto { Id = 1, Name = "yi念之間", Age = 18 });
            });


            //能夠添加Configure
            builder.Configure(app => {
                //模擬添加一箇中間件
                app.Use(async (context, next) =>
                {
                    await next();
                });
            });
        }
    }
}

僅僅使用上面所示的這些代碼,即可在Web程序啓動的時候去自動執行HostingStartupInWeb的Configure方法,在這裏面咱們幾乎可使用全部針對ASP.NET Core程序配置的操做,並且不須要在Web程序中額外添加別的代碼就能夠自動調用HostingStartupInWeb的Configure方法。json

外部程序集引入

咱們以前也說過,上面的方式雖然使用起來相對簡單一點,僅僅是一點,那就是省去了指定啓動程序集的邏輯。可是,上面的方式須要在Web程序中添加,這樣的話仍是會修改代碼。並且,可能更多的時候咱們是在外部的程序集中編寫HostingStartup邏輯,這時候就須要使用另外一種方式在將外部程序集中引入HostingStartup。首先咱們要在自定義的程序集中至少引入Microsoft.AspNetCore.Hosting包才能使用HostingStartupapp

<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />

若是你不須要使用註冊中間件的邏輯那麼僅僅引入Microsoft.AspNetCore.Hosting.Abstractions便可async

<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />

若是須要使用其餘功能包,能夠自行在定義的程序集中引入。好比咱們定義了一個名爲HostStartupLib的Standard類庫,並建立了名爲HostStartupLib的類ide

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
[assembly: HostingStartup(typeof(HostStartupLib.HostingStartupInLib))]
namespace HostStartupLib
{
    public class HostingStartupInLib : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            Debug.WriteLine("Lib程序中HostingStartupInLib類啓動");


            //添加配置
            builder.ConfigureAppConfiguration((context, config) => {
                var datas = new List<KeyValuePair<string, string>>
                {
                    new KeyValuePair<string, string>("ServiceName", "HostStartupLib")
                };
                config.AddInMemoryCollection(datas);
            });


            //添加ConfigureServices
            builder.ConfigureServices(services=> {
                services.AddScoped(provider=>new PersonDto { Id = 2, Name = "er念之間", Age = 19 });
            });


            //添加Configure
            builder.Configure(app => {
                app.Use(async (context, next) =>
                {
                    await next();
                });
            });


        }
    }
}

而後咱們將自定義的HostStartupLib這個Standard類庫引入Web項目中,運行Web程序,發現HostingStartupInLib的Configure方法並不能被調用。其實咱們上面說過了,將HostingStartup從外部程序集引入的話須要手動指定啓動程序集的名稱。指定啓動程序集的方式有兩種,一種是指定IWebHostBuilder的擴展UseSetting指定學習

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    //經過UseSetting的方式指定程序集的名稱
                    webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "HostStartupLib");
                    //若是HostingStartup存在多個程序集中可使用;分隔,好比HostStartupLib;HostStartupLib2
                    //webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "HostStartupLib;HostStartupLib2");
                    webBuilder.UseStartup<Startup>();
                });

另外一種經過添加環境變量ASPNETCORE_HOSTINGSTARTUPASSEMBLIES的方式,能夠經過設置launchSettings.json中ui

"environmentVariables": {
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "HostStartupLib"
        //若是HostingStartup存在多個程序集中可使用;分隔,好比HostStartupLib;HostStartupLib2
        //"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "HostStartupLib;HostStartupLib2"
}

能夠引入多個包含HostingStartup的程序集,在設置WebHostDefaults.HostingStartupAssembliesKey或者ASPNETCORE_HOSTINGSTARTUPASSEMBLIES指定多個程序集名稱可使用英文分號(;)隔開程序集名稱。雖然是兩種形似指定,可是其實本質是同樣的那就是設置配置key爲hostingStartupAssemblie配置的值,下面咱們會詳細講解。
    經過在程序中設置環境變量的方式等同於Window系統中Set的方式設置環境變量,或Linux系統中export的方式設置環境變量,亦或是直接設置系統環境變量,效果都是一致的。指定完成啓動程序集以後,再次運行程序即可以看到HostingStartupInLib的Configure方法被調用到了。在這裏咱們能夠看到若是是使用的環境變量的方式去指定啓動程序集的話,對現有代碼能夠作到徹底無入侵。

源碼探究

在上面咱們簡單的介紹了HostingStartup的概念及基本的使用方式,基於這些咱們產生了幾個疑問

  • 首先是關於HostingStartup的基本工做方式是什麼

  • 其次是爲何HostingStartup在Web程序中不須要配置程序集信息就能夠被調用到,而經過外部程序集引入HostingStartup須要手動指定程序集

  • 最後是經過外部程序集引入HostingStartup的指定方式爲什麼只能是UseSetting和環境變量的方式
    基於以上幾個疑問,咱們來探索一下HostingStartup的相關源碼,來揭開它的神祕面紗。首先廢話很少說直接找到源碼位置[點擊查看源碼????]在GenericWebHostBuilder類中的ExecuteHostingStartups方法中,關於GenericWebHostBuilder類咱們在上篇文章深刻探究ASP.NET Core Startup初始化中主要就是分析這個類,由於這是構建WebHost的默認類,而咱們接下來要說的ExecuteHostingStartups方法也是承載在這個類中,直接貼代碼以下所示

private void ExecuteHostingStartups()
{
    //經過配置_config和當前程序集名稱構建WebHostOptions類
    var webHostOptions = new WebHostOptions(_config, Assembly.GetEntryAssembly()?.GetName().Name);
    //若是PreventHostingStartup屬性爲true則直接返回
    //經過這個能夠配置阻止啓動邏輯
    if (webHostOptions.PreventHostingStartup)
    {
        return;
    }


    var exceptions = new List<Exception>();
    //構建HostingStartupWebHostBuilder
    _hostingStartupWebHostBuilder = new HostingStartupWebHostBuilder(this);
    //GetFinalHostingStartupAssemblies獲取最終要執行的程序集名稱
    foreach (var assemblyName in webHostOptions.GetFinalHostingStartupAssemblies().Distinct(StringComparer.OrdinalIgnoreCase))
    {
        try
        {
            //經過程序集名稱加載程序集信息,由於使用了AssemblyName因此只須要使用程序集名稱便可
            var assembly = Assembly.Load(new AssemblyName(assemblyName));
            //獲取包含HostingStartupAttribute的程序集
            foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>())
            {
                //實例化HostingStartupAttribute的HostingStartupType屬性的對象實例
                //即咱們上面聲明的[assembly: HostingStartup(typeof(HostStartupWeb.HostingStartupInWeb))]
                var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType);
                //調用HostingStartup的Configure方法
                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);
    }
}

經過上面的源碼咱們就能夠很清楚的瞭解到HostingStartup的基本工做方式。獲取的程序集中包含的HostingStartupAttribute,經過獲取HostingStartupAttribute的HostingStartupType屬性獲得要執行的IHostingStartup實例,最後執行Configure方法,Configure方法須要傳遞IWebHostBuilder的實例,而HostingStartupWebHostBuilder正是實現了IWebHostBuilder接口。
    咱們瞭解到了HostStartup的工做方式,接下來咱們來探究一下爲何HostingStartup在Web程序中不須要配置程序集信息就能夠被調用到,而經過外部程序集引入HostingStartup須要手動指定程序集。經過上面的源碼咱們能夠獲得一個信息那就是全部須要啓動的程序集信息都是來自WebHostOptions的GetFinalHostingStartupAssemblies方法,接下來咱們就來查看一下GetFinalHostingStartupAssemblies方法的實現源碼[點擊查看源碼????]

public IEnumerable<string> GetFinalHostingStartupAssemblies()
{
    return HostingStartupAssemblies.Except(HostingStartupExcludeAssemblies, StringComparer.OrdinalIgnoreCase);
}

從這裏咱們能夠看出程序集信息來自於HostingStartupAssemblies屬性,並且還要排除掉HostingStartupExcludeAssemblies包含的程序集。咱們找到他們初始化的相關邏輯大體以下

//承載啓動是須要調用的HostingStartup程序集
public IReadOnlyList<string> HostingStartupAssemblies { get; set; }
//承載啓動時排除掉不不要執行的程序集
public IReadOnlyList<string> HostingStartupExcludeAssemblies { get; set; }
//是否阻止HostingStartup啓動執行功能,若是設置爲false則HostingStartup功能失效
//經過上面的ExecuteHostingStartups方法源碼可知
public bool PreventHostingStartup { get; set; }
//應用程序名稱
public string ApplicationName { get; set; }


public WebHostOptions(IConfiguration configuration, string applicationNameFallback)
{
    ApplicationName = configuration[WebHostDefaults.ApplicationKey] ?? applicationNameFallback;
    HostingStartupAssemblies = Split($"{ApplicationName};{configuration[WebHostDefaults.HostingStartupAssembliesKey]}");
    HostingStartupExcludeAssemblies = Split(configuration[WebHostDefaults.HostingStartupExcludeAssembliesKey]);
    PreventHostingStartup = WebHostUtilities.ParseBool(configuration, WebHostDefaults.PreventHostingStartupKey);
}


//分隔配置的程序集信息,分隔依據爲";"分號,這也是咱們上面說過配置多程序集的時候採用分號分隔的緣由
private IReadOnlyList<string> Split(string value)
{
    return value?.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
        ?? Array.Empty<string>();
}

首先,經過HostingStartupAssemblies的初始化邏輯咱們能夠得出,默認會是有兩個數據來源,一個是當前的ApplicationName,另外一個是經過HostingStartupAssembliesKey配置的程序集信息。這也解答了咱們上面說過的爲何HostingStartup在Web程序中不須要配置程序集信息就能夠被調用到,而經過外部程序集引入HostingStartup須要手動指定程序集。其次,咱們能夠了解到經過配置HostingStartupExcludeAssemblies信息排除你不想啓動的HostingStartup程序集,並且還能夠經過配置PreventHostingStartup值來禁止使用HostingStartup的功能。
經過上面的代碼咱們還了解到這三個屬性的來源的配置名稱都是來自WebHostDefaults這個常量類,接下來咱們查看一下這三個屬性對應的配置名稱

public static readonly string HostingStartupAssembliesKey = "hostingStartupAssemblies";
public static readonly string HostingStartupExcludeAssembliesKey = "hostingStartupExcludeAssemblies";
public static readonly string PreventHostingStartupKey = "preventHostingStartup";

也就是說,咱們能夠能夠經過配置這三個名稱的配置,來完成HostingStartup相關的功能好比

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    //經過UseSetting的方式指定程序集的名稱
                    webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "HostStartupLib");
                    //若是HostingStartup存在多個程序集中可使用;分隔,好比HostStartupLib;HostStartupLib2
                    //webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "HostStartupLib;HostStartupLib2");


                    //排除執行HostStartupLib2程序集執行HostingStartup邏輯
                    webBuilder.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, "HostStartupLib2");
                    //禁用HostingStartup功能
                    webBuilder.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true");
                    webBuilder.UseStartup<Startup>();
                });

或經過環境變量的方式去操做

"environmentVariables": {
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "HostStartupLib",
        //若是HostingStartup存在多個程序集中可使用;分隔,好比HostStartupLib;HostStartupLib2
        //"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "HostStartupLib;HostStartupLib2"


       //排除執行HostStartupLib2程序集執行HostingStartup邏輯
       "ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES":"HostStartupLib2",
       //禁用HostingStartup功能
       "ASPNETCORE_PREVENTHOSTINGSTARTUP":"true"
}

其實這兩種配置方式是徹底等價的,爲何這麼說呢?首先是在Configuration中獲取配置是忽略大小寫的,實際上是使用ConfigureWebHostDefaults配置WebHost相關信息的時候會添加configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_")邏輯這樣的話獲取環境變量的時候能夠忽略ASPNETCORE_前綴。
那麼到目前爲止,還有一個疑問還沒有解決,那就是爲什麼只能經過UseSetting和環境變量的方式去配置HostingStartup相關配置,解鈴還須繫鈴人,咱們在上面的ExecuteHostingStartups方法中看到了這個邏輯

//這裏傳遞了一個_config
var webHostOptions = new WebHostOptions(_config, Assembly.GetEntryAssembly()?.GetName().Name);

咱們能夠看到傳遞了配置Configuration的實例_config,咱們到初始化_config地方有以下邏輯

var configBuilder = new ConfigurationBuilder()
                .AddInMemoryCollection();
if (!options.SuppressEnvironmentConfiguration)
{
    //添加環境變量
    configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_");
}
//構建了_config實例
private readonly IConfiguration _config = configBuilder.Build();

也就能夠解釋爲什麼咱們能夠經過環境變量去配置HostingStartup,而後咱們再來看UseSetting方法的邏輯

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

原來UseSetting也是給_config實例設置值,因此不管經過UseSetting或環境環境變量的方式去配置,本質都是在操做_config這個配置實例,到此爲止全部謎團均以解開。

在SkyAPM中的使用

咱們上面說了HostingStartup能夠加強啓動時候的操做,能夠經過對現有代碼無入侵的方式加強程序功能。而SkyAPM-dotnet也正是使用了這個功能,實現了無入侵啓動APM監控。咱們來回顧一下SkyAPM-dotnet的使用方式

  • 首先是使用Nuget添加SkyAPM.Agent.AspNetCore程序集引用。

  • 其次是在launchSettings.json文件中添加ASPNETCORE_HOSTINGSTARTUPASSEMBLIES:"SkyAPM.Agent.AspNetCore"環境變量配置(等同於set ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=SkyAPM.Agent.AspNetCore或export ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=SkyAPM.Agent.AspNetCore
    的方式,本質都是在配置環境變量)

  • 最後經過SKYWALKING__SERVICENAME設置程序名稱
    這裏咱們經過須要配置ASPNETCORE_HOSTINGSTARTUPASSEMBLIES名稱能夠看出確實是使用了HostingStartup功能,而經過HostingStartup加強的操做入口確定就在SkyAPM.Agent.AspNetCore程序集中,咱們找到SkyAPM.Agent.AspNetCore程序集的源碼[點擊查看源碼????]看到了SkyApmHostingStartup類實現以下

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SkyApm.Agent.AspNetCore;
using SkyApm.AspNetCore.Diagnostics;


[assembly: HostingStartup(typeof(SkyApmHostingStartup))]


namespace SkyApm.Agent.AspNetCore
{
    internal class SkyApmHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services => services.AddSkyAPM(ext => ext.AddAspNetCoreHosting()));
        }
    }
}

經過這個咱們能夠看出確實如此,固然也是等同於咱們經過UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "SkyApm.Agent.AspNetCore")去配置,咱們甚至可以使用以下的方式去使用SkyAPM-dotnet

public void ConfigureServices(IServiceCollection services)
{
   services.AddSkyAPM(ext => ext.AddAspNetCoreHosting())
}

這些寫法實際上是徹底等價的,可是經過環境變量的方式配置HostingStartup啓動程序集的方式無疑是最優雅的。因此咱們在平常的學習開發中,最好仍是經過這種方式去操做。

改造Zipkin使用

咱們在以前的文章ASP.NET Core整合Zipkin鏈路跟蹤中曾演示過基於診斷日誌DiagnosticSource改進Zipkin的集成方式,經過本篇文章講述的HostingStartup咱們能夠進步一改進Zipkin的集成方式,可讓它使用起來和SkyAPM-dotnet相似的方式,咱們基於以前的示例中的ZipkinExtensions程序集中添加一個ZipkinHostingStartup類,用於承載集成Zipkin的操做,代碼以下

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;


namespace ZipkinExtensions
{
    public class ZipkinHostingStartup: IHostingStartup
    {


        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services=> {
                services.AddZipkin();
                services.AddSingleton<ITraceDiagnosticListener, HttpDiagnosticListener>();
            });


            builder.Configure(app=> {
                IHostApplicationLifetime lifetime = app.ApplicationServices.GetService<IHostApplicationLifetime>();
                ILoggerFactory loggerFactory = app.ApplicationServices.GetService<ILoggerFactory>();
                IConfiguration configuration = app.ApplicationServices.GetService<IConfiguration>();
                string serivceName = configuration.GetValue<string>("ServiceName");
                string zipKinUrl = configuration.GetValue<string>("ASPNETCORE_ZIPKINADDRESS");


                app.UseZipkin(lifetime, loggerFactory, serivceName, zipKinUrl);
            });
        }
    }
}

而後在每一個項目的launchSettings.json文件中添加以下所示的配置便可,這樣的話就能夠作到對現有業務代碼無任何入侵。

"environmentVariables": {
    "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "ZipkinExtensions",
    "ASPNETCORE_ZIPKINADDRESS": "http://localhost:9411/"
  }

總結

    本文介紹了HostingStartup的基本概念,基礎使用以及對其源碼的分析和在SkyAPM-dotnet中的應用,最後咱們改造了Zipkin的集成方式。HostingStartup在一些集成APM或者鏈路跟蹤的相似場景仍是很是實用的,或者若是咱們有集成一些基礎組件或者三方的組件,可是咱們的代碼中並不須要直接的使用這些組件中的類或者直接的代碼關係,都可以使用HostingStartup的方式去集成,爲咱們實現對現有代碼提供無入侵加強提供了強大的支持。關於HostingStartup我也是在看源碼中無心發現的,後來發現微軟ASP.NET Core官方文檔Use hosting startup assemblies in ASP.NET Core一文中有講解,而後聯想到本身使用過的SkyAPM-dotnet正是使用了HostingStartup+診斷日誌DiagnosticSource的方式實現了對代碼無入侵的方式進行監控和鏈路跟蹤。因而決定深刻研究一下,可謂收穫滿滿,便寫下這篇文章但願更多的人可以瞭解使用這個功能。

????歡迎掃碼關注個人公衆號????

相關文章
相關標籤/搜索