ASP.NET Core 6框架揭秘实例演示[22]:如何承载你的后台服务[补充]

语言: CN / TW / HK

借助 .NET提供的服务承载(Hosting)系统,我们可以将一个或者多个长时间运行的后台服务寄宿或者承载我们创建的应用中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载,ASP.NET Core应用最终也体现为这样一个承载服务。(本篇提供的实例已经汇总到《 ASP.NET Core 6框架揭秘-实例演示版 》)

[S1407]利用IHostApplicationLifetime对象关闭应用( 源代码

[S1408]与第三方依赖注入框架的整合( 源代码

[S1409]利用配置初始化承载环境( 源代码

[S1407]利用IHostApplicationLifetime对象关闭应用

我们接下来通过一个简单的实例演示如何利用IHostApplicationLifetime服务来关闭整个承载应用。我们在一个控制台应用程序中定义了如下这个承载服务类型FakeHostedService,并在其构造函数中注入了IHostApplicationLifetime服务。在得到其三个属性返回的CancellationToken对象之后,我们在它们上面分别注册了一个回调在控制台输出相应的文字。

public sealed class FakeHostedService : IHostedService
{
    private readonly IHostApplicationLifetime 	_lifetime;
    private IDisposable? 			_tokenSource;

    public FakeHostedService(IHostApplicationLifetime lifetime)
    {
        _lifetime = lifetime;
        _lifetime.ApplicationStarted.Register(() => Console.WriteLine("[{0}]Application started", DateTimeOffset.Now));
        _lifetime.ApplicationStopping.Register(() => Console.WriteLine("[{0}]Application is stopping.", DateTimeOffset.Now));
        _lifetime.ApplicationStopped.Register(() => Console.WriteLine("[{0}]Application stopped.", DateTimeOffset.Now));
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token.Register(_lifetime.StopApplication);
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _tokenSource?.Dispose();
        return Task.CompletedTask;
    }
}

在实现的StartAsync方法中,我们采用如上的方式在等待5秒之后调用IHostApplicationLifetime对象的StopApplication方法关闭应用程序。FakeHostedService服务最后采用如下所示的方式承载于当前应用程序中。

using App;
Host.CreateDefaultBuilder(args)
    .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>())
    .Build()
    .Run();

该程序运行之后在控制台上输出的结果如图1所示,从三条消息输出的时间间隔可以确定当前应用程序正是承载FakeHostedService通过调用IHostApplicationLifetime服务的StopApplication方法关闭的。

图1调用IHostApplicationLifetime服务关闭应用程序

[S1408]与第三方依赖注入框架的整合

一个Mini版的依赖注入框架 》中创建了一个名为Cat的简易版依赖注入框架,并在《 与第三方依赖注入框架Cat的整合 》中为其创建了一个IServiceProviderFactory<TContainerBuilder>实现类型,具体类型为CatServiceProvider,我们接下来演示一下如何通过注册CatServiceProvider实现与Cat这个第三方依赖注入框架的整合。在创建的演示程序中,我们采用这样的方式定义了三个服务(Foo、Bar和Baz)和对应的接口(IFoo、IBar和IBaz),并在服务类型上标注MapToAttribute特性来定义服务注册信息。

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类型表示承载的服务。我们在构造函数中注入了IFoo、IBar和IBaz对象,构造函数提供的调试断言用于验证上述三个服务被成功注入。

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

我们在如下的演示程序中创建了一个IHostBuilder对象,通过调用其ConfigureServices方法注册了需要承载的FakeHostedService服务后,我们调用它的UseServiceProviderFactory方法完成了对CatServiceProvider的注册。我们随后调用了CatBuilder的Register方法完成了针对入口程序集的批量服务注册。调用IHostBuilder的Build方法构建出作为宿主的IHost对象并启动它之后,承载的FakeHostedService服务将自动被创建并启动(S1408)。

using App;
using System.Reflection;

Host.CreateDefaultBuilder()
    .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>())
    .UseServiceProviderFactory(new CatServiceProviderFactory())
.ConfigureContainer<CatBuilder>(
    builder => builder.Register(Assembly.GetEntryAssembly()!))
    .Build()
    .Run();

[S1409]利用配置初始化承载环境

一个HostBuilderContext上下文由承载针对宿主配置的IConfiguration对象和描述当前承载环境的IHostEnvironment对象组成,后者提供的环境名称、应用名称和内容文件根目录路径可以通过前者来指定,具体的配置项名称定义在如下这个静态类型HostDefaults中。

public static class HostDefaults
{
    public static readonly string EnvironmentKey = "environment";
    public static readonly string ContentRootKey = "contentRoot";
    public static readonly string ApplicationKey = "applicationName";
}

下面我们通过一个简单的实例演示如何利用配置的方式来指定上述三个与承载环境相关的属性。我们定义了如下一个名为FakeHostedService的承载服务,并在构造函数中注入IHostEnvironment对象。FakeHostedService派生于抽象类BackgroundService,我们在在ExecuteAsync方法中将与承载环境相关的环境名称、应用名称和内容文件根目录路径输出到控制台上。

public class FakeHostedService : BackgroundService
{
    private readonly IHostEnvironment _environment;
    public FakeHostedService(IHostEnvironment environment) => _environment = environment;
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine("{0,-15}:{1}", nameof(_environment.EnvironmentName), _environment.EnvironmentName);
        Console.WriteLine("{0,-15}:{1}", nameof(_environment.ApplicationName),_environment.ApplicationName);
        Console.WriteLine("{0,-15}:{1}", nameof(_environment.ContentRootPath),_environment.ContentRootPath);
        return Task.CompletedTask;
    }
}

FakeHostedService采用如下形式进行承载。如代码片段所示,为了避免输出日志的“干扰”,我们调用IHostBuilder接口的ConfigureLogging扩展方法将注册的ILoggerProvider对象全部清除。如果调用Host静态类型的CreateDefaultBuilder方法时传入当前的命令行参数,创建的IHostBuilder对象会将其作为配置源,所以我们就能以命令行参数的形式来指定承载上下文的三个属性。

using App;
Host.CreateDefaultBuilder(args)
    .ConfigureLogging(logging=>logging.ClearProviders())
    .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>())
    .Build()
    .Run();

我们采用命令行的方式启动这个演示程序,并利用传入的命令行参数指定环境名称、应用名称和内容文件根目录路径(确保路径确实存在)。图2所示的输出结果表明,应用程序当前的承载环境与基于宿主的配置是一致的。

图2利用配置来初始化承载环境