一、承载系统
1、托管服务
在业务场景中经常需要后台服务不停的或定时处理一些任务,在 asp.net中会使用windows服务来处理,在 asp.net core中可以使用托管服务来实现,托管服务是一个类,具有实现IHostService接口的后台任务逻辑。托管服务必须实现IHostedService接口。抽象类BackgroundService是IHostedService的派生类,实现了IHostService接口定义的方法,因此自定义的托管服务类也可以继承BackgroundService实现ExecuteAsync()方法即可。
(1)自定义托管服务类直接继承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);
}
}
(2)使用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方法也随之被调用。
2、IHostService
托管服务必须实现IHostedService接口:
public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); }
-
StartAsync:当应用程序主机准备好启动服务时触发。
-
StartAsync:当应用程序主机执行正常关闭时触发。
3、承载系统和托管服务的关系
利用承载系统可以将任意一个或者多个长时间运行的服务寄宿或者承载于托管进程中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载。
一个ASP.NET Core应用本质上是一个需要长时间运行的服务,开启这个服务是为了启动一个网络监听器。当监听到抵达的HTTP请求之后,该监听器会将请求传递给应用提供的管道进行处理。管道完成了对请求处理之后会生成HTTP响应,并通过监听器返回客户端。因此ASP.NET Core也提供了以IHostBuilder/IHost为核心的承载服务。
二、netcore中的承载系统
1、netcore中承载系统模型
服务承载模型主要由三个核心对象组成:多个通过IHostedService接口表示的服务被承载于通过IHost接口表示的宿主上,IHostBuilder接口表示IHost对象的构建者。
//定义托管服务GenericWebHostService
internal class GenericWebHostService : IHostedService{}
//GenericWebHostService 托管服务承载于Ihost宿主。
_hostBuilder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>()).Build().Run();
1、IHost
IHost位于.NET Runtime 而不是ASP.NET Core 中。通过IHostedService接口表示的托管服务GenericWebHostService 最终被承载于通过IHost接口表示的宿主上,即netcore中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)
2、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()
public static IHostBuilder CreateDefaultBuilder(string[] args);//返回IHostBuilder
使用预配置默认值初始化 HostBuilder 类的新实例。以下默认值将应用于返回的 HostBuilder :
-
将设置 内容根目录ContentRootPath 的结果为GetCurrentDirectory()
-
从提供的命令行参数加载主机
-
载入 appsettings.json文件和appsettings.[enviromentName].json文件的配置
-
开发模式下,保存项目程序集到用户机密
-
从环境变量中加载应用
-
从提供的命令行参数加载应用
-
配置 ILoggerFactory 以记录到控制台、调试和事件源输出
-
开发模式下,在DI容器上启用作用域验证 EnvironmentName
Host.CreateDefaultBuilder()源码如下:
public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder = new WebHostBuilder(); //将设置 内容根目录ContentRootPath 的结果为GetCurrentDirectory() if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey))) { builder.UseContentRoot(Directory.GetCurrentDirectory()); } //从提供的命令行参数加载主机 if (args != null) { builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build()); } //载入 appsettings.json文件和appsettings.[enviromentName].json文件的配置 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()) { 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, loggingBuilder) => { loggingBuilder.Configure(options => { options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId | ActivityTrackingOptions.ParentId; }); loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); loggingBuilder.AddConsole(); loggingBuilder.AddDebug(); loggingBuilder.AddEventSourceLogger(); }). //开发模式下,在DI容器上启用作用域验证 EnvironmentName UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }); ConfigureWebDefaults(builder); return builder; }
(2)IHostBuilder方法和扩展方法
Host.CreateDefaultBuilder(args)返回了IHostBuilder对象,并做了一些默认初始化的配置。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); } }
- ConfigureAppConfiguration:载入 appsettings.json文件和appsettings.[enviromentName].json文件的配置。可多次调用,累加结果。ConfigureAppConfiguration源码如下:
public static IWebHostBuilder UseStaticWebAssets(this IWebHostBuilder builder) { builder.ConfigureAppConfiguration((context, configBuilder) => { StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration); }); return builder; }
public static void UseStaticWebAssets(IWebHostEnvironment environment, IConfiguration configuration) { using var manifest = ResolveManifest(environment, configuration); if (manifest != null) { UseStaticWebAssetsCore(environment, manifest); } } internal static void UseStaticWebAssetsCore(IWebHostEnvironment environment, Stream manifest) { var webRootFileProvider = environment.WebRootFileProvider; var additionalFiles = StaticWebAssetsReader.Parse(manifest) .Select(cr => new StaticWebAssetsFileProvider(cr.BasePath, cr.Path)) .OfType<IFileProvider>() // Upcast so we can insert on the resulting list. .ToList(); if (additionalFiles.Count == 0) { return; } else { additionalFiles.Insert(0, webRootFileProvider); environment.WebRootFileProvider = new CompositeFileProvider(additionalFiles); } } internal static Stream? ResolveManifest(IWebHostEnvironment environment, IConfiguration configuration) { try { var manifestPath = configuration.GetValue<string>(WebHostDefaults.StaticWebAssetsKey); var filePath = manifestPath ?? ResolveRelativeToAssembly(environment); if (filePath != null && File.Exists(filePath)) { return File.OpenRead(filePath); } else { // A missing manifest might simply mean that the feature is not enabled, so we simply // return early. Misconfigurations will be uncommon given that the entire process is automated // at build time. return null; } } catch { return null; } }
- ConfigureContainer:配置DI容器,可多次调用,累加结果。源码如下:
public Action<MyContainer> ConfigureContainer(Action<MyContainer> next) { return services => { services.Services.TryAddSingleton(new ServiceBefore { Message = $"ConfigureContainerFilter Before {AdditionalData}" }); next(services); // Ensures we can always override. if (OverrideAfterService) { services.Services.AddSingleton(new ServiceAfter { Message = $"ConfigureContainerFilter After {AdditionalData}" }); } else { services.Services.TryAddSingleton(new ServiceAfter { Message = $"ConfigureContainerFilter After {AdditionalData}" }); } }; }
- ConfigureHostConfiguration:设置生成器自身的配置。 这将用于初始化 IHostEnvironment 以便稍后在生成过程中使用,设置内容根目录。 可多次进行调用,并累加结果。
- ConfigureServices:为Host的容器添加服务,可多次调用,并累加结果。
- UseServiceProviderFactory:设置用于创建服务提供者的工厂
IHostBuilder同时提供了一些扩展方法,以下是部分扩展方法:
- 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 应用的能力,ConfigureWebHostDefaults方法为 Host 添加一个“WebHost”应具备的核心功能:
- 使用 Kestrel 作为Web服务器,并载入配置文件中 Kestrel 的相关配置。
- 配置默认的静态文件目录。
- 添加HostFiltering中间件。
- 如果ASPNETCORE_FORWARDEDHEADERS_ENABLED为true则添加ForwardedHeaders中间件
- 配置为默认使用IIS集成模式。
ConfigureWebHostDefaults源码如下:
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); }); }
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中间件。 // Change notification services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>( new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration)); services.AddTransient<IStartupFilter, HostFilteringStartupFilter>(); //如果ASPNETCORE_FORWARDEDHEADERS_ENABLED为true则添加ForwardedHeaders中间件 if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase)) { services.Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; // Only loopback proxies are allowed by default. Clear that restriction because forwarders are // being enabled by explicit configuration. options.KnownNetworks.Clear(); options.KnownProxies.Clear(); }); services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>(); } services.AddRouting(); }) //配置为默认使用IIS集成模式。 .UseIIS() .UseIISIntegration(); }
(4)IWebHostBuilder
默认配置完成后,我们可以开始在ConfigureWebHostDefaults(Action<IWebHostBuilder> configure)方法中使用IWebHostBuilder对象来进行自定义的配置了。
IWebHostBuilder 对象的基础配置方法包括:
- ConfigureAppConfiguration,配置 IConfigurationBuilder对象,用于在之后生成 IConfiguration。
- GetSetting,获取配置项的值。
- UseSetting,设置配置。
- ConfigureServices,添加和配置服务。
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 目录。
完成 IHostBuilder 和 IWebHostBuilder 之后,就开始进入 Startup 类。
正常流程如下:
- 通过下面方法创建一个HostBuilder对象:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseKestrel(); webBuilder.UseStartup<Startup>(); });
-
在调用Build方法之前,可以调用IHostBuilder接口的ConfigureServices方法将通过IHostService实现的托管服务进行注册,并将生命周期模式设置成Singleton。如下:
var hostBuilder = new HostBuilder().ConfigureServices((hostContext, services) => { services.AddSingleton<IHostedService, TimedHostedService>(); });
- 除了采用普通的依赖服务注册方式,针对IHostedService服务的注册还可以调用IServiceCollection接口的AddHostedService<THostedService>扩展方法来完成,如下:
var hostBuilder = new HostBuilder().ConfigureServices((hostContext, services) => { services.AddHostedService<TimedHostedService>(); });
- 调用Build方法来提供作为宿主的IHost对象。
var host=hostBuilder.Build();
- 最后调用Run方法启动通过IHost对象表示的承载服务宿主,进而启动由它承载的TimedHostedService服务
(2)详述
- Host.CreateDefaultBuilder()
- 设置Host 的 ContentRootPath 为当前目录。
- 载入 DOTNET_ 前缀的环境变量配置。
- 载入命令行参数 agrs 的配置。
- 载入 appsettings.json文件和appsettings.[enviromentName].json文件的配置。
- 当 EnvironmentName 为 Development 时载入 User Secrets。
3、应用生命周期
承载宿主Host的生命周期和接口IHostApplicationLifetime有关。
public interface IHostApplicationLifetime { CancellationToken ApplicationStarted { get; } CancellationToken ApplicationStopping { get; } CancellationToken ApplicationStopped { get; } void StopApplication(); }
该接口除了提供了三个CancellationToken类型的属性来检测应用何时开启与关闭之外,还提供了一个StopApplication来关闭应用程序。ApplicationLifetime类型是对IHostApplicationLifetime接口的默认实现。
public class ApplicationLifetime : IHostApplicationLifetime { ....... }
4、Run扩展方法
在前面演示的实例中,在利用HostBuilder对象构建出IHost对象之后,我们并没有调用其StartAsync方法启动它,而是另一个名为Run的扩展方法。Run方法涉及到服务承载应用生命周期的管理,如果我们调用IHost对象的扩展方法Run,它会在内部调用StartAsync方法,接下来它会持续等待下去直到接收到应用被关闭的通知。当IHost对象利用IHostApplicationLifetime服务接收到关于应用关闭的通知后,它会调用自身的StopAsync方法,针对Run方法的调用此时才会返回。启动IHost对象直到应用关闭的实现体现在WaitForShutdownAsync扩展方法上:
public static class HostingAbstractionsHostExtensions { 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(state => { var tcs = (TaskCompletionSource<object>)state; tcs.TrySetResult(null); }, waitForStop); await waitForStop.Task; await host.StopAsync(); } }