停了近一个月的技术博客,随着正式脱离996的魔窟,接下来也正式恢复了。本文从源码角度进一步讨论.NET Core 3.0 中关于Host扩展的一些技术点,主要讨论Long Run Program的创建与守护。
关于Host,我们最容易想到的就是程序的启动与停止,而其中隐藏着非常关键的功能,就是Host的初始化,我们所需要的所有资源都必须而且应该在程序启动过程中初始化完成,当然本文的主要内容并不是Host初始化,前文已经累述。当然,为了更好的守护与管理已经启动的Host,.NET Core 3.0将程序的生命周期事件的订阅开放给开发者,当然也包括自定义的Host Service对象。
注:本文代码基于.NET Core 3.0 Preview9
当我们创建Long Run Program时,会首先关注程序的启动与停止,.NET Core 3.0为此提供了一个接口IHost,该接口位于Microsoft.Extensions.Hosting类库中,其源码如下:
/// <summary>
/// A program abstraction.
/// </summary>
public interface IHost : IDisposable
{
/// <summary>
/// The programs configured services.
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Start the program.
/// </summary>
/// <param name="cancellationToken">Used to abort program start.</param>
/// <returns>A <see cref="Task"/> that will be completed when the <see cref="IHost"/> starts.</returns>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Attempts to gracefully stop the program.
/// </summary>
/// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
/// <returns>A <see cref="Task"/> that will be completed when the <see cref="IHost"/> stops.</returns>
Task StopAsync(CancellationToken cancellationToken = default);
}
该接口含有一个只读属性:IServiceProvider Services { get; },通过该属性,我们可以拿到所有Host初始化时所注入的对象信息。
IHostBuilder接口所承担的核心功能就是程序的初始化,通过:IHost Build()来完成,当然只需要运行一次即可。其初始化内容一般包括以下几个功能:
另外需要说明的是,以上功能的初始化,是通过IHostBuilder提供的接口获取用户输入的信息后,通过调用Build()方法来完成初始化。以下为IHostBuilder的部分源代码:
/// <summary>
/// Set up the configuration for the builder itself. This will be used to initialize the <see cref="IHostEnvironment"/>
/// for use later in the build process. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
/// to construct the <see cref="IConfiguration"/> for the host.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
{
_configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
/// to construct the <see cref="IConfiguration"/> for the host.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
_configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
/// <summary>
/// Overrides the factory used to create the service provider.
/// </summary>
/// <typeparam name="TContainerBuilder">The type of the builder to create.</typeparam>
/// <param name="factory">A factory used for creating service providers.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
{
_serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(tory)));
return this;
}
/// <summary>
/// Enables configuring the instantiated dependency container. This can be called multiple times and
/// the results will be additive.
/// </summary>
/// <typeparam name="TContainerBuilder">The type of the builder to create.</typeparam>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
/// to construct the <see cref="IConfiguration"/> for the host.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate)
{
_configureContainerActions.Add(new ConfigureContainerAdapter<TContainerBuilder>(configureDelegate
?? throw new ArgumentNullException(nameof(configureDelegate))));
return this;
}
文章开头有说过自定义Host Service对象,那么我们如何自定义呢,其实很简单只需要实现IHostService,并在ConfigureServices中调用services.AddHostedService<MyServiceA>()即可,以下是IHostService的源码:
/// <summary>
/// Defines methods for objects that are managed by the host.
/// </summary>
public interface IHostedService
{
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
Task StartAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
Task StopAsync(CancellationToken cancellationToken);
}
根据源码我们可以知道,该接口只有两个方法,即代码程序开始与停止的方法。具体的实现可以参考如下:
public class MyServiceA : IHostedService, IDisposable
{
private bool _stopping;
private Task _backgroundTask;
public MyServiceA(ILoggerFactory loggerFactory)
{
Logger = loggerFactory.CreateLogger<MyServiceB>();
}
public ILogger Logger { get; }
public Task StartAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("MyServiceB is starting.");
_backgroundTask = BackgroundTask();
return Task.CompletedTask;
}
private async Task BackgroundTask()
{
while (!_stopping)
{
await Task.Delay(TimeSpan.FromSeconds(7));
Logger.LogInformation("MyServiceB is doing background work.");
}
Logger.LogInformation("MyServiceB background task is stopping.");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("MyServiceB is stopping.");
_stopping = true;
if (_backgroundTask != null)
{
// TODO: cancellation
await _backgroundTask;
}
}
public void Dispose()
{
Logger.LogInformation("MyServiceB is disposing.");
}
}
IHostService是我们自定义Host管理对象的入口,所有需要压入到Host托管的对象都必须实现此接口。
该接口提供了一种我们可以在程序运行期间进行管理的功能,如程序的启动与停止事件的订阅,关于Host生命周期的管理,主要由IHostApplicationLifetime和IHostLifetime这两个接口来完成。
以下是IHostApplicationLifetime的源码
public interface IHostApplicationLifetime
{
/// <summary>
/// Triggered when the application host has fully started.
/// </summary>
CancellationToken ApplicationStarted { get; }
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// Shutdown will block until this event completes.
/// </summary>
CancellationToken ApplicationStopping { get; }
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// Shutdown will block until this event completes.
/// </summary>
CancellationToken ApplicationStopped { get; }
/// <summary>
/// Requests termination of the current application.
/// </summary>
void StopApplication();
}
IHostLifetime源码如下:
public interface IHostLifetime
{
/// <summary>
/// Called at the start of <see cref="IHost.StartAsync(CancellationToken)"/> which will wait until it's complete before
/// continuing. This can be used to delay startup until signaled by an external event.
/// </summary>
/// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
/// <returns>A <see cref="Task"/>.</returns>
Task WaitForStartAsync(CancellationToken cancellationToken);
/// <summary>
/// Called from <see cref="IHost.StopAsync(CancellationToken)"/> to indicate that the host is stopping and it's time to shut down.
/// </summary>
/// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
/// <returns>A <see cref="Task"/>.</returns>
Task StopAsync(CancellationToken cancellationToken);
}
具体的使用可以参考如下代码:
public class MyLifetime : IHostLifetime, IDisposable
{
.........
private IHostApplicationLifetime ApplicationLifetime { get; }
public ConsoleLifetime(IHostApplicationLifetime applicationLifetime)
{
ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
}
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
_applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(state =>
{
((ConsoleLifetime)state).OnApplicationStarted();
},
this);
_applicationStoppingRegistration = ApplicationLifetime.ApplicationStopping.Register(state =>
{
((ConsoleLifetime)state).OnApplicationStopping();
},
this);
.......
return Task.CompletedTask;
}
private void OnApplicationStarted()
{
Logger.LogInformation("Application started. Press Ctrl+C to shut down.");
Logger.LogInformation("Hosting environment: {envName}", Environment.EnvironmentName);
Logger.LogInformation("Content root path: {contentRoot}", Environment.ContentRootPath);
}
private void OnApplicationStopping()
{
Logger.LogInformation("Application is shutting down...");
}
........
}
至此,我们知道了创建Long Run Program所需要关注的几个点,分别是继承IHostService、订阅程序的生命周期时间以及Host的初始化过程。相对来说这段内容还是比较简单的,但是开发过程中,依然会遇到很多的问题,比如任务的定时机制、消息的接入、以及程序的性能优化等等,这些都需要我们在实践中进一步总结完善。