专栏首页大内老A[ASP.NET Core 3框架揭秘]服务承载系统[5]: 承载服务启动流程[上篇]

[ASP.NET Core 3框架揭秘]服务承载系统[5]: 承载服务启动流程[上篇]

一、服务宿主

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();
    }
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Dora.Interception,为.NET Core度身打造的AOP框架 [4]:与依赖注入框架的无缝集成

    Dora.Interception最初的定位就是专门针对.NET Core的AOP框架,所以在整个迭代过程中我大部分是在做减法。对于.NET Core程序开发来...

    蒋金楠
  • Dora.Interception,为.NET Core度身打造的AOP框架 [5]:轻松地实现与其他AOP框架的整合

    这里所谓的与第三方AOP框架的整合不是说改变Dora.Interception现有的编程,而是恰好相反,即在不改变现有编程模式下采用第三方AOP框架或者自行实现...

    蒋金楠
  • 为了支持AOP的编程模式,我为.NET Core写了一个轻量级的Interception框架[开源]

    ASP.NET Core具有一个以ServiceCollection和ServiceProvider为核心的依赖注入框架,虽然这只是一个很轻量级的框架,但是在大...

    蒋金楠
  • 建造者模式多产品情况反射实现

    前面在《重温设计模式之建造者模式(Builder)》中关于建造者模式的的介绍比较简单,代码只是实现了基本的模式,因为实际应用中不可能只考虑一个模式的,因此这里给...

    the5fire
  • 设计模式 里氏替换原则

    在场景中,三毛需要什么枪支,就直接new 出一个枪支即可,然后其内通过抽象类获取到对象,然后对齐进行修饰

    mySoul
  • UML图示与代码对照

    本文转载:http://www.cnblogs.com/iamlilinfeng/archive/2012/08/29/2662740.html

    跟着阿笨一起玩NET
  • Java开发中的23种设计模式详解

    结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

    用户5640963
  • Java开发中的23种设计模式详解(转)

                                      ——可复用面向对象软件的基础

    肖哥哥
  • 设计模式 接口隔离原则

    接着,要进行更改了。对好看的定义,发生了改变,那么就应该改变PettyGirl中的内容,但是已经在接口中定义了。那么就有问题了。即,接口承担的内容过多导致

    mySoul
  • 重拾Java(0)-基础知识点

    叶应是叶

扫码关注云+社区

领取腾讯云代金券