理解ASP.NET Core - [04] Host
注:本文隶属于《理解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主机的启动流程,有了新的认识!
理解ASP.NET Core - [04] Host的更多相关文章
- 目录-理解ASP.NET Core
<理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...
- 理解ASP.NET Core - [01] Startup
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...
- 理解ASP.NET Core - [03] Dependency Injection
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接 ...
- 理解ASP.NET Core - 配置(Configuration)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: 文件配置提供程 ...
- 理解ASP.NET Core - 文件服务器(File Server)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 提供静态文件 静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(C ...
- 理解ASP.NET Core - 日志(Logging)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 快速上手 添加日志提供程序 在文章主机(Host)中,讲到Host.CreateDefault ...
- 理解 ASP.NET Core: 处理管道
理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...
- 理解ASP.NET Core - [02] Middleware
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 中间件 先借用微软官方文档的一张图: 可以看到,中间件实际上是一种配置在HTTP请求管道中,用 ...
- 理解ASP.NET Core - 选项(Options)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Options绑定 上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊O ...
随机推荐
- 在Linux系统上查找文件
Find命令 格式:find <指定搜索范围> <指定条件> <指定动作> 其中搜索范围是一个目录名,指定条件包括文件名.文件属性(修改时间所属用户等).所在位置特 ...
- 授予mysql的其他用户数据库的使用权限
场景:不同的开发人员有不同的数据库的权限:也可适用于外包公司不同的开发权限. root用户登录数据库,命令行执行下面语句即可. grant select,delete,update,create,dr ...
- ACM学习笔记:二叉堆
title : 堆 date : 2021-8-3 tags : ACM,数据结构 什么是堆 堆是一棵具有特定性质的二叉树,堆的基本要求是堆中所有结点的值必须大于等于(或小于等于)其孩子结点的值,这也 ...
- MySQL-09-SQL执行计划
SQL执行计划获取及分析 介绍 (1)获取到的是优化器选择完成的,他认为代价最小的执行计划. 作用: 语句执行前,先看执行计划信息,可以有效的防止性能较差的语句带来的性能问题. 如果业务中出现了慢语句 ...
- 【笔记】numpy.array基础(2)
numpy数组的基本操作 以几个数组为例 使用ndim可以显示出是几维数组 使用shape可以查看元素维度数 使用size可以查看元素个数 对一维数组进行数据访问 对多维数组进行数据访问 切片访问,使 ...
- 使用sklearn中的fetch_mldata的错误情况以及可能可行的解决方法
使用sklearn中的fetch_mldata的错误情况以及可能可行的解决方法 在notebook使用的时候出现了报错 from sklearn.datasets import fetch_mldat ...
- ACL的配置
一.实验拓扑 实验要求: 二.实验编址 三.实验步骤: 1.启动设备(全选) 2.配置端口IP R1: R2: R3: R4: 2.搭建OSPF网络: R1: R2: R3: R4: 4.配置ACL控 ...
- Blazor Server 应用程序中进行 HTTP 请求
翻译自 Waqas Anwar 2021年5月4日的文章 <Making HTTP Requests in Blazor Server Apps> [1] Blazor Server 应用 ...
- 教你IO流来便利电脑磁盘所有文件,把图片放到一个文件夹里(会发现什么不可告人的密码)
一.需求 我要把C盘下面的所有图片都拿出来,放到一个新文件夹中.今天小编一身正气,看看有没有什么意外发现!!学会看看自己的盘,悄悄的哦!!! 二.代码展示(运行时间可能有点长) import java ...
- C#串口通信SeriPort 电表DLT645 RS234/RS485
难受,三个多月前有一个电表电量监控的项目.做完了就没再管了.今天有需求需要改一些地方,但是....我想不起来干了啥,怎么干的啦.真的完全忘了.....项目名称叫啥都忘了.找了半天 不知道有没有和我一样 ...