一、托管服務(IHostedService)
在業務場景中經常需要后台服務不停的或定時處理一些任務,在 asp.net中會使用windows服務來處理,在 asp.net core中可以使用托管服務來實現,托管服務是一個類,具有實現IHostService接口的后台任務邏輯。托管服務必須實現IHostedService接口。抽象類BackgroundService是IHostedService的派生類,實現了IHostService接口定義的方法,因此自定義的托管服務類也可以繼承BackgroundService實現ExecuteAsync()方法即可。
1、IHostService
直接上IHostService的定義。托管服務必須實現IHostedService接口:
public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); }
-
StartAsync:當應用程序主機准備好啟動服務時觸發。
-
StartAsync:當應用程序主機執行正常關閉時觸發。
2、使用IHostedService自定義托管服務
自定義托管服務類直接繼承IHostedService和IDisposable接口。定時后台任務使用 System.Threading.Timer 類。計時器觸發任務的 DoWork
方法。 在 StopAsync
上禁用計時器,並在 Dispose
上處置服務容器時處置計時器:
public class TimedHostedService : IHostedService, IDisposable
{
private int executionCount = 0; private Timer _timer; public Task StartAsync(CancellationToken stoppingToken) { Console.WriteLine("Timed Hosted Service running."); _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); return Task.CompletedTask; } private void DoWork(object state) { var count = Interlocked.Increment(ref executionCount); Console.WriteLine("Timed Hosted Service is working. Count: {Count}", count); } public Task StopAsync(CancellationToken stoppingToken) { Console.WriteLine("Timed Hosted Service is stopping."); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } }
使用 AddHostedService
擴展方法在 IHostBuilder.ConfigureServices
(Program.cs) 中注冊該服務:
static void Main(string[] args)
{
try
{
var host = new HostBuilder().ConfigureServices((hostContext, services) =>
{
services.AddHostedService<TimedHostedService>();
}).Build();
host.Run();
Console.WriteLine("Hello World!");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
3、使用BackgroundService 實現自定義托管服務
由於抽象類BackgroundService已經實現了IHostService接口定義的方法,只需要寫子類去繼承BackgroundService, 在自己的自定義托管服務類中實現ExecuteAsync()方法
public class MyBackGroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { Console.WriteLine("MyBackGroundService doing"); //延遲500毫秒執行 相當於使用了定時器 await Task.Delay(500, stoppingToken); } } }
使用 AddHostedService
擴展方法在 IHostBuilder.ConfigureServices
(Program.cs) 中注冊該服務:
static void Main(string[] args)
{
try { var host = new HostBuilder().ConfigureServices((hostContext, services) => { services.AddHostedService<MyBackGroundService>(); }).Build(); host.Run(); Console.WriteLine("Hello World!"); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
托管的服務總是會被定義成IHostedService接口的實現類型。
public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); }
該接口僅定義了兩個用來啟動和關閉自身服務的方法。當作為宿主的IHost對象被啟動的時候,它會利用依賴注入框架激活每個注冊的IHostedService服務,並通過調用StartAsync方法來啟動它們。當服務關閉的時候,作為服務宿主的IHost對象會被關閉,由它承載的每個IHostedService服務對象的StopAsync方法也隨之被調用。
4、通用主機和托管服務的關系
(1)通用主機是什么?
就是IHostBuilder/IHost為核心的體系。下面會詳細介紹。
(2)為什么需要通用主機?
上邊已經介紹過,有些后台服務需要不停的或定時處理一些任務,在 asp.net中會使用windows服務來處理,在 asp.net core中可以使用托管服務來實現,Windows服務也好,托管服務也好,都不過是一個類,那么如何將這些服務類托管起來,運行起來呢?ASP.NET Core提供了以IHostBuilder/IHost為核心的體系用來寄宿托管服務,IHostBuilder/IHost位於.NET Runtime 而不是ASP.NET Core 中,籠統來說,IHostBuilder/IHost能夠與操作系統交互,以IHostedService接口實現的托管服務寄宿到IHostBuilder/IHost就可以在操作系統上運行起來了。利用通用主機可以將任意一個或者多個長時間運行的服務寄宿於托管進程中。任何需要在后台長時間運行的操作都可以定義成標准化的服務並利用通用主機來寄宿。
那么,一個ASP.NET Core應用本質上是一個需要長時間運行的服務,開啟這個服務是為了啟動一個網絡監聽器,當監聽到抵達的HTTP請求之后,該監聽器會將請求傳遞給應用提供的管道進行處理,管道完成了對請求處理之后會生成HTTP響應,並通過監聽器返回客戶端。從asp.net core源碼中看到GenericWebHostService就是一個托管服務,用來實現一系列的工作,為了讓GenericWebHostService運行起來,ASP.NET Core提供了以IHostBuilder/IHost為核心的體系。
反過來理解也可以,Asp.net Core的Runtime中構建了以IHostBuilder/IHost為核心的體系,目的是能夠跨平台,能夠與不同的操作系統打交道,是Asp.net Core應用的基礎。一個Asp.Net Core應用的一些核心工作交給托管服務處理,托管服務依賴IHostBuilder/IHost為核心的體系。關系圖如下:
二、通用主機模型
ASP.NET Core應用程序擁有一個內置的Self-Hosted(自托管)的Web Server(Web服務器),用來處理外部請求。不管是托管還是自托管,都離不開Host(宿主),即通用主機。在ASP.NET Core應用中通過配置並啟動一個Host來完成應用程序的啟動和其生命周期的管理。而Host的主要的職責就是Web Server的配置和Pilpeline(請求處理管道)的構建。
通用主機也叫泛型主機,為什么通用?是因為能夠與不同的操作系統交互。為什么能夠和不同的操作系統交互?籠統來說,通用主機能夠適配不同類型的Http服務器,其中kestrel服務器就是跨平台的關鍵。具體可以參考NetCore解讀服務器。
通用主機主要由三個核心對象組成:多個通過IHostedService接口表示的服務被寄宿於通過IHost接口表示的宿主上,IHostBuilder接口表示IHost對象的構建者。
//定義托管服務GenericWebHostService
internal class GenericWebHostService : IHostedService{}
//GenericWebHostService 托管服務承載於Ihost宿主。
_hostBuilder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>()).Build().Run();
在ASP.NET Core應用中通過配置並啟動一個Host來完成應用程序的啟動和其生命周期的管理,大概流程如下圖:
類Host調用CreateDefaultBuilder方法創建HostBuilder實例,HostBuilder實例調用相應的方法和擴展方法完成基礎配置后,調用Build()方法創建IHost實例,IHost實例再次調用Run()方法完成通用主機的啟動。下面將會詳細介紹通用主機模型的實現。
三、IHost
先來介紹下IHost,IHost位於.NET Runtime 而不是ASP.NET Core 中。通過IHostedService接口表示的托管服務GenericWebHostService 最終被承載於通過IHost接口表示的宿主上。一般來說,aspnetcore應用整個生命周期內只會創建一個IHost對象,啟動和關閉應用程序本質上就是啟動和關閉作為宿主的IHost對象。
public interface IHost : IDisposable { IServiceProvider Services { get; } Task StartAsync(CancellationToken cancellationToken = default); Task StopAsync(CancellationToken cancellationToken = default); }
IHost接口派生於IDisposable接口,所以當它在關閉之后,應用程序還會調用其Dispose方法作一些額外的資源釋放工作。IHost接口的Services屬性返回作為依賴注入容器的IServiceProvider對象,該對象提供了服務承載過程中所需的服務實例,其中就包括需要承載的IHostedService服務。定義在IHost接口中的StartAsync和StopAsync方法完成了針對服務宿主的啟動和關閉。
ASP.NET Core 所使用的 Host 是一個通用的宿主,它位於.NET Runtime 而不是ASP.NET Core 中。總而言之,Host是封裝應用程序資源的對象,為它所承載的應用提供環境和基本的功能,封裝的內容主要包括:
- DI容器
- 日志
- 配置
- IHostedService實現,啟動HTTP服務器的實現的服務就是一個IHostedService(GenericWebHostService)
1、Run方法
/// <summary> /// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered. /// </summary> /// <param name="host">The <see cref="IHost"/> to run.</param> /// <param name="token">The token to trigger shutdown.</param> /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns> public static async Task RunAsync(this IHost host, CancellationToken token = default) { try { await host.StartAsync(token); await host.WaitForShutdownAsync(token); } finally { if (host is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync(); } else { host.Dispose(); } } } /// <summary> /// Returns a Task that completes when shutdown is triggered via the given token. /// </summary> /// <param name="host">The running <see cref="IHost"/>.</param> /// <param name="token">The token to trigger shutdown.</param> /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns> public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default) { var applicationLifetime = host.Services.GetService<IHostApplicationLifetime>(); token.Register(state => { ((IHostApplicationLifetime)state).StopApplication(); }, applicationLifetime); var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); applicationLifetime.ApplicationStopping.Register(obj => { var tcs = (TaskCompletionSource<object>)obj; tcs.TrySetResult(null); }, waitForStop); await waitForStop.Task; // Host will use its default ShutdownTimeout if none is specified. await host.StopAsync(); }
IHost是由 IHostBuilder.Build() 方法創建的,所以我們通過對 IHostBuilder 的配置來實現對 Host 的配置。下面會介紹到IHostBuilder 。
四、類Host
此Host非IHost中的Host,此處的Host是一個類,提供了創建HostBuilder實例的方法,具體看下源碼:
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Provides convenience methods for creating instances of <see cref="IHostBuilder"/> with pre-configured defaults.
/// </summary>
public static class Host { /// <summary> /// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults. /// </summary> /// <remarks> /// The following defaults are applied to the returned <see cref="HostBuilder"/>: /// <list type="bullet"> /// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item> /// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item> /// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item> /// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item> /// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item> /// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item> /// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item> /// </list> /// </remarks> /// <returns>The initialized <see cref="IHostBuilder"/>.</returns> public static IHostBuilder CreateDefaultBuilder() => CreateDefaultBuilder(args: null); /// <summary> /// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults. /// </summary> /// <remarks> /// The following defaults are applied to the returned <see cref="HostBuilder"/>: /// <list type="bullet"> /// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item> /// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item> /// <item><description>load host <see cref="IConfiguration"/> from supplied command line args</description></item> /// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item> /// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item> /// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item> /// <item><description>load app <see cref="IConfiguration"/> from supplied command line args</description></item> /// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item> /// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item> /// </list> /// </remarks> /// <param name="args">The command line args.</param> /// <returns>The initialized <see cref="IHostBuilder"/>.</returns> public static IHostBuilder CreateDefaultBuilder(string[] args) { var builder = new HostBuilder(); builder.UseContentRoot(Directory.GetCurrentDirectory()); builder.ConfigureHostConfiguration(config => { config.AddEnvironmentVariables(prefix: "DOTNET_"); if (args != null) { config.AddCommandLine(args); } }); builder.ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName)) { var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null) { config.AddUserSecrets(appAssembly, optional: true); } } config.AddEnvironmentVariables(); if (args != null) { config.AddCommandLine(args); } }) .ConfigureLogging((hostingContext, logging) => { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); // IMPORTANT: This needs to be added *before* configuration is loaded, this lets // the defaults be overridden by the configuration. if (isWindows) { // Default the EventLogLoggerProvider to warning or above logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning); } logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); logging.AddDebug(); logging.AddEventSourceLogger(); if (isWindows) { // Add the EventLogLoggerProvider on windows machines logging.AddEventLog(); } }) .UseDefaultServiceProvider((context, options) => { var isDevelopment = context.HostingEnvironment.IsDevelopment(); options.ValidateScopes = isDevelopment; options.ValidateOnBuild = isDevelopment; }); return builder; } } }
類Host提供了兩個重載方法,用於生成HostBuilder實例。下面將詳細介紹CreateDefaultBuilder方法。
五、IHostBuilder
通用主機的配置基本上都是在IHostBuilder中實現的,IHostBuilder位於.NET Runtime 而不是ASP.NET Core 中。顧名思義,IHostBuilder是用來構建IHost宿主的,IHost對象在應用啟動過程中采用Builder模式由對應的IHostBuilder對象來創建,HostBuilder類型是對IHostBuilder接口的默認實現。通過下面方法創建一個HostBuilder對象並進行相應的配置:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseKestrel(); webBuilder.UseStartup<Startup>(); });
1、Host.CreateDefaultBuilder(string[] args)
public static IHostBuilder CreateDefaultBuilder(string[] args);//返回IHostBuilder
Host.CreateDefaultBuilder()用於創建HostBuilder實例,並使用預配置默認值初始化 HostBuilder 類的新實例。以下默認值將應用於返回的 HostBuilder :
-
將設置 內容根目錄ContentRootPath 的結果為GetCurrentDirectory()
-
從提供的命令行參數加載主機
-
載入 appsettings.json文件和appsettings.[enviromentName].json文件的配置
-
開發模式下,保存項目程序集到用戶機密
-
從環境變量中加載應用
-
從提供的命令行參數加載應用
-
配置 ILoggerFactory 以記錄到控制台、調試和事件源輸出
-
開發模式下,在DI容器上啟用作用域驗證 EnvironmentName
具體實現看下Host.CreateDefaultBuilder()源碼:https://github.com/dotnet/extensions/blob/release/3.1/src/Hosting/Hosting/src/Host.cs
/// <summary> /// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults. /// </summary> /// <remarks> /// The following defaults are applied to the returned <see cref="HostBuilder"/>: /// <list type="bullet"> /// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item> /// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item> /// <item><description>load host <see cref="IConfiguration"/> from supplied command line args</description></item> /// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item> /// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item> /// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item> /// <item><description>load app <see cref="IConfiguration"/> from supplied command line args</description></item> /// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item> /// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item> /// </list> /// </remarks> /// <param name="args">The command line args.</param> /// <returns>The initialized <see cref="IHostBuilder"/>.</returns> public static IHostBuilder CreateDefaultBuilder(string[] args) { var builder = new HostBuilder(); //將設置 內容根目錄ContentRootPath 的結果為GetCurrentDirectory() builder.UseContentRoot(Directory.GetCurrentDirectory()); //從提供的命令行參數加載主機 builder.ConfigureHostConfiguration(config => { config.AddEnvironmentVariables(prefix: "DOTNET_"); if (args != null) { config.AddCommandLine(args); } }); builder.ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; //載入 appsettings.json文件和appsettings.[enviromentName].json文件的配置 config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); //開發模式下,保存項目程序集到用戶機密 if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName)) { var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null) { config.AddUserSecrets(appAssembly, optional: true); } } //從環境變量中加載應用 config.AddEnvironmentVariables(); //從提供的命令行參數加載應用 if (args != null) { config.AddCommandLine(args); } }) //配置 ILoggerFactory 以記錄到控制台、調試和事件源輸出 .ConfigureLogging((hostingContext, logging) => { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); // IMPORTANT: This needs to be added *before* configuration is loaded, this lets // the defaults be overridden by the configuration. if (isWindows) { // Default the EventLogLoggerProvider to warning or above logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning); } logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); logging.AddDebug(); logging.AddEventSourceLogger(); if (isWindows) { // Add the EventLogLoggerProvider on windows machines logging.AddEventLog(); } }) //開發模式下,在DI容器上啟用作用域驗證 EnvironmentName .UseDefaultServiceProvider((context, options) => { var isDevelopment = context.HostingEnvironment.IsDevelopment(); options.ValidateScopes = isDevelopment; options.ValidateOnBuild = isDevelopment; }); return builder; }
HostBuilder創建完成后,下面先來看下HostBuilder的一些擴展方法
2、IHostBuilder方法和擴展方法
Host.CreateDefaultBuilder(args)返回了HostBuilder實例,並做了一些默認初始化的配置。IHostBuilder也提供了一些方法和擴展方法用來進一步的配置,IHostBuilder源碼如下:
namespace Microsoft.Extensions.Hosting
{
public interface IHostBuilder { IDictionary<object, object> Properties { get; } IHost Build(); IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate); IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate); IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate); IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate); IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory); IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory); } }
類HostBuilder實現了IHostBuilder,具體實現可以參考源碼:https://github.com/dotnet/extensions/blob/release/3.1/src/Hosting/Hosting/src/HostBuilder.cs
- ConfigureAppConfiguration:載入 appsettings.json文件和appsettings.[enviromentName].json文件的配置。可多次調用,累加結果。
- ConfigureContainer:配置DI容器,可多次調用,累加結果。
- ConfigureHostConfiguration:設置生成器自身的配置。 這將用於初始化 IHostEnvironment 以便稍后在生成過程中使用,設置內容根目錄。 可多次進行調用,並累加結果。
- ConfigureServices:為Host的容器添加服務,可多次調用,並累加結果。
- UseServiceProviderFactory:設置用於創建服務提供者的工廠
IHostBuilder同時提供了一些擴展方法,以下是部分擴展方法,擴展方法存在於類HostingHostBuilderExtensions中具體實現源碼可以參考:https://github.com/dotnet/extensions/blob/release/3.1/src/Hosting/Hosting/src/HostingHostBuilderExtensions.cs
- ConfigureLogging,該方法提供ILoggingBuilder對象對日志進行配置。
- ConfigureWebHost,該方法提供IWebHostBuilder對象對ASP.NET Core應用進行配置。后邊會詳細介紹
- UseConsoleLifetime,該方法使Host監聽Console的停止事件如:Ctrl+C 或 SIGTERM。
- UseContentRoot,指定Host的內容根目錄。
- UseDefaultServiceProvider,使用默認的IServiceProvider,並對其進行配置。
- UseEnvironment,指定Host的環境參數。
3、IHostBuilder.ConfigureWebHostDefaults()
ConfigureWebHostDefaults也是IHostBuilder的擴展方法,這里重點介紹下。上面介紹的配置主要是 Host 的通用配置,這些配置是各類應用所需的基礎功能,所以到目前為止我們的 Host 仍然不具備承載 Web 應用的能力,所以IHostBuilder的擴展方法ConfigureWebHostDefaults引入IWebHostBuilder實例,為 Host 添加一個“WebHost”應具備的核心功能,下面是ConfigureWebHostDefaults的默認配置:
- 使用 Kestrel 作為Web服務器,並載入配置文件中 Kestrel 的相關配置。
- 配置默認的靜態文件目錄。
- 添加HostFiltering中間件。
- 如果ASPNETCORE_FORWARDEDHEADERS_ENABLED為true則添加ForwardedHeaders中間件
- 配置為默認使用IIS集成模式。
具體試下,請參考ConfigureWebHostDefaults源碼:
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore; namespace Microsoft.Extensions.Hosting { /// <summary> /// Extension methods for configuring the <see cref="IHostBuilder" />. /// </summary> public static class GenericHostBuilderExtensions { /// <summary> /// Configures a <see cref="IHostBuilder" /> with defaults for hosting a web app. This should be called /// before application specific configuration to avoid it overwriting provided services, configuration sources, /// environments, content root, etc. /// </summary> /// <remarks> /// The following defaults are applied to the <see cref="IHostBuilder"/>: /// <list type="bullet"> /// <item><description>use Kestrel as the web server and configure it using the application's configuration providers</description></item> /// <item><description>configure <see cref="IWebHostEnvironment.WebRootFileProvider"/> to include static web assets from projects referenced by the entry assembly during development</description></item> /// <item><description>adds the HostFiltering middleware</description></item> /// <item><description>adds the ForwardedHeaders middleware if ASPNETCORE_FORWARDEDHEADERS_ENABLED=true,</description></item> /// <item><description>enable IIS integration</description></item> /// </list> /// </remarks> /// <param name="builder">The <see cref="IHostBuilder" /> instance to configure.</param> /// <param name="configure">The configure callback</param> /// <returns>A reference to the <paramref name="builder"/> after the operation has completed.</returns>
//IHostBuilder的擴展方法,用於接入IWebHostBuilder實例 public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure) { if (configure is null) { throw new ArgumentNullException(nameof(configure)); } //調用IhostBuilder擴展方法ConfigureWebHost,配置Host return builder.ConfigureWebHost(webHostBuilder => {
//調用方法ConfigureWebDefaults,配置webhost WebHost.ConfigureWebDefaults(webHostBuilder); configure(webHostBuilder); }); } } }
從上面源碼中可以看出,ConfigureWebHostDefaults中調用了ConfigureWebHost和ConfigureWebDefaults兩個方法。其中ConfigureWebHost是IHostBuilder的擴展方法,主要是對ASP.NET Core應用進行配置。ConfigureWebDefaults用來對WebHost做默認配置。先來看下ConfigureWebDefaults。
(1)ConfigureWebDefaults
ConfigureWebDefaults是WebHost的默認配置,ConfigureWebDefaults的源碼定義如下:
//IwebHostBuilder的基本配置
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
//配置默認的靜態文件目錄。
builder.ConfigureAppConfiguration((ctx, cb) => { if (ctx.HostingEnvironment.IsDevelopment()) { StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration); } }); //使用 Kestrel 作為Web服務器,並載入配置文件中 Kestrel 的相關配置。 builder.UseKestrel((builderContext, options) => { options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true); }) .ConfigureServices((hostingContext, services) => { // Fallback services.PostConfigure<HostFilteringOptions>(options => { if (options.AllowedHosts == null || options.AllowedHosts.Count == 0) { // "AllowedHosts": "localhost;127.0.0.1;[::1]" var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); // Fall back to "*" to disable. options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" }); } }); //添加HostFiltering中間件。 services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>( new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration)); services.AddTransient<IStartupFilter, HostFilteringStartupFilter>(); //如果ASPNETCORE_FORWARDEDHEADERS_ENABLED為true則添加ForwardedHeaders中間件 services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>(); services.AddTransient<IConfigureOptions<ForwardedHeadersOptions>, ForwardedHeadersOptionsSetup>(); services.AddRouting(); }) //配置為默認使用IIS集成模式。 .UseIIS() .UseIISIntegration(); }
從源碼中可以看出,IHostBuilder.ConfigureWebHostDefaults()相關的配置都集中在ConfigureWebDefaults方法中:
- 使用 Kestrel 作為Web服務器,並載入配置文件中 Kestrel 的相關配置。
- 配置默認的靜態文件目錄。
- 添加HostFiltering中間件。
- 如果ASPNETCORE_FORWARDEDHEADERS_ENABLED為true則添加ForwardedHeaders中間件
- 配置為默認使用IIS集成模式。
(2)ConfigureWebHost
ConfigureWebHost東東有點多,不在這里陳述了,具體在下面五、ConfigureWebHost中陳述。
4、IWebHostBuilder
上邊介紹了IHost/IHostBuilder體系的整個流程,其中IHostBuilder.ConfigureWebHostDefaults的參數類型為Action<IWebHostBuilder>,也介紹了IHostBuilder.ConfigureWebHostDefaults方法中對Action<IWebHostBuilder>類型的委托處理過程。那么傳入的Action<IWebHostBuilder>都做了什么事情呢?換句話說,通過HostBuilder.ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)方法引入了IWebHostBuilder,默認為 Host 添加一個“WebHost”應具備的核心功能。默認配置完成后我們可以根據IWebHostBuilder的擴展方法來進行自定義的配置了。
- Configure,指定一個“啟動”方法用於配置web應用,提供 IApplicationBuilder 對象用於配置。
- CaptureStartupErrors,設置是否要捕獲啟動時的錯誤。
- ConfigureKestrel,配置Kestrel的各種選項。
- ConfigureLogging,使用 ILoggingBuilder 進行 Logging 配置。
- PreferHostingUrls,指示主機是否應監聽 IWebHostBuilder上配置的Url,而不是 IServer上配置的Url。
- SuppressStatusMessages,設置是否禁止啟動狀態消息。
- UseConfiguration,使用指定的配置。
- UseContentRoot,設置Host內容目錄。
- UseDefaultServiceProvider,使用默認的服務提供程序,並進行配置。
- UseEnvironment,配置環境參數。
- UseHttpSys,將Http.sys指定為Host要使用的Web服務器。
- UseIIS,使用IISHttpServer替換Kestrel,並啟用進程內(w3wp.exe 或 iisexpress.exe)模式。
- UseIISIntegration,使用IIS作為Kestrel的反向代理服務器。
- UseKestrel,指定Kestrel作為Web服務器。
- UseServer,為Host指定Web服務器,IServer。
- UseShutdownTimeout,設置 Host 的關閉超時時間。
- UseSockets,指定及配置Kestrel 傳輸使用的 Sockets。
- UseStartup,指定用於配置Web應用的Startup類型。
- UseStaticWebAssets,配置 WebRootFileProvider 使用由引用項目和程序包定義的靜態Web資產文件。
- UseUrls,指定Host需要監聽的Url。
- UseWebRoot,指定 Web 服務器使用的 Root 目錄。
下面重點介紹IWebHostBuilder的擴展方法UseStartup。
5、Startup類
從上面的描述中,我們知道IWebHostBuilder提供了UseStartup擴展方法,用於配置Web相關的應用,返回IWebHostBuilder類型實例。Startup中默認定義了兩個方法ConfigureServices和Configure。
(1)Startup的構造函數
在構造函數中,可以注入以下服務來使用:
-
IWebHostEnvironment
-
IHostEnvironment
-
IConfiguration
代碼如下:
public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment,IHostEnvironment hostEnvironment) { Configuration = configuration; WebHostEnvironment = webHostEnvironment; HostEnvironment1 = hostEnvironment; } public IConfiguration Configuration { get; } public IWebHostEnvironment WebHostEnvironment { get; } public IHostEnvironment HostEnvironment1 { get; }
(2)ConfigureServices
ConfigureServices是可選方法,並且會在Configure之前被主機調用。在 ConfigureServices
中注冊服務,並通過依賴關系注入 (DI) 或 ApplicationServices 在整個應用中使用服務。該方法的作用就是將服務添加到服務容器,使這些服務可以在應用以及Configure方法中被使用。
public virtual void ConfigureServices (Microsoft.Extensions.DependencyInjection.IServiceCollection services);
這里重點提及IServiceCollection,先來看下IServiceCollection的命名空間:
Microsoft.Extensions.DependencyInjection
顧名思義,IServiceCollection和依賴注入有關,內部有DI容器相關的構造,具體原理這里不做詳解。下面列下IServiceCollection 中的部分方法和擴展方法:
AddScoped,添加服務,服務實例的生命周期為Scoped。 AddTransient,添加服務,服務實例的生命周期為Transient(每次被使用都創建新對象)。 AddSingleton,添加服務,服務實例的生命周期為單例。 AddMvc,添加所有MVC所需的服務。 AddMvcCore,僅添加核心必要的MVC所需的服務。 AddControllers,添加啟用Controller 所需要的服務,不包括View和Pages所需要的服務。 AddControllersWithViews,添加啟用 Controller 以及 Razor 頁面所需要的服務。 AddRazorPages,添加 Razor Pages 所需要的服務。 AddAntiforgery,添加防止CSRF攻擊的服務。 AddAuthentication,添加啟用Authentication中間件所需的服務。 AddAuthenticationCore,添加啟用Authentication中間件所需的核心服務。 AddAuthorization,添加啟用Authorization中間件所需的服務。 AddAuthorizationCore,添加啟用Authorization中間件所需的核心服務。 AddAuthorizationPolicyEvaluator,添加 Authorization 策略評估服務。 AddCertificateForwarding,添加CertificateForwarding中間件所需的服務。 AddConnections,添加 http://ASP.NET Core Connection Handlers 所需的服務。 AddCors,添加CORS中間件 所需的服務。 AddDataProtection,添加 http://ASP.NET Core Data Protection 所需的服務。 AddDirectoryBrowser,添加 DirectoryBrowser 中間件所需的服務。 AddDistributedMemoryCache,添加分布式緩沖服務IDistributedCache,默認的實現將緩沖保存在內存中,要實現實際上的分布式緩沖你需要提供一個保存緩存的實現(Redis或數據庫,如AddStackExchangeRedisCache和AddDistributedSqlServerCache)。 AddHealthChecks,添加HealthChecks中間件所需的服務。 AddHostedService,添加宿主服務,如持續運行的服務。 AddHostFiltering,添加HostFiltering中間件所需的服務。 AddHsts,添加HSTS中間件所需的服務。 AddHttpClient,添加IHttpClientFactory服務用於獲取在服務器端發起Http請求的HttpClient對象。 AddHttpContextAccessor,添加Http上下文訪問器服務,在例如Controller里有HttpContext屬性的地方優先使用HttpContext,但如果在一個自定義的服務里你就需要IHttpContextAccessor服務來獲取Http上下文。 AddHttpsRedirection,為HttpsRedirection中間件添加所需的服務。 AddIdentity,添加默認的身份系統,並制定 Role和User類型。 AddIdentityCore,添加默認身份執行的核心部分,並制定User類型。 AddLocalization,添加本地化服務。 AddLogging,添加日志服務。 AddMemoryCache,添加非分布式的內存緩存服務。 AddOptions,添加 Option 服務。 AddResponseCaching,為ResponseCaching中間件添加所需的服務。 AddResponseCompression,為ResponseCompression中間件添加所需的服務。 AddRouting,添加Routing中間件所需的服務。 AddSignalR,添加SignalR所需的服務。 AddSignalRCore,添加SignalR所需的核心服務。 AddServerSideBlazor,添加 Server-Side Blazor所需的服務。 AddWebEncoders,添加 HtmlEncoder,JavaScriptEncoder,UrlEncoder 三個服務。
(3)Configure
Configure 方法用於指定應用響應 HTTP 請求的方式,既配置Http請求處理管道。 具體方法是通過將中間件組件添加到 IApplicationBuilder 實例來配置請求處理管道。每個 Use 擴展方法會將一個或多個中間件組件添加到請求管道。 例如,UseStaticFiles 配置中間件提供靜態文件。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
這里重點提及IApplicationBuilder ,不做詳細介紹。下面介紹下IApplicationBuilder 常用的方法和擴展方法:
用於添加自定義中間件的方法: Use,添加一個中間件委托到應用的請求管道。 Run,添加一個請求管道的終點中間件,在該方法之后添加的中間件將不會被執行。 Map,該方法可根據請求路徑為請求處理管道創建分支。當請求來自指定路徑時運行分支請求處理管道。 UseWhen,基於提供的條件判斷委托來決定是否將中間件添加到請求處理管道。 MapWhen,基於提供的條件判斷委托來決定是否為請求處理管道創建分支。 UseMiddleware,將中間件類型添加到請求處理管道。 UseOwin,OWIN 為 Open Web Interface for .NET 的縮寫,該方法可以為請求處理管道添加符合OWIN規范的中間件。
添加內建中間件的擴展方法: UseRequestLocalization,添加RequestLocalizationMiddleware,用於自動根據請求信息設置 Culture 信息。 UseAuthentication,添加AuthenticationMiddleware用於啟用驗證功能。 UseAuthorization,添加AuthorizationMiddleware用於啟用授權功能。 UseCertificateForwarding,添加一個用於在請求中解碼證書信息,並能將證書設置到HttpContext.Connection.ClientCertificate的中間件。 UseBlazorFrameworkFiles,配置應用程序以從指定的路徑提供Blazor WebAssembly框架文件。 UseConcurrencyLimiter,添加一個並發限制中間件來顯示執行並發請求的數量。 UseCookiePolicy,添加Cookie Policy 中間件來啟用Cookie Policy 功能。 UseCors,添加一個 CORS 中間件來啟用跨域請求的功能。 UseDatabaseErrorPage,捕獲與數據相關的同步和異步的的異常,當此類異常發生時,一個帶有可能解決這些異常信息的HMTL返回頁面會被生成。(該方法已被標記為 Obsolete,在未來的版本中可能被移除)。 UseDefaultFiles,使用指定的配置進行默認文件映射。 UseDeveloperExceptionPage,該中間件在請求管道中捕獲同步和異步的異常並生成供開發者查看的HTML錯誤頁面。 UseDirectoryBrowser,使用指定的配置啟用目錄瀏覽功能。 UseEndpoints,添加 EndpointMiddleware 中間件,你可以通過IEndpointRouteBuilder對象進行配置,該中間件會執行與請求匹配的 Endpoint。 UseRouting,添加 EndpointRoutingMiddleware 中間件,該中間件的作用是將請求匹配到一個Endpoint。 UseExceptionHandler,使用指定的配置添加一個用於捕獲異常的中間件,該中間件可以捕獲異常,記錄日志,並在另外一個請求處理管道重新執行請求,如果返回已經開始則不會重新執行請求。 UseFileServer,使用指定的配置啟用所有的靜態文件中間件(除了 DirectoryBrowser 中間件)。 UseForwardedHeaders,該中間件將轉發的頭信息應用到當前請求對應的字段。 具體來說,按照慣例 HTTP 代理通過眾所周知的HTTP頭信息轉發來自客戶端的信息。 該中間件讀取這些頭信息並填寫HttpContext上的關聯字段。 UseHeaderPropagation,添加一個中間件用於收集頭信息並"傳播"到HttpClient。 UseHealthChecks,使用指定的配置啟用 HelthChecks 中間件。 UseHostFiltering,添加 HostFilter 中間件來過濾請求主機的頭信息,不符合的請求會被拒絕並返回 400。 UseHsts,添加中間件來啟用HSTS,該中間件會添加 Strict-Transport-Security 頭信息。 UseHttpMethodOverride,該中間件可以將 POST 請求重寫為其他 HTTP method, 重寫后的 HTTP method 可在頭信息或表單中指定(默認使用X-HTTP-Method-Override頭信息),這個方法通常會在客戶端只能發送GET或POST方法,但需要條用其他HTTP method時使用。 UseHttpsRedirection,添加一個中間件,可將HTTP的請求重定向到HTTPS。 UseMigrationsEndPoint,該中間件會檢查當前的數據庫是否有未被執行的 EF Core migrations,若有需要改中間件就會執行它,你可以在 MigrationsEndPointOptions.DefaultPath 來配置默認路徑。 UseMvc,添加MVC及相關功能的中間件。 UseMvcWithDefaultRoute,添加MVC及相關功能的中間件,並使用名為“default”的Route,其模板為'{controller=Home}/{action=Index}/{id?}'。 UseResponseCaching,添加 ResponseCachingMiddleware 中間件來啟用HTTP返回的緩存。 UseResponseCompression,添加一個可以動態壓縮HTTP 返回的中間件。 UseRewriter,該中間件檢查請求Url是否符合指定的規則和條件,並在滿足條件時修改HttpContext。 UseRouter,添加 RouterMiddleware, 可指定一個 IRouter對象或Action<IRouteBuilder>。 UseSession,使用指定的配置添加 SessionMiddleware 中間件以為應用添加會話功能。 UseStaticFiles,使用配置將指定的路徑作為靜態文件目錄。 UseStatusCodePages,添加可配置的 StatusCodePages 中間件,status code 為 400 到 599 之間並不帶 body 的返回將由該中間件處理 UseStatusCodePagesWithRedirects,將 StatusCodePages 中間件添加到管道中。 與之前的 UseStatusCodePages 方法相比,該方法可以指定 location url。 UseStatusCodePagesWithReExecute,將 StatusCodePages 中間件添加到管道中。 與之前的 UseStatusCodePages 方法相比,該方法會要求以另一個路徑重新執行請求處理管道。 UsePathBase,添加一個中間件,該中間件從請求路徑中提取指定的Path Base,並將其后綴到請求的Path Base中。 UseWebAssemblyDebugging,啟動WebAssembly調試,使Chromium調試工具可以調試Blazor WebAssembly應用。 UseWebSockets,使用指定的配置添加WebSockets中間件。 UseWelcomePage,使用指定的配置添加歡迎頁面中間件。
需要注意的是,為了讓上面某些中間件正常工作,你需要在 ConfigureServices 方法中添加這些中間件所需的服務。
六、ConfigureWebHost
先來看下IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)的源碼:從ConfigureWebHostDefaults說起:
using System; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore; namespace Microsoft.Extensions.Hosting { public static class GenericHostBuilderExtensions { public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure) { if (configure is null) { throw new ArgumentNullException(nameof(configure)); } return builder.ConfigureWebHost(webHostBuilder => { WebHost.ConfigureWebDefaults(webHostBuilder); configure(webHostBuilder); }); } } }
在上邊模塊中也說道IHostBuilder.ConfigureWebHostDefaults()方法中會做兩個處理,一個是WebHost.ConfigureWebDefaults(webHostBuilder)用於默認配置webhost。另一個就是接下來要介紹的IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)。先來看下IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)的源碼:
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Infrastructure; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Hosting { /// <summary> /// Contains extensions for an <see cref="IHostBuilder"/>. /// </summary> public static class GenericHostWebHostBuilderExtensions { /// <summary> /// Adds and configures an ASP.NET Core web application. /// </summary> /// <param name="builder">The <see cref="IHostBuilder"/> to add the <see cref="IWebHostBuilder"/> to.</param> /// <param name="configure">The delegate that configures the <see cref="IWebHostBuilder"/>.</param> /// <returns>The <see cref="IHostBuilder"/>.</returns> public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure) { if (configure is null) { throw new ArgumentNullException(nameof(configure)); } return builder.ConfigureWebHost(configure, _ => { }); } /// <summary> /// Adds and configures an ASP.NET Core web application. /// </summary> /// <param name="builder">The <see cref="IHostBuilder"/> to add the <see cref="IWebHostBuilder"/> to.</param> /// <param name="configure">The delegate that configures the <see cref="IWebHostBuilder"/>.</param> /// <param name="configureWebHostBuilder">The delegate that configures the <see cref="WebHostBuilderOptions"/>.</param> /// <returns>The <see cref="IHostBuilder"/>.</returns> public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder) { if (configure is null) { throw new ArgumentNullException(nameof(configure)); } if (configureWebHostBuilder is null) { throw new ArgumentNullException(nameof(configureWebHostBuilder)); } // Light up custom implementations namely ConfigureHostBuilder which throws. if (builder is ISupportsConfigureWebHost supportsConfigureWebHost) { return supportsConfigureWebHost.ConfigureWebHost(configure, configureWebHostBuilder); } var webHostBuilderOptions = new WebHostBuilderOptions(); configureWebHostBuilder(webHostBuilderOptions); var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions); configure(webhostBuilder); builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>()); return builder; } } }
可以看出,在ConfigureWebHost方法中,生成了類GenericWebHostBuilder的對象,同時也注入了GenericWebHostService服務。先來看下GenericWebHostBuilder定義:
public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options) { _builder = builder; var configBuilder = new ConfigurationBuilder() .AddInMemoryCollection(); if (!options.SuppressEnvironmentConfiguration) { configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_"); } _config = configBuilder.Build(); _builder.ConfigureHostConfiguration(config => { config.AddConfiguration(_config); // We do this super early but still late enough that we can process the configuration // wired up by calls to UseSetting ExecuteHostingStartups(); }); // IHostingStartup needs to be executed before any direct methods on the builder // so register these callbacks first _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)]; // Add the IHostingEnvironment and IApplicationLifetime from Microsoft.AspNetCore.Hosting services.AddSingleton(webhostContext.HostingEnvironment); #pragma warning disable CS0618 // Type or member is obsolete services.AddSingleton((AspNetCore.Hosting.IHostingEnvironment)webhostContext.HostingEnvironment); services.AddSingleton<IApplicationLifetime, GenericWebHostApplicationLifetime>(); #pragma warning restore CS0618 // Type or member is obsolete services.Configure<GenericWebHostServiceOptions>(options => { // Set the options options.WebHostOptions = webHostOptions; // Store and forward any startup errors options.HostingStartupExceptions = _hostingStartupErrors; }); // REVIEW: This is bad since we don't own this type. Anybody could add one of these and it would mess things up // We need to flow this differently services.TryAddSingleton(sp => new DiagnosticListener("Microsoft.AspNetCore")); services.TryAddSingleton<DiagnosticSource>(sp => sp.GetRequiredService<DiagnosticListener>()); services.TryAddSingleton(sp => new ActivitySource("Microsoft.AspNetCore")); services.TryAddSingleton(DistributedContextPropagator.Current); services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>(); services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>(); services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>(); // IMPORTANT: This needs to run *before* direct calls on the builder (like UseStartup) _hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services); // Support UseStartup(assemblyName) 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 => { // Throw if there was any errors initializing startup capture.Throw(); }; }); } } }); }
GenericWebHostBuilder的構造函數接收IHostBuilder作為參數,其實是把asp.netcore用到的服務和配置加載到IHostBuilder中。配置完成后調用UseStartup方法,UseStartup定義如下:
private void UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType, HostBuilderContext context, IServiceCollection services, object? instance = null) { var webHostBuilderContext = GetWebHostBuilderContext(context); var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)]; ExceptionDispatchInfo? startupError = null; ConfigureBuilder? configureBuilder = null; try { // We cannot support methods that return IServiceProvider as that is terminal and we need ConfigureServices to compose if (typeof(IStartup).IsAssignableFrom(startupType)) { throw new NotSupportedException($"{typeof(IStartup)} isn't supported"); } if (StartupLoader.HasConfigureServicesIServiceProviderDelegate(startupType, context.HostingEnvironment.EnvironmentName)) { throw new NotSupportedException($"ConfigureServices returning an {typeof(IServiceProvider)} isn't supported."); } instance ??= ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType); context.Properties[_startupKey] = instance; // Startup.ConfigureServices var configureServicesBuilder = StartupLoader.FindConfigureServicesDelegate(startupType, context.HostingEnvironment.EnvironmentName); var configureServices = configureServicesBuilder.Build(instance); configureServices(services); // REVIEW: We're doing this in the callback so that we have access to the hosting environment // Startup.ConfigureContainer var configureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName); if (configureContainerBuilder.MethodInfo != null) { var containerType = configureContainerBuilder.GetContainerType(); // Store the builder in the property bag _builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder; var actionType = typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType); // Get the private ConfigureContainer method on this type then close over the container type var configureCallback = typeof(GenericWebHostBuilder).GetMethod(nameof(ConfigureContainerImpl), BindingFlags.NonPublic | BindingFlags.Instance)! .MakeGenericMethod(containerType) .CreateDelegate(actionType, this); // _builder.ConfigureContainer<T>(ConfigureContainer); typeof(IHostBuilder).GetMethod(nameof(IHostBuilder.ConfigureContainer))! .MakeGenericMethod(containerType) .InvokeWithoutWrappingExceptions(_builder, new object[] { configureCallback }); } // Resolve Configure after calling ConfigureServices and ConfigureContainer configureBuilder = StartupLoader.FindConfigureDelegate(startupType, context.HostingEnvironment.EnvironmentName); } catch (Exception ex) when (webHostOptions.CaptureStartupErrors) { startupError = ExceptionDispatchInfo.Capture(ex); } // Startup.Configure services.Configure<GenericWebHostServiceOptions>(options => { options.ConfigureApplication = app => { // Throw if there was any errors initializing startup startupError?.Throw(); // Execute Startup.Configure if (instance != null && configureBuilder != null) { configureBuilder.Build(instance)(app); } }; }); }
從上面代碼中可以看出,先調用ConfigureServices方法注冊服務,再調用ConfigureContainer方法配置容器,最后調用Configure方法配置請求鏈。我們再看下GenericWebHostService類中的StartAsync方法,定義如下:
public async Task StartAsync(CancellationToken cancellationToken) { HostingEventSource.Log.HostStart(); var serverAddressesFeature = Server.Features.Get<IServerAddressesFeature>(); var addresses = serverAddressesFeature?.Addresses; if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0) { var urls = Configuration[WebHostDefaults.ServerUrlsKey]; if (!string.IsNullOrEmpty(urls)) { serverAddressesFeature!.PreferHostingUrls = WebHostUtilities.ParseBool(Configuration, WebHostDefaults.PreferHostingUrlsKey); foreach (var value in urls.Split(';', StringSplitOptions.RemoveEmptyEntries)) { addresses.Add(value); } } } RequestDelegate? application = null; try {
//1、從GenericWebHostServiceOptions的ConfigureApplication屬性中獲取RequestDelegate委托鏈
var configure = Options.ConfigureApplication; if (configure == null) { throw new InvalidOperationException($"No application configured. Please specify an application via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration."); } //2、調用IApplicationBuilderFactory接口的CreateBuilder創建IApplicationBuilder
var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);
//3、調用IStartupFilter的Configure方法配置整個委托鏈
foreach (var filter in StartupFilters.Reverse()) { configure = filter.Configure(configure); } configure(builder); //4、Build the request pipeline,調用IApplicationBuilder方法構建最終的RequestDelegate application = builder.Build(); } catch (Exception ex) { Logger.ApplicationError(ex); if (!Options.WebHostOptions.CaptureStartupErrors) { throw; } var showDetailedErrors = HostingEnvironment.IsDevelopment() || Options.WebHostOptions.DetailedErrors; application = ErrorPageBuilder.BuildErrorPageApplication(HostingEnvironment.ContentRootFileProvider, Logger, showDetailedErrors, ex); } //5、創建HostingApplication對象 var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory); //6、調用IServer接口的StartAsync開始處理請求 await Server.StartAsync(httpApplication, cancellationToken); if (addresses != null) { foreach (var address in addresses) { Log.ListeningOnAddress(LifetimeLogger, address); } } if (Logger.IsEnabled(LogLevel.Debug)) { foreach (var assembly in Options.WebHostOptions.GetFinalHostingStartupAssemblies()) { Log.StartupAssemblyLoaded(Logger, assembly); } } if (Options.HostingStartupExceptions != null) { foreach (var exception in Options.HostingStartupExceptions.InnerExceptions) { Logger.HostingStartupAssemblyError(exception); } } }
綜上所述,由於ASP.NET Core應用是由GenericWebHostService服務托管的,所以啟動應用程序本質上就是啟動這個托管服務。托管服務GenericWebHostService在啟動過程中的處理流程基本上體現在如下所示的StartAsync方法中,StartAsync方法中的調用流程如下:
-
從GenericWebHostServiceOptions的ConfigureApplication屬性中獲取RequestDelegate委托鏈
-
然后調用IApplicationBuilderFactory接口的CreateBuilder創建IApplicationBuilder
-
再調用IStartupFilter的Configure方法配置整個委托鏈
-
接着調用IApplicationBuilder方法構建最終的RequestDelegate
-
接着創建HostingApplication對象
-
最后調用IServer接口的StartAsync開始處理請求