1. 程式人生 > >[ASP.NET Core 3框架揭祕]服務承載系統[5]: 承載服務啟動流程[上篇]

[ASP.NET Core 3框架揭祕]服務承載系統[5]: 承載服務啟動流程[上篇]

我們在《總體設計[上篇]》和《總體設計[下篇]》中通過對IHostedService、IHost和IHostBuider這三個介面的介紹讓讀者朋友們對服務承載模型有了大致的瞭解。接下來我們從抽象轉向具體,看看承載系統針對該模型的實現是如何落地的。要了解承載模型的預設實現,只需要瞭解IHost介面和IHostBuilder的預設實現型別就可以了。從下圖所示的UML可以看出,這兩個介面的預設實現型別分別是Host和HostBuilder,本篇將會著重介紹這兩個型別。

一、服務宿主

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]: 承載服務啟動流程