我們在《總體設計[上篇]》和《總體設計[下篇]》中通過對IHostedService、IHost和IHostBuider這三個接口的介紹讓讀者朋友們對服務承載模型有了大致的了解。接下來我們從抽象轉向具體,看看承載系統針對該模型的實現是如何落地的。要了解承載模型的默認實現,只需要了解IHost接口和IHostBuilder的默認實現類型就可以了。從下圖所示的UML可以看出,這兩個接口的默認實現類型分別是Host和HostBuilder,本篇將會着重介紹這兩個類型。本篇內容節選自即將出版的《ASP.NET Core 3框架解密》,感興趣的朋友可以通過《“ASP.NET Core 3框架揭秘”讀者群,歡迎加入》加入本書讀者群
一、服務宿主
Host類型是對IHost接口的默認實現,它僅僅是定義在NuGet包“Microsoft.Extensions.Hosting”中的一個內部類型,由於我們在本節最后還會涉及另一個同名的公共靜態類型,在容易出現混淆的地方,我們會將它稱為“實例類型Host”以示區別。在正式介紹Host類型的具體實現之前,我們得先來認識兩個相關的類型,其中一個是承載相關配置選項的HostOptions。如下面的代碼片段所示,HostOptions僅包含唯一的屬性ShutdownTimeout表示關閉Host對象的超時時限,它的默認值為5秒鍾。
public class HostOptions { public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); }
我們在《總體設計[上篇]》已經認識了一個與承載應用生命周期相關的IHostApplicationLifetime接口,Host類型還涉及到另一個與生命周期相關的IHostLifetime接口。當我們調用Host對象的StartAsync方法將它啟動之后,該方法會先調用IHostLifetime服務的WaitForStartAsync方法。當Host對象的StopAsync方法在執行過程中,如果它成功關閉了所有承載的服務,注冊IHostLifetime服務的StopAsync方法會被調用。
public interface IHostLifetime { Task WaitForStartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); }
在《承載長時間運行的服務[下篇]》進行日志編程的演示時,程序啟動后控制台上會輸出三條級別為Information的日志,其中第一條日志的內容為“Application started. Press Ctrl+C to shut down.”,后面兩條則會輸出當前的承載環境的信息和存放內容文件的根目錄路徑。當應用程序關閉之前,控制台上還會出現一條內容為“Application is shutting down...”的日志。上述這四條日志在控制台上輸出額效果體現在下圖中。
上圖所示的四條日志都是如下這個ConsoleLifetime對象輸出的,ConsoleLifetime類型是對IHostLifetime接口的實現。除了以日志的形式輸出與當前承載應用程序相關的狀態信息之外,針對Cancel按鍵(Ctrl + C)的捕捉以及隨后關閉當前應用的功能也實現在ConsoleLifetime類型中。ConsoleLifetime采用的配置選項定義在ConsoleLifetimeOptions類型中,該類型唯一的屬性成員SuppressStatusMessages用來決定上述四條日志是否需要被輸出。
public class ConsoleLifetime : IHostLifetime, IDisposable { public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime); public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory); public Task StopAsync(CancellationToken cancellationToken); public Task WaitForStartAsync(CancellationToken cancellationToken); public void Dispose(); } public class ConsoleLifetimeOptions { public bool SuppressStatusMessages { get; set; } }
下面的代碼片段展示的是經過簡化的Host類型的定義。Host類型的構造函數中注入了一系列依賴服務,其中包括作為依賴注入容器的IServiceProvider對象,用來記錄日志的ILogger<Host>對象和提供配置選項的IOptions<HostOptions>對象,以及兩個與生命周期相關的IHostApplicationLifetime對象和IHostLifetime對象。值得一提的是,這里提供的IHostApplicationLifetime對象的類型必需是ApplicationLifetime,因為它需要調用其NotifyStarted和NotifyStopped方法在應用程序啟動和關閉之后向訂閱者發出通知,但是這兩個方法並沒有定義在IHostApplicationLifetime接口中。
internal class Host : IHost { private readonly ILogger<Host> _logger; private readonly IHostLifetime _hostLifetime; private readonly ApplicationLifetime _applicationLifetime; private readonly HostOptions _options; private IEnumerable<IHostedService> _hostedServices; public IServiceProvider Services { get; } public Host(IServiceProvider services, IHostApplicationLifetime applicationLifetime, ILogger<Host> logger, IHostLifetime hostLifetime, IOptions<HostOptions> options) { Services = services; _applicationLifetime = (ApplicationLifetime)applicationLifetime; _logger = logger; _hostLifetime = hostLifetime; _options = options.Value); } public async Task StartAsync(CancellationToken cancellationToken = default) { await _hostLifetime.WaitForStartAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); _hostedServices = Services.GetService<IEnumerable<IHostedService>>(); foreach (var hostedService in _hostedServices) { await hostedService.StartAsync(cancellationToken).ConfigureAwait(false); } _applicationLifetime?.NotifyStarted(); } public async Task StopAsync(CancellationToken cancellationToken = default) { using (var cts = new CancellationTokenSource(_options.ShutdownTimeout)) using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken)) { var token = linkedCts.Token; _applicationLifetime?.StopApplication(); foreach (var hostedService in _hostedServices.Reverse()) { await hostedService.StopAsync(token).ConfigureAwait(false); } token.ThrowIfCancellationRequested(); await _hostLifetime.StopAsync(token); _applicationLifetime?.NotifyStopped(); } } public void Dispose() => (Services as IDisposable)?.Dispose(); }
在實現的StartAsync中,Host對象率先調用了IHostLifetime對象的WaitForStartAsync方法。如果注冊的服務類型為ConsoleLifetime,它會輸出前面提及的三條日志。於此同時,ConsoleLifetime對象還會注冊控制台的按鍵事件,其目的在於確保在用戶按下取消組合鍵(Ctrl + C)后應用能夠被正常關閉。
Host對象會利用作為依賴注入容器的IServiceProvider對象提取出代表承載服務的所有IHostedService對象,並通過StartAsync方法來啟動它們。當所有承載的服務正常啟動之后,ApplicationLifetime對象的NotifyStarted方法會被調用,此時訂閱者會接收到應用啟動的通知。有一點需要着重指出:代表承載服務的所有IHostedService對象是“逐個(不是並發)”被啟動的,而且只有等待所有承載服務全部被啟動之后,我們的應用程序才算成功啟動了。在整個啟動過程中,如果利用作為參數的CancellationToken接收到取消請求,啟動操作會中止。
當Host對象的StopAsync方法被調用的時候,它會調用ApplicationLifetime對象的StopApplication方法對外發出應用程序即將被關閉的通知,此后它會調用每個IHostedService對象的StopAsync方法。當所有承載服務被成功關閉之后,Host對象會先后調用IHostLifetime對象的StopAsync和ApplicationLifetime對象的NotifyStopped方法。在Host關閉過程中,如果超出了通過HostOptions配置選項設定的超時時限,或者利用作為參數的CancellationToken接收到取消請求,整個過程會中止。
二、針對配置系統的設置
作為服務宿主的IHost對象總是通過對應的IHostBuilder對象構建出來的,上面這個Host類型對應的IHostBuilder實現類型為HostBuilder,我們接下來就來探討一下Host對象是如何HostBuilder對象構建出來的。除了用於構建IHost對象的Build方法,IHostBuilder接口還定義了一系列的方法使我們可以對最終提供的IHost對象作相應的前期設置,這些設置將會被緩存起來最后應用到Build方法上。
我們先來介紹HostBuilder針對配置系統的設置。如下面的代碼片段所示,ConfigureHostConfiguration方法中針對面向宿主配置和ConfigureAppConfiguration方法面向應用配置提供的委托對象都暫存在對應集合對象中,對應的字段分別是configureHostConfigActions和configureAppConfigActions。
public class HostBuilder : IHostBuilder { private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>(); private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>(); public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>(); public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate) { _configureHostConfigActions.Add(configureDelegate); return this; } public IHostBuilder ConfigureAppConfiguration( Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) { _configureAppConfigActions.Add(configureDelegate); return this; } … }
IHostBuilder接口上的很多方法都與依賴注入有關。針對依賴注入框架的設置主要體現在兩個方面:其一,利用ConfigureServices方法添加服務注冊;其二,利用兩個UseServiceProviderFactory<TContainerBuilder>方法注冊IServiceProviderFactory<TContainerBuilder>工廠,以及利用ConfigureContainer<TContainerBuilder>方對該工廠創建的ContainerBuilder作進一步設置。
三、注冊依賴服務
與針對配置系統的設置一樣,ConfigureServices方法中用來注冊依賴服務的Action<HostBuilderContext, IServiceCollection>委托對象同樣被暫存在對應的字段configureServicesActions表示的集合中,它們最終會在Build方法中被使用。
public class HostBuilder : IHostBuilder { private List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<HostBuilderContext, IServiceCollection>>(); public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate) { _configureServicesActions.Add(configureDelegate); return this; } … }
除了直接調用IHostBuilder接口的ConfigureServices方法進行服務注冊之外,我們還可以調用如下這些擴展方法完成針對某些特殊服務的注冊。兩個ConfigureLogging擴展方法重載幫助我們注冊針對日志框架相關的服務,兩個UseConsoleLifetime擴展方法重載添加的是針對ConsoleLifetime的服務注冊,兩個RunConsoleAsync擴展方法重載則在注冊ConsoleLifetime服務的基礎上,進一步構建並啟動作為宿主的IHost對象。
public static class HostingHostBuilderExtensions { public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action<HostBuilderContext, ILoggingBuilder> configureLogging) => hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder))); public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action<ILoggingBuilder> configureLogging) => hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(builder))); public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder) => hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>()); public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Action<ConsoleLifetimeOptions> configureOptions) => hostBuilder.ConfigureServices((context, collection) => { collection.AddSingleton<IHostLifetime, ConsoleLifetime>(); collection.Configure(configureOptions); }); public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) => hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken); public static Task RunConsoleAsync(this IHostBuilder hostBuilder, Action<ConsoleLifetimeOptions> configureOptions, CancellationToken cancellationToken = default) => hostBuilder.UseConsoleLifetime(configureOptions).Build().RunAsync(cancellationToken); }
四、注冊IServiceProviderFactory<TContainerBuilder>
作為依賴注入容器的IServiceProvider對象總是由注冊的IServiceProviderFactory<TContainerBuilder>工廠創建的。由於UseServiceProviderFactory<TContainerBuilder>方法注冊的IServiceProviderFactory<TContainerBuilder>是個泛型對象,所以HostBuilder會將它轉換成如下這個IServiceFactoryAdapter接口類型作為適配。如下面的代碼片段所示,它僅僅是將ContainerBuilder轉換成Object類型而已。ServiceFactoryAdapter<TContainerBuilder>類型是對IServiceFactoryAdapter接口的默認實現。
internal class ServiceFactoryAdapter<TContainerBuilder> : IServiceFactoryAdapter { private IServiceProviderFactory<TContainerBuilder> _serviceProviderFactory; private readonly Func<HostBuilderContext> _contextResolver; private Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> _factoryResolver; public ServiceFactoryAdapter(IServiceProviderFactory<TContainerBuilder> serviceProviderFactory) => _serviceProviderFactory = serviceProviderFactory; public ServiceFactoryAdapter(Func<HostBuilderContext> contextResolver, Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factoryResolver) { _contextResolver = contextResolver; _factoryResolver = factoryResolver; } public object CreateBuilder(IServiceCollection services) => _serviceProviderFactory ?? _factoryResolver(_contextResolver()).CreateBuilder(services); public IServiceProvider CreateServiceProvider(object containerBuilder) => _serviceProviderFactory.CreateServiceProvider((TContainerBuilder)containerBuilder); }
如下所示的是兩個UseServiceProviderFactory<TContainerBuilder>重載的定義,第一個方法重載提供的IServiceProviderFactory<TContainerBuilder>對象和第二個方法重載提供的Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>>會被轉換成一個ServiceFactoryAdapter<TContainerBuilder>對象並通過_serviceProviderFactory字段暫存起來。如果UseServiceProviderFactory<TContainerBuilder>方法並沒有被調用,_serviceProviderFactory 字段返回的將是根據DefaultServiceProviderFactory對象創建的ServiceFactoryAdapter<IServiceCollection>對象,下面給出的代碼片段也體現了這一點。
public class HostBuilder : IHostBuilder { private List<IConfigureContainerAdapter> _configureContainerActions = new List<IConfigureContainerAdapter>(); private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory()); public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) { _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory); return this; } public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) { _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(() => _hostBuilderContext, factory)); return this; } }
注冊IServiceProviderFactory<TContainerBuilder>工廠提供的TContainerBuilder對象可以通過ConfigureContainer<TContainerBuilder>方法做進一步設置,具體的設置由提供的Action<HostBuilderContext, TContainerBuilder>對象來完成。這個泛型的委托對象同樣需要做類似的適配才能被暫存起來,它最終轉換成如下IConfigureContainerAdapter接口類型,這個適配本質上也是將TContainerBuilder對象轉換成了Object類型。如下所示的ConfigureContainerAdapter<TContainerBuilder>類型是對這個接口的默認實現。
internal interface IServiceFactoryAdapter { object CreateBuilder(IServiceCollection services); IServiceProvider CreateServiceProvider(object containerBuilder); } internal interface IConfigureContainerAdapter { void ConfigureContainer(HostBuilderContext hostContext, object containerBuilder); } internal class ConfigureContainerAdapter<TContainerBuilder> : IConfigureContainerAdapter { private Action<HostBuilderContext, TContainerBuilder> _action; public ConfigureContainerAdapter(Action<HostBuilderContext, TContainerBuilder> action) => _action = action; public void ConfigureContainer(HostBuilderContext hostContext, object containerBuilder) => _action(hostContext, (TContainerBuilder)containerBuilder); }
如下所示的是ConfigureContainer<TContainerBuilder>方法的定義,我們會發現該方法會將提供的Action<HostBuilderContext, TContainerBuilder>對象轉換成ConfigureContainerAdapter<TContainerBuilder>對象,並添加到通過configureContainerActions字段表示的集合中。
public class HostBuilder : IHostBuilder { private List<IConfigureContainerAdapter> _configureContainerActions = new List<IConfigureContainerAdapter>(); public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate) { _configureContainerActions.Add(new ConfigureContainerAdapter<TContainerBuilder>(configureDelegate)); return this; } … }
五、與第三方依賴注入框架的整合
我們在《一個Mini版的依賴注入框架》中創建了一個名為Cat的簡易版依賴注入框架,並在《與第三方依賴注入框架的適配》中為它創建了一個IServiceProviderFactory<TContainerBuilder>實現,具體類型為CatServiceProvider,接下來我們演示一下如何通過注冊這個CatServiceProvider實現與Cat這個第三方依賴注入框架的整合。如果使用Cat框架,我們可以在服務類型上標注MapToAttribute特性的方式來定義服務注冊信息。在創建的演示程序中,我們采用這樣的方式定義了三個服務(Foo、Bar和Baz)和對應的接口(IFoo、IBar和IBaz)。
public interface IFoo { } public interface IBar { } public interface IBaz { } [MapTo(typeof(IFoo), Lifetime.Root)] public class Foo : IFoo { } [MapTo(typeof(IBar), Lifetime.Root)] public class Bar : IBar { } [MapTo(typeof(IBaz), Lifetime.Root)] public class Baz : IBaz { }
如下所示的FakeHostedService表示我們演示的應用程序承載的服務。我們在構造函數中注入了上面定義的三個服務,構造函數提供的調試斷言確保這三個服務被成功注入。
public sealed class FakeHostedService: IHostedService { public FakeHostedService(IFoo foo, IBar bar, IBaz baz) { Debug.Assert(foo != null); Debug.Assert(bar != null); Debug.Assert(baz != null); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
在如下所示的服務承載程序中,我們創建了一個HostBuilder對象,並通過調用ConfigureServices方法注冊了需要承載的FakeHostedService服務。我們接下來調用UseServiceProviderFactory方法完成了對CatServiceProvider的注冊,並在隨后調用了CatBuilder的Register方法完成了針對入口程序集的批量服務注冊。當我們調用HostBuilder的Build方法構建出作為宿主的Host對象並啟動它之后,承載的FakeHostedService服務將自動被創建並啟動。(源代碼從這里下載)
class Program { static void Main() { new HostBuilder() .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>()) .UseServiceProviderFactory(new CatServiceProviderFactory()) .ConfigureContainer<CatBuilder>(builder=>builder.Register(Assembly.GetEntryAssembly())) .Build() .Run(); } }
服務承載系統[1]: 承載長時間運行的服務[上篇]
服務承載系統[2]: 承載長時間運行的服務[下篇]
服務承載系統[3]: 總體設計[上篇]
服務承載系統[4]: 總體設計[下篇]
服務承載系統[5]: 承載服務啟動流程[上篇]
服務承載系統[6]: 承載服務啟動流程[下篇]