写在前面

停了近一个月的技术博客,随着正式脱离996的魔窟,接下来也正式恢复了。本文从源码角度进一步讨论.NET Core 3.0
中关于Host扩展的一些技术点,主要讨论Long Run Program的创建与守护。


关于Host,我们最容易想到的就是程序的启动与停止,而其中隐藏着非常关键的功能,就是Host的初始化,我们所需要的所有资源都必须而且应该在程序启动过程中初始化完成,当然本文的主要内容并不是Host初始化,前文已经累述。当然,为了更好的守护与管理已经启动的Host,.NET
Core 3.0将程序的生命周期事件的订阅开放给开发者,当然也包括自定义的Host Service对象。

注:本文代码基于.NET Core 3.0 Preview9


<https://img2018.cnblogs.com/blog/533598/201909/533598-20190914220234020-1611831647.png>

.NET Core 3.0中创建Long Run Program

IHost与IHostBuilder

当我们创建Long Run Program时,会首先关注程序的启动与停止,.NET Core
3.0为此提供了一个接口IHost,该接口位于Microsoft.Extensions.Hosting类库中,其源码如下:
1: /// <summary> 2: /// A program abstraction. 3: /// </summary> 4: public
interface IHost : IDisposable 5: { 6: /// <summary> 7: /// The programs
configured services. 8: /// </summary> 9: IServiceProvider Services { get; }
10:  11: /// <summary> 12: /// Start the program. 13: /// </summary> 14:
/// <param name="cancellationToken">Used to abort program start.</param> 15:
/// <returns>A <see cref="Task"/> that will be completed when the <see
cref="IHost"/> starts.</returns> 16: Task StartAsync(CancellationToken
cancellationToken =default); 17:   18: /// <summary> 19: /// Attempts to
gracefully stop the program. 20: /// </summary> 21: /// <param
name="cancellationToken">Used to indicate when stop should no longer be
graceful.</param> 22: /// <returns>A <see cref="Task"/> that will be completed
when the <see cref="IHost"/> stops.</returns> 23: Task
StopAsync(CancellationToken cancellationToken =default); 24: }
该接口含有一个只读属性:IServiceProvider Services { get; },通过该属性,我们可以拿到所有Host初始化时所注入的对象信息。

IHostBuilder接口所承担的核心功能就是程序的初始化,通过:IHost
Build()来完成,当然只需要运行一次即可。其初始化内容一般包括以下几个功能:


<https://img2018.cnblogs.com/blog/533598/201909/533598-20190914220234654-1227722267.png>


另外需要说明的是,以上功能的初始化,是通过IHostBuilder提供的接口获取用户输入的信息后,通过调用Build()方法来完成初始化。以下为IHostBuilder的部分源代码:
1: /// <summary> 2: /// Set up the configuration for the builder itself.
This will be used to initialize the <see cref="IHostEnvironment"/> 3: /// for
use later in the build process. This can be called multiple times and the
results will be additive. 4: /// </summary> 5: /// <param
name="configureDelegate">The delegate for configuring the <see
cref="IConfigurationBuilder"/> that will be used 6: /// to construct the <see
cref="IConfiguration"/> for the host.</param> 7: /// <returns>The same
instance of the <see cref="IHostBuilder"/> for chaining.</returns> 8: public
IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder>
configureDelegate) 9: { 10:
_configureHostConfigActions.Add(configureDelegate ??throw new
ArgumentNullException(nameof(configureDelegate))); 11: return this; 12: } 13:
  14: /// <summary> 15: /// Adds services to the container. This can be
called multiple times and the results will be additive. 16: /// </summary> 17:
/// <param name="configureDelegate">The delegate for configuring the <see
cref="IConfigurationBuilder"/> that will be used 18: /// to construct the <see
cref="IConfiguration"/> for the host.</param> 19: /// <returns>The same
instance of the <see cref="IHostBuilder"/> for chaining.</returns> 20: public
IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection>
configureDelegate) 21: { 22: _configureServicesActions.Add(configureDelegate
??throw new ArgumentNullException(nameof(configureDelegate))); 23: return this;
24:} 25:   26: /// <summary> 27: /// Overrides the factory used to create
the service provider. 28: /// </summary> 29: /// <typeparam
name="TContainerBuilder">The type of the builder to create.</typeparam> 30:
/// <param name="factory">A factory used for creating service providers.</param>
31:/// <returns>The same instance of the <see cref="IHostBuilder"/> for
chaining.</returns> 32: public IHostBuilder
UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder>
factory) 33: { 34: _serviceProviderFactory = new
ServiceFactoryAdapter<TContainerBuilder>(factory ??throw new
ArgumentNullException(nameof(factory))); 35: return this; 36: } 37:   38:
/// <summary> 39: /// Enables configuring the instantiated dependency
container. This can be called multiple times and 40: /// the results will be
additive. 41: /// </summary> 42: /// <typeparam name="TContainerBuilder">The
type of the builder to create.</typeparam> 43: /// <param
name="configureDelegate">The delegate for configuring the <see
cref="IConfigurationBuilder"/> that will be used 44: /// to construct the <see
cref="IConfiguration"/> for the host.</param> 45: /// <returns>The same
instance of the <see cref="IHostBuilder"/> for chaining.</returns> 46: public
IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext,
TContainerBuilder> configureDelegate) 47: { 48:
_configureContainerActions.Add(new
ConfigureContainerAdapter<TContainerBuilder>(configureDelegate 49: ?? throw
new ArgumentNullException(nameof(configureDelegate)))); 50: return this; 51: }
IHostService

文章开头有说过自定义Host
Service对象,那么我们如何自定义呢,其实很简单只需要实现IHostService,并在ConfigureServices中调用services.AddHostedService<MyServiceA>()即可,以下是IHostService的源码:
1: /// <summary> 2: /// Defines methods for objects that are managed by the
host. 3: /// </summary> 4: public interface IHostedService 5: { 6: ///
<summary> 7: /// Triggered when the application host is ready to start the
service. 8: /// </summary> 9: /// <param name="cancellationToken">Indicates
that the start process has been aborted.</param> 10: Task
StartAsync(CancellationToken cancellationToken); 11:   12: /// <summary> 13:
/// Triggered when the application host is performing a graceful shutdown. 14:
/// </summary> 15: /// <param name="cancellationToken">Indicates that the
shutdown process should no longer be graceful.</param> 16: Task
StopAsync(CancellationToken cancellationToken); 17: }
根据源码我们可以知道,该接口只有两个方法,即代码程序开始与停止的方法。具体的实现可以参考如下:
1: public class MyServiceA : IHostedService, IDisposable 2: { 3: private
bool _stopping; 4: private Task _backgroundTask; 5:   6: public
MyServiceA(ILoggerFactory loggerFactory) 7: { 8: Logger =
loggerFactory.CreateLogger<MyServiceB>(); 9: } 10:   11: public ILogger
Logger { get; } 12:   13: public Task StartAsync(CancellationToken
cancellationToken) 14: { 15: Logger.LogInformation("MyServiceB is starting."
); 16: _backgroundTask = BackgroundTask(); 17: return Task.CompletedTask;
18: } 19:   20: private async Task BackgroundTask() 21: { 22: while
(!_stopping) 23: { 24: await Task.Delay(TimeSpan.FromSeconds(7)); 25:
Logger.LogInformation("MyServiceB is doing background work."); 26: } 27:  
28: Logger.LogInformation("MyServiceB background task is stopping."); 29: }
30:  31: public async Task StopAsync(CancellationToken cancellationToken) 32:
{ 33: Logger.LogInformation("MyServiceB is stopping."); 34: _stopping =
true; 35: if (_backgroundTask != null) 36: { 37: // TODO: cancellation 38:
await _backgroundTask; 39: } 40: } 41:   42: public void Dispose() 43:
{ 44: Logger.LogInformation("MyServiceB is disposing."); 45: } 46: }
IHostService是我们自定义Host管理对象的入口,所有需要压入到Host托管的对象都必须要实现此接口。

Host生命周期的管理


该接口提供了一种我们可以在程序运行期间进行管理的功能,如程序的启动与停止事件的订阅,关于Host生命周期的管理,主要由IHostApplicationLifetime和IHostLifetime这两个接口来完成。

以下是IHostApplicationLifetime的源码​
1: public interface IHostApplicationLifetime 2: { 3: /// <summary> 4: ///
Triggered when the application host has fully started. 5: /// </summary> 6:
CancellationToken ApplicationStarted { get; } 7:   8: /// <summary> 9: ///
Triggered when the application host is performing a graceful shutdown. 10: ///
Shutdown will block until this event completes. 11: /// </summary> 12:
CancellationToken ApplicationStopping { get; } 13:   14: /// <summary> 15:
/// Triggered when the application host is performing a graceful shutdown. 16:
/// Shutdown will block until this event completes. 17: /// </summary> 18:
CancellationToken ApplicationStopped { get; } 19:   20: /// <summary> 21:
/// Requests termination of the current application. 22: /// </summary> 23:
void StopApplication(); 24: }
IHostLifetime源码如下:
1: public interface IHostLifetime 2: { 3: /// <summary> 4: /// Called at
the start of <see cref="IHost.StartAsync(CancellationToken)"/> which will wait
until it's complete before 5: /// continuing. This can be used to delay
startup until signaled by an external event. 6: /// </summary> 7: /// <param
name="cancellationToken">Used to indicate when stop should no longer be
graceful.</param> 8: /// <returns>A <see cref="Task"/>.</returns> 9: Task
WaitForStartAsync(CancellationToken cancellationToken); 10:   11: ///
<summary> 12: /// Called from <see cref="IHost.StopAsync(CancellationToken)"/>
to indicate that the host is stopping and it's time to shut down. 13: ///
</summary> 14: /// <param name="cancellationToken">Used to indicate when stop
should no longer be graceful.</param> 15: /// <returns>A <see
cref="Task"/>.</returns> 16: Task StopAsync(CancellationToken
cancellationToken); 17: }
具体的使用可以参考如下代码:
1: public class MyLifetime : IHostLifetime, IDisposable 2: { 3: .........
4:  5: private IHostApplicationLifetime ApplicationLifetime { get; } 6:   7:
public ConsoleLifetime(IHostApplicationLifetime applicationLifetime) 8: { 9:
ApplicationLifetime = applicationLifetime ??throw new
ArgumentNullException(nameof(applicationLifetime)); 10: } 11:   12: public
Task WaitForStartAsync(CancellationToken cancellationToken) 13: { 14:
_applicationStartedRegistration =
ApplicationLifetime.ApplicationStarted.Register(state => 15: { 16:
((ConsoleLifetime)state).OnApplicationStarted(); 17: }, 18: this); 19:
_applicationStoppingRegistration =
ApplicationLifetime.ApplicationStopping.Register(state => 20: { 21:
((ConsoleLifetime)state).OnApplicationStopping(); 22: }, 23: this); 24:  
25: ....... 26:   27: return Task.CompletedTask; 28: } 29:   30: private
void OnApplicationStarted() 31: { 32: Logger.LogInformation("Application
started. Press Ctrl+C to shut down."); 33: Logger.LogInformation("Hosting
environment: {envName}", Environment.EnvironmentName); 34:
Logger.LogInformation("Content root path: {contentRoot}",
Environment.ContentRootPath); 35: } 36:   37: private void
OnApplicationStopping() 38: { 39: Logger.LogInformation("Application is
shutting down..."); 40: } 41:   42: ........ 43: }
总结

至此,我们知道了创建Long Run
Program所需要关注的几个点,分别是继承IHostService、订阅程序的生命周期时间以及Host的初始化过程。相对来说这段内容还是比较简单的,但是开发过程中,依然会遇到很多的问题,比如任务的定时机制、消息的接入、以及程序的性能优化等等,这些都需要我们在实践中进一步总结完善。