寫在前面
停了近一個月的技術博客,隨着正式脫離996的魔窟,接下來也正式恢復了。本文從源碼角度進一步討論.NET Core 3.0 中關於Host擴展的一些技術點,主要討論Long Run Program的創建與守護。
關於Host,我們最容易想到的就是程序的啟動與停止,而其中隱藏着非常關鍵的功能,就是Host的初始化,我們所需要的所有資源都必須而且應該在程序啟動過程中初始化完成,本文的主要內容並不是Host初始化,前文已經累述。為了更好的守護與管理已經啟動的Host,.NET Core 3.0將程序的生命周期事件的訂閱開放給開發者,也包括自定義的Host Service對象。
注:本文代碼基於.NET Core 3.0 Preview9
.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()來完成,當然只需要運行一次即可。其初始化內容一般包括以下幾個功能:
另外需要說明的是,以上功能的初始化,是通過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的初始化過程。相對來說這段內容還是比較簡單的,但是開發過程中,依然會遇到很多的問題,比如任務的定時機制、消息的接入、以及程序的性能優化等等,這些都需要我們在實踐中進一步總結完善。