注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄
本文會涉及部分 Host 相關的源碼,並會附上 github 源碼地址,不過為了降低篇幅,我會刪除一些不涉及的代碼。
為了方便,還是建議你將源碼(.net5)runtime 和 aspnetcore 下載下來,通過VS等工具閱讀
請耐心閱讀!
Generic Host & WebHost
在.NET Core 2.x時,ASP.NET Core 默認使用的是WebHost
:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
而到了.NET Core 3.x,ASP.NET Core 默認選擇使用Generic Host
:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
那么,為什么.NET團隊要將Web主機(Web Host)替換為通用主機(Generic Host)呢?
參考 What is the difference between Host and WebHost class in asp.net core
Generic Host
在.NET Core 2.1就已經存在了,並且它就是按照.NET Core未來版本的通用標准來實現的。不過由於當時的Generic Host
只能用於非HTTP工作負載,所以.NET Core 2.x仍然使用的是 Web Host
。不過到了.NET Core 3.x,Generic Host
已經可以同時支持HTTP和非HTTP工作負載了。
為什么要使用Generic Host
呢?那是因為Web Host
與HTTP請求緊密關聯,且用於Web應用。然而,隨着微服務和Docker的出現,.NET團隊認為需要一個更加通用的主機,不僅能夠服務於Web應用,還能服務於控制台等其他類型的應用。所以就實現了Generic Host
。
在我們的ASP.NET Core應用中,需要創建一個Generic Host
,並通過ConfigureWebHostDefaults
等擴展方法針對Web Host
進行配置。
所以,我們應該在所有類型的應用中始終使用通用主機。
因此,接下來咱們就聊一下通用主機。
Generic Host——通用主機
先上兩張Host的啟動流程圖:
請大家就着上面這張兩圖食用以下內容。
ConfigureXXX
在深入之前,大家要先了解一下ConfigureHostConfiguration
、ConfigureAppConfiguration
、ConfigureServices
等方法到底做了什么。其實,很簡單,就是將委托暫存到了一個臨時變量里。
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>>();
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;
}
}
Host.CreateDefaultBuilder(args)
public static class Host
{
public static IHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new HostBuilder();
// 將 Content Root(項目根目錄)設置為 Directory.GetCurrentDirectory (當前工作目錄)
builder.UseContentRoot(Directory.GetCurrentDirectory());
builder.ConfigureHostConfiguration(config =>
{
// 添加以 DOTNET_ 為前綴的環境變量(會將前綴刪除作為環境變量的Key)
config.AddEnvironmentVariables(prefix: "DOTNET_");
if (args != null)
{
// 添加命令行參數 args
config.AddCommandLine(args);
}
});
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
IHostEnvironment env = hostingContext.HostingEnvironment;
// 默認當配置發生更改時,重載配置
bool reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
// appsettings.json、appsettings.{Environment}.json
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
// 啟用 User Secrets(僅當運行在 Development 環境時)
if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
// 添加環境變量(未限定前綴)
// 目的是當應用(App)配置加載完畢后(注意是加載完畢后),允許讀取所有環境變量,且優先級更高
// 即若存在多個同名的環境變量,不帶前綴的比帶前綴的優先級更高
config.AddEnvironmentVariables();
if (args != null)
{
// 添加命令行參數 args
config.AddCommandLine(args);
}
})
.ConfigureLogging((hostingContext, logging) =>
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
}
// 添加 Logging 配置
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
if (isWindows)
{
// 在Windows平台上,添加 EventLogLoggerProvider
logging.AddEventLog();
}
logging.Configure(options =>
{
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
| ActivityTrackingOptions.TraceId
| ActivityTrackingOptions.ParentId;
});
})
.UseDefaultServiceProvider((context, options) =>
{
// 啟用范圍驗證 scope validation 和依賴關系驗證 dependency validation(僅當運行在 Development 環境時)
bool isDevelopment = context.HostingEnvironment.IsDevelopment();
options.ValidateScopes = isDevelopment;
options.ValidateOnBuild = isDevelopment;
});
return builder;
}
}
ConfigureWebHostDefaults
- 源碼請戳ConfigureWebHostDefaults、ConfigureWebHost、GenericWebHostBuilder、WebHost.ConfigureWebDefaults、UseStartup、GenericWebHostBuilder.UseStartup、GenericWebHostService
public static class GenericHostBuilderExtensions
{
public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
return builder.ConfigureWebHost(webHostBuilder =>
{
WebHost.ConfigureWebDefaults(webHostBuilder);
// 執行 UseStartup 等
configure(webHostBuilder);
});
}
}
public static class GenericHostWebHostBuilderExtensions
{
public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
return builder.ConfigureWebHost(configure, _ => { });
}
public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
{
var webHostBuilderOptions = new WebHostBuilderOptions();
configureWebHostBuilder(webHostBuilderOptions);
// 重點1: GenericWebHostBuilder
var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);
configure(webhostBuilder);
// 重點2:GenericWebHostService
builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
return builder;
}
}
上面這段代碼重點有兩個:
- 一個是
GenericWebHostBuilder
這個類,記住它,ConfigureWebHostDefaults
委托中的webBuilder
參數就是它! - 另一個是
GenericWebHostService
。
下面,我們先看一下GenericWebHostBuilder
的構造函數:
internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options)
{
_builder = builder;
var configBuilder = new ConfigurationBuilder()
.AddInMemoryCollection();
if (!options.SuppressEnvironmentConfiguration)
{
// 添加以 ASPNETCORE_ 為前綴的環境變量(會將前綴刪除作為環境變量的Key)
configBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_");
}
_config = configBuilder.Build();
_builder.ConfigureHostConfiguration(config =>
{
// 添加到主機(Host)配置
config.AddConfiguration(_config);
// 執行 HostingStartups,詳見下方的 ExecuteHostingStartups 方法
ExecuteHostingStartups();
});
_builder.ConfigureAppConfiguration((context, configurationBuilder) =>
{
// 在 ExecuteHostingStartups 方法中,該字段通常會被初始化
if (_hostingStartupWebHostBuilder != null)
{
var webhostContext = GetWebHostBuilderContext(context);
// 加載 HostingStartups 中添加的應用(App)配置
_hostingStartupWebHostBuilder.ConfigureAppConfiguration(webhostContext, configurationBuilder);
}
});
_builder.ConfigureServices((context, services) =>
{
var webhostContext = GetWebHostBuilderContext(context);
var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];
// 注冊 IWebHostEnvironment
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;
});
var listener = new DiagnosticListener("Microsoft.AspNetCore");
services.TryAddSingleton<DiagnosticListener>(listener);
services.TryAddSingleton<DiagnosticSource>(listener);
services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();
// 注冊 IHostingStartup 中配置的服務
_hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);
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();
};
});
}
}
});
}
private void ExecuteHostingStartups()
{
var webHostOptions = new WebHostOptions(_config, Assembly.GetEntryAssembly()?.GetName().Name);
if (webHostOptions.PreventHostingStartup)
{
return;
}
var exceptions = new List<Exception>();
// 注意這里對 _hostingStartupWebHostBuilder 進行了初始化
_hostingStartupWebHostBuilder = new HostingStartupWebHostBuilder(this);
// 從當前程序集和環境變量`ASPNETCORE_HOSTINGSTARTUPASSEMBLIES`配置的程序集列表(排除`ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES`中配置的程序集列表)中尋找特性`HostingStartupAttribute`,
// 並通過反射的方式創建特性所標識的`IHostingStartup`實現的實例,並調用其`Configure`方法。
foreach (var assemblyName in webHostOptions.GetFinalHostingStartupAssemblies().Distinct(StringComparer.OrdinalIgnoreCase))
{
try
{
var assembly = Assembly.Load(new AssemblyName(assemblyName));
foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>())
{
var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType);
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);
}
}
}
接着來看WebHost.ConfigureWebDefaults
:
public static class WebHost
{
internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((ctx, cb) =>
{
if (ctx.HostingEnvironment.IsDevelopment())
{
StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
}
});
// 將 Kestrel 服務器設置為 Web 服務器,並添加配置
builder.UseKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
})
.ConfigureServices((hostingContext, services) =>
{
// 配置主機過濾中間件(Host Filtering)
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>();
// 當環境變量 ASPNETCORE_FORWARDEDHEADERS_ENABLED 為 true 時,添加轉接頭中間件(Forwarded Headers)
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();
}
}
我們通常會在ConfigureWebHostDefaults
擴展方法的委托中調用UseStartup
來指定Startup類,下面我們就來看一下UseStartup
到底做了什么:將Startup.ConfigureServices
中要注冊的服務添加到ConfigureServices
的委托中
public static class WebHostBuilderExtensions
{
public static IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)]TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class
{
return hostBuilder.UseStartup(typeof(TStartup));
}
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, [DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType)
{
// ...刪除了一些代碼
// 會進入該條件分支
// 不知道為什么進入該分支?上面讓你牢記的 GenericWebHostBuilder 還記得嗎?快去看看它實現了哪些接口
if (hostBuilder is ISupportsStartup supportsStartup)
{
return supportsStartup.UseStartup(startupType);
}
// ...刪除了一些代碼
}
}
internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
public IWebHostBuilder UseStartup([DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType)
{
// 可以看到,雖然 UseStartup 可以調用多次,但是只有最后一次才有效
_startupObject = startupType;
// 將 Startup.ConfigureServices 中要注冊的服務添加進來
// 好了,暫時看到這里就ok了
_builder.ConfigureServices((context, services) =>
{
if (object.ReferenceEquals(_startupObject, startupType))
{
UseStartup(startupType, context, services);
}
});
return this;
}
}
最后,看一下上面提到的第二個重點GenericWebHostService
:用於后續Run
方法時執行Configure
(包括StartupFilters.Configure
、Startup.Configure
等)
internal class GenericWebHostService : IHostedService
{
// 構造函數注入
public GenericWebHostServiceOptions Options { get; }
// 構造函數注入
public IEnumerable<IStartupFilter> StartupFilters { get; }
public async Task StartAsync(CancellationToken cancellationToken)
{
// ...刪除了一些代碼
RequestDelegate application = null;
try
{
// 這里取到了 Startup.Configure
// 可能你不知道為什么這里可以取到,別着急,文章后面會為你解釋的
Action<IApplicationBuilder> configure = Options.ConfigureApplication;
// 要求 Startup 必須包含 Configure 方法,或必須調用 IWebHostBuilder.Configure
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.");
}
var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);
// 注意:這里來執行 StartupFilters.Configure 與 Startup.Configure
// 將 Startup.Configure 與 StartupFilters.Configure 連接成中間件管道
// 為什么 Reverse?因為要先執行 StartupFilters.Configure,最后才執行 Startup.Configure,
// 所以用類似鏈條的方式,從尾巴開始向頭部牽手,這樣,最終得到的 configure 指向的就是頭部
// 當執行 configure 時,就可以從頭部流轉到尾巴
foreach (var filter in StartupFilters.Reverse())
{
configure = filter.Configure(configure);
}
// 執行 Configure 方法
configure(builder);
// Build HTTP 請求管道
application = builder.Build();
}
catch (Exception ex)
{
Logger.ApplicationError(ex);
if (!Options.WebHostOptions.CaptureStartupErrors)
{
throw;
}
application = BuildErrorPageApplication(ex);
}
var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, HttpContextFactory);
await Server.StartAsync(httpApplication, cancellationToken);
// ...刪除了一些代碼
}
}
Build
- 源碼請戳Build
public class HostBuilder : IHostBuilder
{
public IHost Build()
{
// 加載主機(Host)配置
BuildHostConfiguration();
// 實例化 HostingEnvironment
CreateHostingEnvironment();
// 實例化 HostBuilderContext
CreateHostBuilderContext();
// 加載應用(App)配置
BuildAppConfiguration();
// 注冊服務並創建 Service Provider
CreateServiceProvider();
// 生成 IHost 實例並返回
return _appServices.GetRequiredService<IHost>();
}
}
BuildHostConfiguration
public class HostBuilder : IHostBuilder
{
private void BuildHostConfiguration()
{
IConfigurationBuilder configBuilder = new ConfigurationBuilder()
.AddInMemoryCollection();
// 加載主機(Host)配置(同時會執行上面所說的 IHostingStartup.Configure)
foreach (Action<IConfigurationBuilder> buildAction in _configureHostConfigActions)
{
buildAction(configBuilder);
}
_hostConfiguration = configBuilder.Build();
}
}
CreateHostingEnvironment
public class HostBuilder : IHostBuilder
{
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 = new PhysicalFileProvider(_hostingEnvironment.ContentRootPath);
}
}
CreateHostBuilderContext
public class HostBuilder : IHostBuilder
{
private void CreateHostBuilderContext()
{
_hostBuilderContext = new HostBuilderContext(Properties)
{
HostingEnvironment = _hostingEnvironment,
Configuration = _hostConfiguration
};
}
}
BuildAppConfiguration
public class HostBuilder : IHostBuilder
{
private void BuildAppConfiguration()
{
IConfigurationBuilder configBuilder = new ConfigurationBuilder()
.SetBasePath(_hostingEnvironment.ContentRootPath)
.AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true);
foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
{
buildAction(_hostBuilderContext, configBuilder);
}
_appConfiguration = configBuilder.Build();
_hostBuilderContext.Configuration = _appConfiguration;
}
}
CreateServiceProvider
public class HostBuilder : IHostBuilder
{
private void CreateServiceProvider()
{
var services = new ServiceCollection();
services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
// 注冊 IHostEnvironment
services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
// 注冊 HostBuilderContext
services.AddSingleton(_hostBuilderContext);
// 注冊 IConfiguration,所以能在 Startup 中進行構造函數注入
services.AddSingleton(_ => _appConfiguration);
services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
// 注意這里注冊了 IHostLifetime 服務的實例 ConsoleLifetime
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
// 注冊 IHost 實例
services.AddSingleton<IHost, Internal.Host>();
services.AddOptions();
services.AddLogging();
// 執行 ConfigureServices 方法中的委托進行服務注冊
// 包括使用擴展方法 ConfigureServices、 Startup.ConfigureServices 等設置的委托
foreach (Action<HostBuilderContext, IServiceCollection> configureServicesAction in _configureServicesActions)
{
configureServicesAction(_hostBuilderContext, services);
}
object containerBuilder = _serviceProviderFactory.CreateBuilder(services);
// 加載容器配置
foreach (IConfigureContainerAdapter containerAction in _configureContainerActions)
{
containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
}
// 創建 Service Provider
_appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);
if (_appServices == null)
{
throw new InvalidOperationException($"The IServiceProviderFactory returned a null IServiceProvider.");
}
_ = _appServices.GetService<IConfiguration>();
}
}
Run
- 源碼請戳Run、StartAsync
public static class HostingAbstractionsHostExtensions
{
public static void Run(this IHost host)
{
host.RunAsync().GetAwaiter().GetResult();
}
public static async Task RunAsync(this IHost host, CancellationToken token = default)
{
try
{
await host.StartAsync(token).ConfigureAwait(false);
await host.WaitForShutdownAsync(token).ConfigureAwait(false);
}
finally
{
if (host is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
host.Dispose();
}
}
}
}
StartAsync
internal class Host : IHost, IAsyncDisposable
{
public async Task StartAsync(CancellationToken cancellationToken = default)
{
_logger.Starting();
using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
CancellationToken combinedCancellationToken = combinedCancellationTokenSource.Token;
// _hostLifetime 是在構造函數注入的
// 還記得嗎?在上面的 CreateServiceProvider 方法中,注入了該服務的默認實例 ConsoleLifetime,在下方你可以看到 ConsoleLifetime 的部分實現
await _hostLifetime.WaitForStartAsync(combinedCancellationToken).ConfigureAwait(false);
combinedCancellationToken.ThrowIfCancellationRequested();
// 這里面就包含我們上面提到的重點 GenericWebHostService
_hostedServices = Services.GetService<IEnumerable<IHostedService>>();
foreach (IHostedService hostedService in _hostedServices)
{
// 激活 IHostedService.StartAsync
await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
}
// 激活 IHostApplicationLifetime.Started
_applicationLifetime.NotifyStarted();
_logger.Started();
}
}
public class ConsoleLifetime : IHostLifetime, IDisposable
{
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
// ...刪除了一些代碼
// 注冊了程序退出回調
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
// 注冊了 Ctrl + C 回調(這下你知道為啥執行了 Ctrl + C 程序就退出了吧?)
Console.CancelKeyPress += OnCancelKeyPress;
// 立即啟動 Console applications
return Task.CompletedTask;
}
private void OnProcessExit(object sender, EventArgs e)
{
ApplicationLifetime.StopApplication();
if (!_shutdownBlock.WaitOne(HostOptions.ShutdownTimeout))
{
Logger.LogInformation("Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks.");
}
_shutdownBlock.WaitOne();
System.Environment.ExitCode = 0;
}
private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
e.Cancel = true;
ApplicationLifetime.StopApplication();
}
}
WaitForShutdownAsync
public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default)
{
IHostApplicationLifetime 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);
// 正是由於此處,程序 Run 起來后,在 applicationLifetime.ApplicationStopping 被觸發前,能夠一直保持運行狀態
await waitForStop.Task.ConfigureAwait(false);
await host.StopAsync(CancellationToken.None).ConfigureAwait(false);
}
Host的整個啟動流程,就差不多說完了。
服務接口
接下來咱們就從上面注冊的默認服務中,挑幾個詳細聊一下。
IHostedService
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
IHostedService
用於在應用啟動和關閉時,執行一些額外的操作。可以添加多個,都會被執行。
代碼實例請查看接下來的IHostApplicationLifetime
。
IHostApplicationLifetime
通過該服務,可以針對程序啟動后、正常關閉前和正常關閉后指定要執行的操作。
該服務生命周期被注冊為Singleton
,所以可以將該服務注冊到任何類中。
該服務所擁有的3個屬性ApplicationStarted
、ApplicationStopping
和ApplicationStopped
類型均為CancellationToken
,當程序運行到某個生命周期節點時,就會觸發對應屬性的Cancel
命令,進而執行注冊的委托。
該服務的默認注冊實現是Microsoft.Extensions.Hosting.Internal.ApplicationLifetime
,代碼很簡單,就是在程序啟動后、正常關閉前和正常關閉后觸發對應的3個屬性。
另外,該服務還擁有StopApplication
方法,用於請求停止當前應用程序的運行。
需要注意的是,IHostApplicationLifetime
不允許注冊自己的實現,只能使用微軟提供的默認實現。
接下來就舉個例子吧(配合IHostedService
):
/// <summary>
/// 通用主機服務的生命周期事件
/// </summary>
public class LifetimeEventsHostedService : IHostedService
{
private readonly ILogger _logger;
private readonly IHostApplicationLifetime _appLifetime;
public LifetimeEventsHostedService(
ILogger<LifetimeEventsHostedService> logger,
IHostApplicationLifetime appLifetime)
{
_logger = logger;
_appLifetime = appLifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnStarted()
{
_logger.LogInformation("App Started");
}
private void OnStopping()
{
_logger.LogInformation("App Stopping");
}
private void OnStopped()
{
_logger.LogInformation("App Stopped");
}
}
// 注入服務
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<LifetimeEventsHostedService>();
}
IHostLifetime
該服務生命周期被注冊為Singleton
,以最后一個注冊的實現為准。
默認注冊的實現是Microsoft.Extensions.Hosting.Internal.ConsoleLifetime
,該實現:
- 監聽
Ctrl
+C
指令,並調用IHostApplicationLifetime.StopApplication
方法來關閉程序。 - 解除
RunAsync
和WaitForShutdownAsync
等擴展方法的阻塞調用。
IHostEnvironment & IWebHostEnvironment
這兩個服務生命周期均被注冊為Singleton
。
通過IHostEnvironment
,我們可以獲取到:
- ApplicationName
- EnvironmentName
- ContentRootPath
- ContentRootFileProvider
IWebHostEnvironment
繼承於IHostEnvironment
,在其基礎上,又增加了:
- WebRootPath
- WebRootFileProvider
在 [01] Startup 中,我留下了一個問題,就是Startup
類的構造函數中,IHostEnvironment
和IWebHostEnvironment
是同一個實例,這是為什么呢?接下來就來解開大家的疑惑:
或許你還會疑惑,明明我們使用的 Service Provider 要在 Startup.ConfigureServices 執行完畢后,才會被創建,為啥 Startup 的構造函數中卻還能進行依賴注入呢?下面也會解答你得疑惑!
上面解讀UseStartup
時,看到一半就停下了,那是因為我要在這里和大家一起來更深入的理解:
- 源碼請戳UseStartup
internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
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
{
// 創建 Startup 實例
// 注意,這里使用的 Service Provider 是 HostServiceProvider (不是我們經常使用的那個 service provider,此時它還沒被創建),解決問題的核心就在這個類里面
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);
// 調用 Startup.ConfigureServices
configureServices(services);
// 將 Startup.ConfigureContainer 添加到 IHostBuilder.ConfigureContainer 中
// 這個方法熟悉嗎?你在使用 Autofac 的時候是不是會有一個這個方法?
var configureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName);
if (configureContainerBuilder.MethodInfo != null)
{
var containerType = configureContainerBuilder.GetContainerType();
_builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder;
var actionType = typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType);
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 });
}
// 注意,當執行完 ConfigureServices 和 ConfigureContainer 方法后,
// 會將 Configure 方法解析出來
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();
// 執行 Startup.Configure
// 這下,你明白為什么之前可以通過 Options.ConfigureApplication 獲取到 Startup.Configure 了吧?
if (instance != null && configureBuilder != null)
{
configureBuilder.Build(instance)(app);
}
};
});
}
private class HostServiceProvider : IServiceProvider
{
private readonly WebHostBuilderContext _context;
public HostServiceProvider(WebHostBuilderContext context)
{
_context = context;
}
// 該 ServieceProvider 中,僅提供了 IConfiguration、IHostEnvironment、IWebHostEnvironment 三種服務
// 所以,在Startup的構造函數中,只能注入這三種服務
public object GetService(Type serviceType)
{
// 很顯然,IWebHostEnvironment 和 IHostEnvironment 返回的都是同一實例
if (serviceType == typeof(Microsoft.Extensions.Hosting.IHostingEnvironment)
|| serviceType == typeof(Microsoft.AspNetCore.Hosting.IHostingEnvironment)
|| serviceType == typeof(IWebHostEnvironment)
|| serviceType == typeof(IHostEnvironment)
)
{
return _context.HostingEnvironment;
}
if (serviceType == typeof(IConfiguration))
{
return _context.Configuration;
}
return null;
}
}
}
還有一個要點是:在Startup
構造方法中注入的IHostEnvironment
和在Startup.Configure
等方法中通過常規 Service Provider 解析出來的IHostEnvironment
實例是不同的。 原因就是Startup
構造方法中的依賴注入 Service Provider 和后面我們用的不是同一個,它們解析的服務實例也不是同一個。
配置
ConfigureHostConfiguration—主機配置
我們可以在HostBuilder.ConfigureHostConfiguration
方法中添加主機配置,多次調用該方法也沒關系,最終會將這些配置聚合起來
.ConfigureHostConfiguration(config =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddEnvironmentVariables("MYAPPENVPREFIX_");
})
我們可以通過IHostEnvironment
服務實現的屬性來獲取部分主機配置。
還可以在HostBuilder.ConfigureAppConfiguration
方法中調用HostBuilderContext.Configuration
來獲取主機配置。在執行完ConfigureAppConfiguration
中的委托之后,在其他委托中通過HostBuilderContext.Configuration
獲取的就不再針對主機的配置了,而是針對應用的配置。
ConfigureAppConfiguration—應用配置
通過HostBuilder.ConfigureAppConfiguration
方法,可以添加應用配置。同樣的,該方法也可以多次進行調用,最終會對配置進行聚合。
.ConfigureAppConfiguration((hostingContext, config) =>
{
// 獲取主機配置
var hostingConfig = hostingContext.Configuration;
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("mysettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"mysettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
})
結語
-
一些常用的配置項解釋可以訪問官方文檔
-
由於默認只能在
Development
環境時才會啟用范圍驗證(scope validation)和依賴關系驗證(dependency validation),所以,如果想要手動進行配置,可以通過UseDefaultServiceProvider
(其實默認邏輯的源碼里面也是使用的該擴展方法)
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = true;
options.ValidateOnBuild = true;
});
相信你讀完本篇文章,一定對ASP.NET Core主機的啟動流程,有了新的認識!