原始碼解析.Net中Host主機的構建過程

SnailZz發表於2021-09-10

前言

本篇文章著重講一下在.Net中Host主機的構建過程,依舊延續之前文章的思路,著重講解其原始碼,如果有不知道有哪些用法的同學可以點選這裡,廢話不多說,我們們直接進入正題

Host構建過程

下圖是我自己整理的Host構建過程以及裡面包含的知識點我都以連結的形式放上來,大家可以看下圖,大概瞭解下過程(由於知識點過多,所以只能分上下兩張圖了?):

圖中標識的相關知識點連線如下(ps:與編號對應):

以上就是筆者在原始碼閱讀階段,其總結的自我感覺重要的知識點在微軟文件中的對應位置。

原始碼解析

這部分筆者根據上圖中的四大塊分別進行原始碼解析,可能篇幅比較長,其主要是對原始碼增加了自己理解的註釋,所以讀者在閱讀的過程中,要多注意原始碼中的註釋(ps:展示出的程式碼不是全部程式碼,而只是重要程式碼哦,每個小節總結的點都是按照程式碼順序解釋)

初始化預設配置ConfigDefaultBuilder

public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[] args)
{
    //設定程式執行路徑
    builder.UseContentRoot(Directory.GetCurrentDirectory());
    builder.ConfigureHostConfiguration(config =>
    {
        //新增獲取環境變數的字首
        config.AddEnvironmentVariables(prefix: "DOTNET_");
        //新增命令列引數
        if (args is { Length: > 0 })
        {
            config.AddCommandLine(args);
        }
    });

    builder.ConfigureAppConfiguration((hostingContext, config) =>
    {
        //宿主機環境資訊
        IHostEnvironment env = hostingContext.HostingEnvironment;
        //是否在檔案改變時重新載入,預設是True
        bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);

        //預設新增的配置檔案(格外新增以環境變數為名稱的檔案)
        config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
        //如果是開發環境,並且應用程式的應用名稱不是空字串,則載入使用者機密,預設true(主要是為了不同開發人員的配置不同)
        if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
        {
            var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
            if (appAssembly is not null)
            {
                config.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange);
            }
        }
        
        //這裡再次執行是為了讓環境變數和命令列引數的配置優先順序提高(後載入的key/value獲取時優先順序最高)
        //新增其他環境變數
        config.AddEnvironmentVariables();

        //新增命令列引數
        if (args is { Length: > 0 })
        {
            config.AddCommandLine(args);
        }
    })
    .ConfigureLogging((hostingContext, logging) =>
    {
        //判斷作業系統
        bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
        if (isWindows)
        {
            //新增過濾規則,捕獲warning日誌
            logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
        }
        //獲取Logging配置
        logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
        //新增輸出到控制檯
        logging.AddConsole();
        //新增將debug日誌輸出到控制檯
        logging.AddDebug();
        //新增寫入的事件源
        logging.AddEventSourceLogger();

        if (isWindows)
        {
            //新增事件日誌
            logging.AddEventLog();
        }
        //新增鏈路追蹤選項
        logging.Configure(options =>
        {
            options.ActivityTrackingOptions =
                ActivityTrackingOptions.SpanId |
                ActivityTrackingOptions.TraceId |
                ActivityTrackingOptions.ParentId;
        });
    })
    .UseDefaultServiceProvider((context, options) =>
    {
        bool isDevelopment = context.HostingEnvironment.IsDevelopment();
        //依賴注入相關校驗
        options.ValidateScopes = isDevelopment;
        options.ValidateOnBuild = isDevelopment;
    });
    return builder;
}

原始碼總結:

  • 設定程式執行路徑以及獲取環境變數和載入命令列引數。
  • 根據環境變數載入appsettings.json,載入使用者機密資料(僅開發環境)。
  • 接著又載入環境變數和命令列引數(這裡為什麼又載入了一次呢?是因為這它們執行的順序是不一樣的,而後載入的會覆蓋前面載入的Key/Value,前面載入主要是確定當前執行的環境變數以及使用者自定義的命令列引數,後面是為確保通過key獲取value的時候能夠獲取到準確的值)。
  • 接下來就主要是配置預設Log,如果是開發環境,依賴注入相關的配置預設開啟(驗證scope是否被用於singleton,驗證是否在呼叫期間就建立所有服務至快取)。

初始化主機啟動配置ConfigureWebHostDefaults

public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
{
    _builder = builder;
    var configBuilder = new ConfigurationBuilder()
        .AddInMemoryCollection();

    //新增以ASPNETCORE_開頭的環境變數(ps:判斷當前環境是那個環境)
    if (!options.SuppressEnvironmentConfiguration)
    {
        configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_");
    }
    //這裡主要載入環境變數
    _config = configBuilder.Build();

    _builder.ConfigureHostConfiguration(config =>
    {
        //將上面的配置載入進來
        config.AddConfiguration(_config);

        //通過配置和特性載入額外的Config(或者不載入配置),通過繼承IHostingStartup無侵入性載入。
        ExecuteHostingStartups();
    });
    //將上面Startup中Config的配置放到Build階段載入
    _builder.ConfigureAppConfiguration((context, configurationBuilder) =>
    {
        if (_hostingStartupWebHostBuilder != null)
        {
            var webhostContext = GetWebHostBuilderContext(context);
            _hostingStartupWebHostBuilder.ConfigureAppConfiguration(webhostContext, configurationBuilder);
        }
    });

    //增加註入的服務
    _builder.ConfigureServices((context, services) =>
    {
        var webhostContext = GetWebHostBuilderContext(context);
        var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];

        //注入一些其他服務
        services.AddSingleton(webhostContext.HostingEnvironment);
        services.AddSingleton((AspNetCore.Hosting.IHostingEnvironment)webhostContext.HostingEnvironment);
        services.AddSingleton<IApplicationLifetime, GenericWebHostApplicationLifetime>();

        services.Configure<GenericWebHostServiceOptions>(options =>
        {
            options.WebHostOptions = webHostOptions;
            options.HostingStartupExceptions = _hostingStartupErrors;
        });

        services.TryAddSingleton(sp => new DiagnosticListener("Microsoft.AspNetCore"));
        services.TryAddSingleton<DiagnosticSource>(sp => sp.GetRequiredService<DiagnosticListener>());
        services.TryAddSingleton(sp => new ActivitySource("Microsoft.AspNetCore"));

        services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
        services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
        services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();

        _hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);

        //可以通過配置的方式查詢程式集載入StartUp,但是預設只會載入最後一個StartUp
        if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
        {
            try
            {
                var startupType = StartupLoader.FindStartupType(webHostOptions.StartupAssembly, webhostContext.HostingEnvironment.EnvironmentName);
                UseStartup(startupType, context, services);
            }
            catch (Exception ex) when (webHostOptions.CaptureStartupErrors)
            {
                var capture = ExceptionDispatchInfo.Capture(ex);

                services.Configure<GenericWebHostServiceOptions>(options =>
                {
                    options.ConfigureApplication = app =>
                    {
                        capture.Throw();
                    };
                });
            }
        }
    });
}
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
    //提供.netCore 靜態Web資產(ps:說實話這裡不知道有什麼用)
    builder.ConfigureAppConfiguration((ctx, cb) =>
    {
        if (ctx.HostingEnvironment.IsDevelopment())
        {
            StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
        }
    });
    //使用 Kestrel 配置反向代理
    builder.UseKestrel((builderContext, options) =>
    {
        options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
    })
    .ConfigureServices((hostingContext, services) =>
    {
        //配置啟動的Url
        services.PostConfigure<HostFilteringOptions>(options =>
        {
            if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
            {
                var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
            }
        });
        services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(
                    new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));
        services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
        //用來獲取客戶端的IP地址
        if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
        {
            services.Configure<ForwardedHeadersOptions>(options =>
            {
                options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
                options.KnownNetworks.Clear();
                options.KnownProxies.Clear();
            });
            services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
        }
        //新增路由配置
        services.AddRouting();
    })
    //預設使用IIS
    .UseIIS()
    //使用程式內的託管模式
    .UseIISIntegration();
}

這部分內容可能會多點,原始碼總結:

  • 新增Memory快取,新增ASPNETCORE_開頭的環境變數。
  • 根據使用者的配置,來載入額外的StartUp中Config配置,但是它的引數是IWebHostBuilder,這部分可以參考微軟文件StartUp的部分。
  • 如果有存在這些配置的話,則統一放到Build階段載入。
  • 載入web主機需要的注入的服務,以及判斷是否需要通過程式集來載入StartUp,並且新增一個程式啟動時呼叫的服務(這裡主要是構建HttpContext執行管道)。
  • 引用Kestrel,繼承路由和IIS,並且預設使用程式內託管。
  • 載入使用者自定義的其他配置,例如預設的呼叫UseStartup方法。

根據指定配置開始初始化主機Build

public class HostBuilder : IHostBuilder
{
  private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
  private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>();
  private List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<HostBuilderContext, IServiceCollection>>();
  private List<IConfigureContainerAdapter> _configureContainerActions = new List<IConfigureContainerAdapter>();
  private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory());
  private bool _hostBuilt;
  private IConfiguration _hostConfiguration;
  private IConfiguration _appConfiguration;
  private HostBuilderContext _hostBuilderContext;
  private HostingEnvironment _hostingEnvironment;
  private IServiceProvider _appServices;
  private PhysicalFileProvider _defaultProvider;

  public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();

  public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
  {
      _configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
      return this;
  }

  public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
  {
      _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
      return this;
  }

  public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
  {
      _configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
      return this;
  }

  public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
  {
      _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory)));
      return this;
  }

  public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory)
  {
      _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(() => _hostBuilderContext, factory ?? throw new ArgumentNullException(nameof(factory)));
      return this;
  }

  public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate)
  {
      _configureContainerActions.Add(new ConfigureContainerAdapter<TContainerBuilder>(configureDelegate
          ?? throw new ArgumentNullException(nameof(configureDelegate))));
      return this;
  }

  public IHost Build()
  {
      //只能執行一次這個方法
      if (_hostBuilt)
      {
          throw new InvalidOperationException(SR.BuildCalled);
      }
      _hostBuilt = true;

      using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
      const string hostBuildingEventName = "HostBuilding";
      const string hostBuiltEventName = "HostBuilt";

      if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
      {
          Write(diagnosticListener, hostBuildingEventName, this);
      }

      //執行Host配置(應用程式執行路徑,載入_dotnet環境變數,獲取命令列引數,載入預配置)
      BuildHostConfiguration();
      //設定主機環境變數
      CreateHostingEnvironment();
      //構建HostBuilderContext例項
      CreateHostBuilderContext();
      //構建程式配置(載入appsetting.json,環境變數,命令列引數等)
      BuildAppConfiguration();
      //構造容器,注入服務
      CreateServiceProvider();

      var host = _appServices.GetRequiredService<IHost>();
      if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
      {
          Write(diagnosticListener, hostBuiltEventName, host);
      }

      return host;
  }

  private static void Write<T>(
      DiagnosticSource diagnosticSource,
      string name,
      T value)
  {
      diagnosticSource.Write(name, value);
  }

  private void BuildHostConfiguration()
  {
      IConfigurationBuilder configBuilder = new ConfigurationBuilder()
          .AddInMemoryCollection();

      foreach (Action<IConfigurationBuilder> buildAction in _configureHostConfigActions)
      {
          buildAction(configBuilder);
      }
      //本質是執行ConfigureProvider中的Load方法,載入對應配置
      _hostConfiguration = configBuilder.Build();
  }

  private void CreateHostingEnvironment()
  {
      //設定環境變數
      _hostingEnvironment = new HostingEnvironment()
      {
          ApplicationName = _hostConfiguration[HostDefaults.ApplicationKey],
          EnvironmentName = _hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production,
          ContentRootPath = ResolveContentRootPath(_hostConfiguration[HostDefaults.ContentRootKey], AppContext.BaseDirectory),
      };

      if (string.IsNullOrEmpty(_hostingEnvironment.ApplicationName))
      {
          _hostingEnvironment.ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name;
      }
      //程式執行路徑
      _hostingEnvironment.ContentRootFileProvider = _defaultProvider = new PhysicalFileProvider(_hostingEnvironment.ContentRootPath);
  }

  private void CreateHostBuilderContext()
  {
      _hostBuilderContext = new HostBuilderContext(Properties)
      {
          HostingEnvironment = _hostingEnvironment,
          Configuration = _hostConfiguration
      };
  }

  private void BuildAppConfiguration()
  {
      //對於已經載入過的配置不再重新載入
      IConfigurationBuilder configBuilder = new ConfigurationBuilder()
          .SetBasePath(_hostingEnvironment.ContentRootPath)
          .AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);

      //注意這裡是AppConfig
      foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
      {
          buildAction(_hostBuilderContext, configBuilder);
      }
      _appConfiguration = configBuilder.Build();
      //將新的配置賦值給config
      _hostBuilderContext.Configuration = _appConfiguration;
  }

  private void CreateServiceProvider()
  {
      var services = new ServiceCollection();
      services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
      services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
      services.AddSingleton(_hostBuilderContext);
      services.AddSingleton(_ => _appConfiguration);
      services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
      services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
      services.AddSingleton<IHostLifetime, ConsoleLifetime>();
      services.AddSingleton<IHost>(_ =>
      {
          return new Internal.Host(_appServices,
              _hostingEnvironment,
              _defaultProvider,
              _appServices.GetRequiredService<IHostApplicationLifetime>(),
              _appServices.GetRequiredService<ILogger<Internal.Host>>(),
              _appServices.GetRequiredService<IHostLifetime>(),
              _appServices.GetRequiredService<IOptions<HostOptions>>());
      });
      services.AddOptions().Configure<HostOptions>(options => { options.Initialize(_hostConfiguration); });
      services.AddLogging();
      //主要載入額外注入的服務
      foreach (Action<HostBuilderContext, IServiceCollection> configureServicesAction in _configureServicesActions)
      {
          configureServicesAction(_hostBuilderContext, services);
      }
      //這裡返回object,主要是為了保留擴充套件,讓使用者自定義的依賴注入框架能夠執行。
      object containerBuilder = _serviceProviderFactory.CreateBuilder(services);
      foreach (IConfigureContainerAdapter containerAction in _configureContainerActions)
      {
          containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
      }

      _appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);

      if (_appServices == null)
      {
          throw new InvalidOperationException(SR.NullIServiceProvider);
      }
      //可能是想先把IConfiguration載入到記憶體中
      _ = _appServices.GetService<IConfiguration>();
  }
}

在上面的兩個小單元可以看出,所有的構造是以委託的方式,最後都載入到HostBuilder內部的委託集合中,原始碼總結:

  • 載入環境變數和命令列引數。
  • 構建HostingEnvironment物件。
  • 構建HostBuilderContext物件,裡面包含配置和執行環境。
  • 載入appsettings.json,環境變數和命令列引數等。
  • 注入一些必須的服務,載入日誌配置,WebHost裡面注入的服務,載入StartUp裡面ConfigService裡面的服務,以及其他的一些注入的服務,構建容器(最後那裡獲取IConfiguration猜測可能是想先快取到根容器吧)。

執行主機Run

public async Task StartAsync(CancellationToken cancellationToken = default)
{
    _logger.Starting();

    using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
    CancellationToken combinedCancellationToken = combinedCancellationTokenSource.Token;

    //應用程式啟動和關閉事件
    await _hostLifetime.WaitForStartAsync(combinedCancellationToken).ConfigureAwait(false);

    combinedCancellationToken.ThrowIfCancellationRequested();
    _hostedServices = Services.GetService<IEnumerable<IHostedService>>();
    //主要是執行一些後臺任務,可以重寫啟動和關閉時要做的操作
    foreach (IHostedService hostedService in _hostedServices)
    {
        //立即執行的任務,例如構建管道就是在這裡
        await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
        //執行一些後臺任務
        if (hostedService is BackgroundService backgroundService)
        {
            _ = TryExecuteBackgroundServiceAsync(backgroundService);
        }
    }
    //通知應用程式啟動成功
    _applicationLifetime.NotifyStarted();

    //程式啟動
    _logger.Started();
}

原始碼總結:

  • 監聽程式的啟動關閉事件。
  • 開始執行Hosted服務或者載入後臺執行的任務。
  • 通過TaskCompletionSource來持續監聽Token,hold住程式。

總結

通過模板來構建的.Net泛型主機,其實已經可以滿足大部分的要求,並且微軟保留大量擴充套件讓使用者來自定義,當然你也可以構建其他不同的主機型別(如:Web主機或者控制檯程式啟動項配置),想了解的可以點選這裡

以上就是筆者通過閱讀原始碼來分析的程式執行流程,因為篇幅問題沒有把所有程式碼都放出來,實在是太多了,所以只放了部分程式碼,主要是想給閱讀原始碼的同學在閱讀的時候找到思路,可能會有一點錯誤,還請評論指正?。

相關文章