[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 options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime);
    public ConsoleLifetime(IOptions 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对象和提供配置选项的IOptions对象,以及两个与生命周期相关的IHostApplicationLifetime对象和IHostLifetime对象。值得一提的是,这里提供的IHostApplicationLifetime对象的类型必需是ApplicationLifetime,因为它需要调用其NotifyStarted和NotifyStopped方法在应用程序启动和关闭之后向订阅者发出通知,但是这两个方法并没有定义在IHostApplicationLifetime接口中。

internal class Host : IHost
{
    private readonly ILogger _logger;
    private readonly IHostLifetime _hostLifetime;
    private readonly ApplicationLifetime _applicationLifetime;
    private readonly HostOptions _options;
    private IEnumerable _hostedServices;

    public IServiceProvider Services { get; }

    public Host(IServiceProvider services, IHostApplicationLifetime applicationLifetime, ILogger logger, IHostLifetime hostLifetime, IOptions 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>();
        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> _configureHostConfigActions = new List<Action>();
    private List<Action> _configureAppConfigActions = new List<Action>();

    public IDictionary Properties { get; } = new Dictionary();

    public IHostBuilder ConfigureHostConfiguration(Action configureDelegate)
    {
        _configureHostConfigActions.Add(configureDelegate);
        return this;
    }

    public IHostBuilder ConfigureAppConfiguration(
        Action configureDelegate)
    {
        _configureAppConfigActions.Add(configureDelegate);
        return this;
    }
    …
}

IHostBuilder接口上的很多方法都与依赖注入有关。针对依赖注入框架的设置主要体现在两个方面:其一,利用ConfigureServices方法添加服务注册;其二,利用两个UseServiceProviderFactory方法注册IServiceProviderFactory工厂,以及利用ConfigureContainer方对该工厂创建的ContainerBuilder作进一步设置。

三、注册依赖服务

与针对配置系统的设置一样,ConfigureServices方法中用来注册依赖服务的Action委托对象同样被暂存在对应的字段configureServicesActions表示的集合中,它们最终会在Build方法中被使用。

public class HostBuilder : IHostBuilder
{
    private List<Action> _configureServicesActions = new List<Action>();

    public IHostBuilder ConfigureServices(Action 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 configureLogging)
    => hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder)));

    public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action configureLogging)
    => hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(builder)));

    public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
    =>  hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton());

    public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Action configureOptions)
    =>  hostBuilder.ConfigureServices((context, collection) =>
        {
            collection.AddSingleton();
            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 configureOptions, CancellationToken cancellationToken = default)
    =>  hostBuilder.UseConsoleLifetime(configureOptions).Build().RunAsync(cancellationToken);
}

四、注册IServiceProviderFactory

作为依赖注入容器的IServiceProvider对象总是由注册的IServiceProviderFactory工厂创建的。由于UseServiceProviderFactory方法注册的IServiceProviderFactory是个泛型对象,所以HostBuilder会将它转换成如下这个IServiceFactoryAdapter接口类型作为适配。如下面的代码片段所示,它仅仅是将ContainerBuilder转换成Object类型而已。ServiceFactoryAdapter类型是对IServiceFactoryAdapter接口的默认实现。

internal class ServiceFactoryAdapter : IServiceFactoryAdapter
{
    private IServiceProviderFactory _serviceProviderFactory;
    private readonly Func _contextResolver;
    private Func<HostBuilderContext, IServiceProviderFactory> _factoryResolver;

    public ServiceFactoryAdapter(IServiceProviderFactory serviceProviderFactory)
    => _serviceProviderFactory = serviceProviderFactory;

    public ServiceFactoryAdapter(Func contextResolver, Func<HostBuilderContext, IServiceProviderFactory> 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重载的定义,第一个方法重载提供的IServiceProviderFactory对象和第二个方法重载提供的Func<HostBuilderContext, IServiceProviderFactory>会被转换成一个ServiceFactoryAdapter对象并通过_serviceProviderFactory字段暂存起来。如果UseServiceProviderFactory方法并没有被调用,_serviceProviderFactory 字段返回的将是根据DefaultServiceProviderFactory对象创建的ServiceFactoryAdapter对象,下面给出的代码片段也体现了这一点。

public class HostBuilder : IHostBuilder
{
    private List _configureContainerActions = new List();
    private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter(new DefaultServiceProviderFactory());

    public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory)
    {
        _serviceProviderFactory = new ServiceFactoryAdapter(factory);
        return this;
    }

    public IHostBuilder UseServiceProviderFactory(Func<HostBuilderContext, IServiceProviderFactory> factory)
    {
        _serviceProviderFactory = new ServiceFactoryAdapter(() => _hostBuilderContext, factory));
        return this;
    }
}

注册IServiceProviderFactory工厂提供的TContainerBuilder对象可以通过ConfigureContainer方法做进一步设置,具体的设置由提供的Action对象来完成。这个泛型的委托对象同样需要做类似的适配才能被暂存起来,它最终转换成如下IConfigureContainerAdapter接口类型,这个适配本质上也是将TContainerBuilder对象转换成了Object类型。如下所示的ConfigureContainerAdapter类型是对这个接口的默认实现。

internal interface IServiceFactoryAdapter
{
    object CreateBuilder(IServiceCollection services);
    IServiceProvider CreateServiceProvider(object containerBuilder);
}

internal interface IConfigureContainerAdapter
{
    void ConfigureContainer(HostBuilderContext hostContext, object containerBuilder);
}

internal class ConfigureContainerAdapter : IConfigureContainerAdapter
{
    private Action _action;
    public ConfigureContainerAdapter(Action action)
        => _action = action;
    public void ConfigureContainer(HostBuilderContext hostContext, object containerBuilder)
        => _action(hostContext, (TContainerBuilder)containerBuilder);
}

如下所示的是ConfigureContainer方法的定义,我们会发现该方法会将提供的Action对象转换成ConfigureContainerAdapter对象,并添加到通过configureContainerActions字段表示的集合中。

public class HostBuilder : IHostBuilder
{
    private List _configureContainerActions = new List();
    public IHostBuilder ConfigureContainer(Action configureDelegate)
    {
        _configureContainerActions.Add(new ConfigureContainerAdapter(configureDelegate));
        return this;
    }
    …
}

五、与第三方依赖注入框架的整合

我们在《 一个Mini版的依赖注入框架 》中创建了一个名为Cat的简易版依赖注入框架,并在《 与第三方依赖注入框架的适配 》中为它创建了一个IServiceProviderFactory实现,具体类型为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())
            .UseServiceProviderFactory(new CatServiceProviderFactory())
            .ConfigureContainer(builder=>builder.Register(Assembly.GetEntryAssembly()))
            .Build()
            .Run();
    }
}