天天看點

ASP.NET Core使用HostingStartup增強啟動操作

概念

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

使用方式

    HostingStartup屬性表示要在運作時激活的承載啟動程式集。大緻分為兩種情況,一種是自動掃描目前Web程式集中通過HostingStartup指定的類,另一種是手動添加配置hostingstartupassembles指定外部的程式集中通過HostingStartup指定的類。第一種方式相對簡單,但是對Web程式本身有入侵,第二種方式稍微複雜一點點,但是可以做到對現有代碼無入侵操作,接下來我們分别示範這兩種使用方式。

ASP.NET Core中直接定義

首先是在ASP.NET Core程式中直接使用HostingStartup,這種方式比較簡單首先在Web程式中随便定義一個類,然後實作IHostingStartup接口,最後别忘了在程式集中添加HostingStartupAttribute指定要啟動的類的類型,具體代碼如下所示

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方法。

外部程式集引入

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

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

如果你不需要使用注冊中間件的邏輯那麼僅僅引入Microsoft.AspNetCore.Hosting.Abstractions即可

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

如果需要使用其他功能包,可以自行在定義的程式集中引入。比如我們定義了一個名為HostStartupLib的Standard類庫,并建立了名為HostStartupLib的類

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中

"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的方式實作了對代碼無入侵的方式進行監控和鍊路跟蹤。于是決定深入研究一下,可謂收獲滿滿,便寫下這篇文章希望更多的人能夠了解使用這個功能。

👇歡迎掃碼關注我的公衆号👇

ASP.NET Core使用HostingStartup增強啟動操作