关于 .NET Core 中的 Host


前言

当前文章用的 .NET Core SDK 3.1。

ASP.NET Core 应用程序本质上是一个服务,这个服务启动了一个网络监听器,当监听器监听到Http请求以后,将请求传递给应用的管道进行处理,完成处理后生成Http响应。
长时间运行的服务需要寄宿在托管进程中,提供了这个功能的系统称为 Host,Host 实现的功能将一个或多个长时间运行的服务给寄宿的托管进程中,并由 Host 来管理长时间运行的服务,这些服务被称为托管服务。

任何需要在后台长时间运行的程序,我们都可以按照标准把他定义为托管服务将其寄宿在 Host 上面。


一、将托管服务寄宿在 Host 上

托管服务需要实现 IHostedService,程序启动时,Host 才会自动找到注册的所有托管服务并将他们启动。
需要引入相关的NuGet包: dotnet add package Microsoft.Extensions.Hosting -v 3.1.10
这里定义了一个时钟,用来打印当前时间。

    public class SystemClock : IHostedService
    {
    
    
        private Timer _timer;
        public Task StartAsync(CancellationToken cancellationToken)
        {
    
    
            _timer = new Timer(state =>
            {
    
    
                Console.WriteLine($"Current Time:{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
            }, null, 0, 1000);
            return Task.CompletedTask;
        }

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

然后实例化一个主机构建器(HostBuilder),并使用 ConfigureServices 方法给主机构建器配置服务,
主机构建器整合了依赖注入框架,这里的 collection 参数是服务注册对象(IServiceCollection)。
托管服务的容器最终也是依赖注入框架来提供的,托管服务自身所托管的服务也可以注册到依赖注入框架中。
然后在服务注册对象中注册 时钟服务(SystemClock)。
最后通过主机构建器创建主机对象,并调用 Run 方法运行主机,这时托管服务也会被启动。

    static void Main(string[] args)
    {
    
    
        IHostBuilder hostBuilder = new HostBuilder()
            .ConfigureServices(collection =>
            {
    
    
                collection.AddHostedService<SystemClock>();
               	// 下面注释的代码和上一行代码效果相同
                // collection.AddSingleton<IHostedService, SystemClock>();
            });
    
        IHost host = hostBuilder.Build();
        host.Run();
    }

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

二、在服务注册对象中注册服务

添加一个时间服务类:

    public class DateTimeService
    {
    
    
        public string GetDateTimeNow() => $"{DateTime.Now:yyyy-MM-dd HH:mm:ss}";
    }

然后将其注册到服务注册对象中:

    IHostBuilder hostBuilder = new HostBuilder()
        .ConfigureServices(collection =>
        {
    
    
            collection.AddHostedService<SystemClock>();
            // 下面注释的代码和上一行代码效果相同
            // collection.AddSingleton<IHostedService, SystemClock>();
            collection.AddSingleton<DateTimeService>();
        });

然后将 DateTimeService 通过 SystemClock 的构造函数注入到 SystemClock 对象中,
并更改打印时间的代码为:

	Console.WriteLine($"Current Time:{_dateTimeService.GetDateTimeNow()}");

三、使用配置和选项服务

1. 添加配置

在 Host 中有两种注册配置的函数,ConfigureHostConfigurationConfigureAppConfiguration,其中 ConfigureHostConfigurationConfigureAppConfiguration 之前执行。
它们之间的关系是 ConfigureHostConfiguration 会初始化 IHostingEnvironment,然后 ConfigureAppConfiguration 的上下中能够拿到初始化过的配置,最终的配置是两个配置方法配置之和,当然,后者将会覆盖前者的相同配置。
例如:在 ASP.NET Core 应用程序中,我们可以在系统的环境变量配置 ENVIRONMENT=Development, 然后在项目文件~\Properties\launchSettings.json 中配置 "ASPNETCORE_ENVIRONMENT": "Production",最后我们可以看到后者将前者值进行了覆盖,然后我们又可以通过我们设置的环境变量参数来加载争对不同环境的配置。

在项目中增加四个Json文件,参数Interval配置打印的时间间隔:

#region launchSettings.json 文件的内容
	{
    
     "ENVIRONMENT": "Development" }
#endregion

#region appsettings.json 文件的内容
	{
    
     "Interval": 1000 }
#endregion

#region appsettings.Development.json 文件的内容
	{
    
     "Interval": 2000 }
#endregion

#region appsettings.Production.json 文件的内容
	{
    
     "Interval": 3000 }
#endregion

然后我们修改主机构建器的配置:

    IHostBuilder hostBuilder = new HostBuilder()
        .ConfigureHostConfiguration(builder => builder
            .AddJsonFile("launchSettings.json")
        )
        .ConfigureAppConfiguration((context, builder) => builder
            .AddJsonFile("appsettings.json")
            .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json")
            // 下面注释的代码和上一行代码效果相同
            // .AddJsonFile($"appsettings.{context.Configuration["ENVIRONMENT"]}.json")
        )
        .ConfigureServices(collection =>
        {
    
    
            collection.AddHostedService<SystemClock>();
            // 下面注释的代码和上一行代码效果相同
            // collection.AddSingleton<IHostedService, SystemClock>();
            collection.AddSingleton<DateTimeService>();
        });

在上面代码中,使用 ConfigureHostConfiguration 方法添加当前环境变量为 Development,然后通过 ConfigureAppConfiguration 重载方法(这个重载的context可以拿到前面的配置)加载不同环境对应的配置。

2. 选项服务

增加一个配置对象的类:

    public class AppSettings
    {
    
    
        public string Environment {
    
     get; set; }
        public int Interval {
    
     get; set; }
    }

在服务注册对象中配置选项服务:

    IHostBuilder hostBuilder = new HostBuilder()
        .ConfigureHostConfiguration(builder => builder
            .AddJsonFile("Properties/launchSettings.json")
        )
        .ConfigureAppConfiguration((context, builder) => builder
            .AddJsonFile("appsettings.json")
            .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json")
        // 上面一句配置与下面一句效果相同
        //.AddJsonFile($"appsettings.{context.Configuration["ENVIRONMENT"]}.json")
        )
        .ConfigureServices((context, collection) => collection
            .AddHostedService<SystemClock>()
            // 下面注释的代码和上一行代码效果相同
            // .AddSingleton<IHostedService, SystemClock>();
            .AddSingleton<DateTimeService>()
            .AddOptions()
            .Configure<AppSettings>(context.Configuration)
        );

然后通过将 IOptions 或者 IConfiguration 将其注入到 SystemClock 对象中设置打印时间的时间间隔,
现在 SystemClock 的全部代码应该为:

    public class SystemClock : IHostedService
    {
    
    
        private readonly DateTimeService _dateTimeService;
        private readonly IConfiguration _configuration;
        private readonly AppSettings _appSettings;

        public SystemClock(
            IConfiguration configuration,
            DateTimeService dateTimeService,
            IOptions<AppSettings> options)
        {
    
    
            _configuration = configuration;
            _dateTimeService = dateTimeService;
            _appSettings = options.Value;
        }

        private Timer _timer;

        public Task StartAsync(CancellationToken cancellationToken)
        {
    
    
            var interval = _configuration["Interval"]; // 2000
            var environment = _configuration["ENVIRONMENT"]; // Development

            _timer = new Timer(state =>
            {
    
    
                Console.WriteLine($"Current Time:{_dateTimeService.GetDateTimeNow()}");
            }, null, 0, _appSettings.Interval);

            return Task.CompletedTask;
        }

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

启动项目,我们可以看到打印时间的时间间隔为两秒。

四、将日志功能添加到 Host 中

使用日志功能需要添加NuGet,我这里用的是 Microsoft.Extensions.Logging.Console,使用命令 dotnet add package Microsoft.Extensions.Logging.Console -v 3.1.10 进行安装。
在 主机构建器 中使用 ConfigureLogging 配置日志,该方法也有重载,能够拿到上下文中的配置。
修改代码:

    IHostBuilder hostBuilder = new HostBuilder()
        .ConfigureHostConfiguration(builder => builder
            .AddJsonFile("Properties/launchSettings.json")
        )
        .ConfigureAppConfiguration((context, builder) => builder
            .AddJsonFile("appsettings.json")
            .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json")
        // 上面一句配置与下面一句效果相同
        //.AddJsonFile($"appsettings.{context.Configuration["ENVIRONMENT"]}.json")
        )
        .ConfigureServices((context, collection) => collection
            .AddHostedService<SystemClock>()
            // 下面注释的代码和上一行代码效果相同
            // .AddSingleton<IHostedService, SystemClock>();
            .AddSingleton<DateTimeService>()
            .AddOptions()
            .Configure<AppSettings>(context.Configuration)
        ) 
        .ConfigureLogging(builder => builder.AddConsole());

然后将对象 ILogger<SystemClock> _logger 注入到 SystemClock 中,
最后修改打印时间的代码为:

    _timer = new Timer(state =>
    {
    
    
        _logger.LogInformation($"Current Time:{_dateTimeService.GetDateTimeNow()}");
    }, null, 0, _appSettings.Interval);

启动项目,即可看到打印的日志系统。

五、HostApplicationLifetime 的简单使用

下面是一个托管服务,该服务注入了 IHostApplicationLifetime 对象,在服务的构造函数中设置了应用程序启动或停止进行打印。
在托管服务启动以后,两秒后调用 IHostApplicationLifetime 对象的 StopApplication 方法将程序关闭。

    public class TestService : IHostedService
    {
    
    
        private readonly IHostApplicationLifetime _lifetime;
        public TestService(IHostApplicationLifetime lifetime)
        {
    
    
            _lifetime = lifetime;
            _lifetime.ApplicationStarted.Register(() => Console.WriteLine($"{DateTimeOffset.Now} 应用程序已启动"));
            _lifetime.ApplicationStopping.Register(() => Console.WriteLine($"{DateTimeOffset.Now} 开始停止应用程序"));
            _lifetime.ApplicationStopped.Register(() => Console.WriteLine($"{DateTimeOffset.Now} 应用程序已停止"));
        }
    
        public Task StartAsync(CancellationToken cancellationToken)
        {
    
    
            var task = Task.Delay(2000).ContinueWith(t => _lifetime.StopApplication());
            return Task.CompletedTask;
        }
    
        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }

该实例我们看到,我们可以通过 IHostApplicationLifetimeStopApplication 来关闭应用程序,
并且还利用 IHostApplicationLifetime 提供的三个令牌对象,向 IHostApplicationLifetime 的不同生命周期事件中注册我们自己的方法,来做一些我们希望做得事情。


总结

Host 有三个主要对象,托管服务(IHostedService)、主机对象(IHost)、主机构建器(IHostBuilder),
IHost 启动以后,将会一直等待应用程序关闭的通知,IHostBuilder 则是 IHost 的构建器,IHost 上运行一个或多个 IHostedService。

当 IHost 启动时(执行IHost.StartAsync方法),利用依赖注入框架获取到所有注册的 IHostedService ,并通过 IHostedService.StartAsync 方法来启动它们,
当应用程序被关闭的时候,IHost 也会被关闭(执行IHost.StopAsync方法),被 IHost 启动的所有 IHostedService 也都会调用 IHostedService.StopAsync 方法来停止服务(IHostedService列表将会被反转,最后Start的IHostedService将最先Stop)。

由于 IHost 集成了 .net core 的依赖注入框架,托管服务所需的依赖服务也包括了 IHost 自身所依赖的服务,也都由这个框架提供,托管服务注册的本质就是将对应的 IHostedService 注册到依赖注入框架中,由于托管服务需要长时间运行,所以一般托管服务采用单例注册。

猜你喜欢

转载自blog.csdn.net/Upgrader/article/details/112256669