.NET 通用主机

辅助角色服务模板会创建一个 .NET 通用主机 HostBuilder。 通用主机可用于其他类型的 .NET 应用程序,如控制台应用。

主机是封装应用资源和生存期功能的对象,例如:

  • 依赖关系注入 (DI)
  • Logging
  • Configuration
  • 应用关闭
  • IHostedService 实现

当主机启动时,它将对在托管服务的服务容器集合中注册的 IHostedService 的每个实现调用 IHostedService.StartAsync。 在辅助角色服务应用中,包含 BackgroundService 实例的所有 IHostedService 实现都调用其 BackgroundService.ExecuteAsync 方法。

一个对象中包含所有应用的相互依赖资源的主要原因是生存期管理:控制应用启动和正常关闭。 这是通过 Microsoft.Extensions.Hosting NuGet 包实现的。

设置主机

主机通常由 Program 类中的代码配置、生成和运行。 Main 方法:

.NET 辅助角色服务模板会生成以下代码来创建通用主机:

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<Worker>();
    })
    .Build();

host.Run();

默认生成器设置

CreateDefaultBuilder 方法:

  • 将内容根路径设置为由 GetCurrentDirectory() 返回的路径。
  • 通过以下对象加载主机配置
    • 前缀为 DOTNET_ 的环境变量。
    • 命令行参数。
  • 通过以下对象加载应用配置:
    • appsettings.json。
    • appsettings.{Environment}.json。
    • 密钥管理器 当应用在 Development 环境中运行时。
    • 环境变量。
    • 命令行参数。
  • 添加以下日志记录提供程序:
    • 控制台
    • 调试
    • EventSource
    • EventLog(仅当在 Windows 上运行时)
  • 当环境为 Development 时,启用范围验证和依赖关系验证

ConfigureServices 方法公开了向 Microsoft.Extensions.DependencyInjection.IServiceCollection 实例添加服务的功能。 以后,可以通过依赖关系注入获取这些服务。

框架提供的服务

自动注册以下服务:

IHostApplicationLifetime

IHostApplicationLifetime 服务注入任何类以处理启动后和正常关闭任务。 接口上的三个属性是用于注册应用启动和应用停止事件处理程序方法的取消令牌。 该接口还包括 StopApplication() 方法。

以下示例是注册 IHostApplicationLifetime 事件的 IHostedService 实现:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AppLifetime.Example;

public sealed class ExampleHostedService : IHostedService
{
    private readonly ILogger _logger;

    public ExampleHostedService(
        ILogger<ExampleHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;

        appLifetime.ApplicationStarted.Register(OnStarted);
        appLifetime.ApplicationStopping.Register(OnStopping);
        appLifetime.ApplicationStopped.Register(OnStopped);
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("1. StartAsync has been called.");

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("4. StopAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("2. OnStarted has been called.");
    }

    private void OnStopping()
    {
        _logger.LogInformation("3. OnStopping has been called.");
    }

    private void OnStopped()
    {
        _logger.LogInformation("5. OnStopped has been called.");
    }
}

可以修改辅助角色服务模板以添加 ExampleHostedService 实现:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AppLifetime.Example;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((_, services) =>
        services.AddHostedService<ExampleHostedService>())
    .Build();

await host.RunAsync();

应用程序会编写以下示例输出:

// Sample output:
//     info: ExampleHostedService[0]
//           1. StartAsync has been called.
//     info: ExampleHostedService[0]
//           2. OnStarted has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application started.Press Ctrl+C to shut down.
//     info: Microsoft.Hosting.Lifetime[0]
//           Hosting environment: Production
//     info: Microsoft.Hosting.Lifetime[0]
//           Content root path: ..\app-lifetime\bin\Debug\net6.0
//     info: ExampleHostedService[0]
//           3. OnStopping has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application is shutting down...
//     info: ExampleHostedService[0]
//           4. StopAsync has been called.
//     info: ExampleHostedService[0]
//           5. OnStopped has been called.

IHostLifetime

IHostLifetime 实现控制主机何时启动和何时停止。 使用了已注册的最后一个实现。 Microsoft.Extensions.Hosting.Internal.ConsoleLifetime 是默认的 IHostLifetime 实现。 有关关闭的生存期机制的详细信息,请参阅主机关闭

IHostEnvironment

IHostEnvironment 服务注册到一个类,获取关于以下设置的信息:

主机配置

主机配置用于配置 IHostEnvironment 实现的属性。

主机配置在 ConfigureAppConfiguration 方法的 HostBuilderContext.Configuration 中可用。 调用 ConfigureAppConfiguration 方法时,HostBuilderContextIConfigurationBuilder 将传递到 configureDelegate 中。 configureDelegate 被定义为 Action<HostBuilderContext, IConfigurationBuilder>。 主机生成器上下文公开 Configuration 属性,该属性是 IConfiguration 的一个实例。 它表示从主机生成的配置,而 IConfigurationBuilder 是用于配置应用的生成器对象。

提示

在调用 ConfigureAppConfiguration 后,HostBuilderContext.Configuration 被替换为应用配置

若要添加主机配置,请对 IHostBuilder 调用 ConfigureHostConfiguration。 可多次调用 ConfigureHostConfiguration,并得到累计结果。 主机使用上一次在一个给定键上设置值的选项。

以下示例创建主机配置:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.SetBasePath(Directory.GetCurrentDirectory());
        configHost.AddJsonFile("hostsettings.json", optional: true);
        configHost.AddEnvironmentVariables(prefix: "PREFIX_");
        configHost.AddCommandLine(args);
    })
    .Build();

// Application code should start here.

await host.RunAsync();

应用配置

通过对 IHostBuilder 调用 ConfigureAppConfiguration 创建应用配置。 可多次调用 ConfigureAppConfiguration,并得到累计结果。 应用使用上一次在一个给定键上设置值的选项。

可以在 HostBuilderContext.Configuration 中获取由 ConfigureAppConfiguration 创建的配置以用于后续操作,并将其作为 DI 的服务。 主机配置也会添加到应用配置。

有关详细信息,请参阅 .NET 中的配置

主机关闭

在下列情况中,可以停止托管服务进程:

以上场景都不是由主机代码直接处理的。 进程所有者需要以处理应用程序的相同方式来处理它们。 在其他几种情况下也可以停止托管服务进程:

  • 使用 ConsoleLifetime 时,它将侦听以下信号,并尝试正常停止主机。
    • SIGINT(或 CTRL+C)。
    • SIGQUIT(或 Windows 上的 CTRL+BREAK,Unix 上的 CTRL+\)。
    • SIGTERM(由其他应用发送,如 docker stop)。
  • 应用调用 Environment.Exit

以上场景都是由内置的主机逻辑(具体来说,是 ConsoleLifetime 类)处理的。 ConsoleLifetime 尝试处理“关闭”信号 SIGINT、SIGQUIT 和 SIGTERM,以支持正常退出应用程序。

在 .NET 6 之前,.NET 代码无法正常地处理 SIGTERM。 为了绕开此限制,ConsoleLifetime 需要订阅 System.AppDomain.ProcessExit。 如果引发了 ProcessExitConsoleLifetime 会发出信号通知主机停止并阻止 ProcessExit 线程,等待主机停止。 这样,应用程序中的清理代码便可运行,例如 IHost.StopAsyncMain 方法中 HostingAbstractionsHostExtensions.Run 之后的代码。

这会导致其他问题,因为 SIGTERM 不是引发 ProcessExit 的唯一方法。 它还由应用程序中调用 Environment.Exit 的代码引发。 Environment.Exit 不是在 Microsoft.Extensions.Hosting 应用模型中正常关闭进程的方法。 它将引发 ProcessExit 事件,并退出该进程。 Main 方法的末尾不会执行。 后台和前台线程会终止,并且 finally 块不会执行。

由于 ConsoleLifetime 在等待主机关闭期间阻止 ProcessExit,此行为导致 Environment.Exit死锁,也阻止等待对 ProcessExit 的调用。 此外,由于 SIGTERM 处理尝试正常关闭进程,因此 ConsoleLifetime 会将 ExitCode 设置为 0,这会强制改写传递给 Environment.Exit 的用户退出代码。

在 .NET 6 中,支持 POSIX 信号并可对其进行处理。 这样,ConsoleLifetime 可正常处理 SIGTERM,并在调用 Environment.Exit 时不再涉及其中。

提示

对于 .NET 6+,ConsoleLifetime 不再具有处理场景 Environment.Exit 的逻辑。 调用 Environment.Exit 并需要执行清理逻辑的应用可以自行订阅 ProcessExit。 在这种情况下,主机代码将不再尝试正常停止主机。

如果应用程序使用主机代码,并且你希望正常停止主机,可调用 IHostApplicationLifetime.StopApplication 而不是 Environment.Exit

主机代码关闭过程

下面的顺序图演示了如何在主机代码中内部处理信号。 大多数用户不需要了解此过程。 但对于需要深入了解的开发人员而言,这有助于入门。

启动主机后,用户调用 RunWaitForShutdown 时,处理程序会注册 IApplicationLifetime.ApplicationStopping。 执行在 WaitForShutdown 中暂停,等待引发 ApplicationStopping 事件。 因此,Main 方法不会立即返回,并且应用会一直运行,直到 RunWaitForShutdown 返回。

信号发送到进程时,它将启动以下序列:

主机代码关闭顺序图。

  1. 控制从 ConsoleLifetime 流向 ApplicationLifetime,引发 ApplicationStopping 事件。 这会指示 WaitForShutdownAsync 解除阻止 Main 执行代码。 同时,由于 POSIX 信号已经过处理,POSIX 信号处理程序返回 Cancel = true
  2. Main 执行代码将再次开始执行,并指示主机 StopAsync(),进而停止所有托管服务,并引发任何其他已停止的事件。
  3. 最后,WaitForShutdown 退出,使任何应用程序清理代码可执行且 Main 方法可正常退出。

另请参阅