Host Genérico .NET

Neste artigo, você aprenderá sobre os vários padrões para configuração e criação de um host genérico .NET disponível no pacote NuGet Microsoft.Extensions.Hosting. O Host Genérico do .NET é responsável pela inicialização e gerenciamento do tempo de vida do aplicativo. Os modelos do Serviço de Trabalho criam um host genérico .NET. HostApplicationBuilder O Host Genérico pode ser usado com outros tipos de aplicativos .NET, como aplicativos de console.

Um host é um objeto que encapsula os recursos e a funcionalidade de tempo de vida de um aplicativo, tais como:

  • DI (injeção de dependência)
  • Log
  • Configuração
  • Desligamento do aplicativo
  • Implementações de IHostedService

Quando um host é iniciado, ele chama IHostedService.StartAsync em cada implementação de IHostedService registrada na coleção de serviços hospedados do contêiner de serviço. Em um aplicativo de serviço de trabalho, todas as implementações IHostedService que contêm instâncias BackgroundService têm seus métodos BackgroundService.ExecuteAsync chamados.

O principal motivo para incluir todos os recursos interdependentes do aplicativo em um objeto é o gerenciamento de tempo de vida: controle sobre a inicialização do aplicativo e desligamento normal.

Configurar um host

O host normalmente é configurado, compilado e executado pelo código na classe Program. O método Main:

Os modelos do Serviço de Trabalho do .NET geram o seguinte código para criar um Host Genérico:

using Example.WorkerService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

IHost host = builder.Build();
host.Run();

Para obter mais informações sobre as Funções de Trabalho, confira Serviços de Trabalho no .NET.

Configurações do construtor de host

O método CreateApplicationBuilder:

  • Define a raiz do conteúdo como o caminho retornado por GetCurrentDirectory().
  • Carrega a configuração do host de:
    • Variáveis de ambiente prefixadas com DOTNET_.
    • Argumentos de linha de comando.
  • Carrega a configuração do aplicativo de:
    • appsettings.json.
    • appsettings.{Environment}.json.
    • Gerenciador de Segredo quando o aplicativo é executado no ambiente Development.
    • Variáveis de ambiente.
    • Argumentos de linha de comando.
  • Adiciona os seguintes provedores de registro em log:
    • Console
    • Depurar
    • EventSource
    • EventLog (somente quando em execução no Windows)
  • Habilita a validação de escopo e a validação de dependência quando o ambiente é Development.

HostApplicationBuilder.Services é uma instância de Microsoft.Extensions.DependencyInjection.IServiceCollection. Esses serviços são usados para criar um IServiceProvider usado com a injeção de dependência para resolver os serviços registrados.

Serviços fornecidos pela estrutura

Ao chamar IHostBuilder.Build() ou HostApplicationBuilder.Build(), os seguintes serviços são registrados automaticamente:

Construtores de host baseados em cenários adicionais

Se você estiver criando para a Web ou escrevendo um aplicativo distribuído, talvez seja necessário usar um construtor de host diferente. Considere a seguinte lista de construtores de host adicionais:

IHostApplicationLifetime

Injete o serviço IHostApplicationLifetime em qualquer classe para lidar com tarefas de pós-inicialização e de desligamento normal. Três propriedades na interface são tokens de cancelamento usados para registrar métodos de manipulador de eventos de inicialização e desligamento do aplicativo. A interface também inclui um método StopApplication().

O exemplo a seguir é uma implementação IHostedService e IHostedLifecycleService que registra eventos IHostApplicationLifetime:

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

namespace AppLifetime.Example;

public sealed class ExampleHostedService : IHostedService, IHostedLifecycleService
{
    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);
    }

    Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("1. StartingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("2. StartAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("3. StartedAsync has been called.");

        return Task.CompletedTask;
    }

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

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

    Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("6. StoppingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("7. StopAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("8. StoppedAsync has been called.");

        return Task.CompletedTask;
    }

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

O modelo do Serviço de Trabalho pode ser modificado para adicionar a implementação ExampleHostedService:

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<ExampleHostedService>();
using IHost host = builder.Build();

await host.RunAsync();

O aplicativo gravaria a seguinte saída de exemplo:

// Sample output:
//     info: AppLifetime.Example.ExampleHostedService[0]
//           1.StartingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           2.StartAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           3.StartedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           4.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\net8.0
//     info: AppLifetime.Example.ExampleHostedService[0]
//           5.OnStopping has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application is shutting down...
//     info: AppLifetime.Example.ExampleHostedService[0]
//           6.StoppingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           7.StopAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           8.StoppedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           9.OnStopped has been called.

A saída mostra a ordem de todos os vários eventos de ciclo de vida:

  1. IHostedLifecycleService.StartingAsync
  2. IHostedService.StartAsync
  3. IHostedLifecycleService.StartedAsync
  4. IHostApplicationLifetime.ApplicationStarted

Quando o aplicativo é interrompido, por exemplo, com Ctrl+C, os seguintes eventos são gerados:

  1. IHostApplicationLifetime.ApplicationStopping
  2. IHostedLifecycleService.StoppingAsync
  3. IHostedService.StopAsync
  4. IHostedLifecycleService.StoppedAsync
  5. IHostApplicationLifetime.ApplicationStopped

IHostLifetime

A implementação IHostLifetime controla quando o host é iniciado e quando ele é interrompido. A última implementação registrada é usada. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime é a implementação IHostLifetime padrão. Para obter mais informações sobre a mecânica de tempo de vida do desligamento, consulte Desligamento do host.

A interface IHostLifetime expõe um método IHostLifetime.WaitForStartAsync, que é chamado no início de IHost.StartAsync que aguardará até que ele seja concluído antes de continuar. Isso pode ser usado para atrasar a inicialização até que seja sinalizado por um evento externo.

Além disso, a interface IHostLifetime expõe um método IHostLifetime.StopAsync, que é chamado de IHost.StopAsync para indicar que o host está parando e é hora de desligar.

IHostEnvironment

Injete o serviço IHostEnvironment em uma classe para obter informações sobre as seguintes configurações:

Além disso, o serviço IHostEnvironment expõe a capacidade de avaliar o ambiente com a ajuda desses métodos de extensão:

Configuração do host

A configuração do host é usada para configurar propriedades da implementação IHostEnvironment.

A configuração do host está disponível na propriedade HostApplicationBuilderSettings.Configuration e a implementação do ambiente está disponível na propriedade IHostApplicationBuilder.Environment. Para configurar o host, acesse a propriedade Configuration e chame qualquer um dos métodos de extensão disponíveis.

Para adicionar a configuração do host, considere o seguinte exemplo:

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

HostApplicationBuilderSettings settings = new()
{
    Args = args,
    Configuration = new ConfigurationManager(),
    ContentRootPath = Directory.GetCurrentDirectory(),
};

settings.Configuration.AddJsonFile("hostsettings.json", optional: true);
settings.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
settings.Configuration.AddCommandLine(args);

HostApplicationBuilder builder = Host.CreateApplicationBuilder(settings);

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

O código anterior:

  • Define a raiz do conteúdo como o caminho retornado por GetCurrentDirectory().
  • Carrega a configuração do host de:
    • hostsettings.json.
    • Variáveis de ambiente prefixadas com PREFIX_.
    • Argumentos de linha de comando.

Configuração do aplicativo

A configuração do aplicativo é criada chamando ConfigureAppConfiguration em um IHostApplicationBuilder. A propriedade públicaIHostApplicationBuilder.Configuration permite que os consumidores leiam ou façam alterações na configuração existente usando métodos de extensão disponíveis.

Para obter mais informações, confira Configuração no .NET.

Desligamento do host

Há várias maneiras como um processo hospedado é interrompido. O mais comum é que um processo de serviço hospedado possa ser interrompido das seguintes maneiras:

O código de hospedagem não é responsável por lidar com esses cenários. O proprietário do processo precisa lidar com eles da mesma forma que qualquer outro aplicativo. Há várias outras maneiras como um processo de serviço hospedado pode ser interrompido:

  • Se ConsoleLifetime for usado (UseConsoleLifetime), ele escutará os sinais a seguir e tentará interromper o host normalmente.
    • SIGINT (ou CTRL+C).
    • SIGQUIT (ou CTRL+BREAK no Windows, CTRL+\ no Unix).
    • SIGTERM (enviado por outros aplicativos, como docker stop).
  • Se o aplicativo chamar Environment.Exit.

A lógica de hospedagem interna lida com esses cenários, especificamente a classe ConsoleLifetime. ConsoleLifetime tenta lidar com os sinais de "desligamento" SIGINT, SIGQUIT e SIGTERM para permitir uma saída normal para o aplicativo.

Antes do .NET 6, não havia uma maneira de o código .NET manipular normalmente o SIGTERM. Para contornar essa limitação, ConsoleLifetime assinaria para System.AppDomain.ProcessExit. Quando ProcessExit era acionado, ConsoleLifetime sinalizaria que o host parasse e bloqueasse o thread ProcessExit, aguardando o host parar.

O manipulador de saída do processo permitiria que o código de limpeza no aplicativo fosse executado , por exemplo, IHost.StopAsync e código depois de HostingAbstractionsHostExtensions.Run no método Main.

No entanto, havia outros problemas com essa abordagem porque SIGTERM não era a única maneira de gerar ProcessExit. SIGTERM também é gerado quando o código do aplicativo chama Environment.Exit. Environment.Exit não é uma maneira normal de desligar um processo no modelo de aplicativo Microsoft.Extensions.Hosting. Ele aciona o evento ProcessExit e, em seguida, sai do processo. O final do método Main não é executado. Os threads em segundo e primeiro plano são encerrados e os blocosfinallynão são executados.

Como ConsoleLifetime bloqueava ProcessExit enquanto esperava o host ser desligado, esse comportamento fazia com que deadlocks de Environment.Exit também bloqueassem a espera pela chamada ProcessExit. Além disso, como a manipulação do SIGTERM estava tentando desligar o processo normalmente, ConsoleLifetime definia ExitCode para 0, o que sobrecarregava o código de saída do usuário passado para Environment.Exit.

No .NET 6, os sinais POSIX têm suporte e são manipulados. O ConsoleLifetime manipula SIGTERM normalmente e não se envolve mais quando Environment.Exit é invocado.

Dica

No .NET 6+, ConsoleLifetime não tem mais lógica para lidar com o cenário Environment.Exit. Os aplicativos que chamam Environment.Exit e precisam executar a lógica de limpeza podem se inscrever em ProcessExit sozinhos. A hospedagem não tentará mais interromper normalmente o host nesses cenários.

Se o aplicativo usa hospedagem e você deseja interromper normalmente o host, você pode chamar IHostApplicationLifetime.StopApplication em vez de Environment.Exit.

Processo de desligamento de hospedagem

O diagrama de sequência a seguir mostra como os sinais são tratados internamente no código de hospedagem. A maioria dos usuários não precisa entender esse processo. Mas para desenvolvedores que precisam de uma compreensão profunda, um bom visual pode ajudá-los a começar.

Depois que o host for iniciado, quando um usuário chamar Run ou WaitForShutdown, um manipulador se registra em IApplicationLifetime.ApplicationStopping. A execução está pausada em WaitForShutdown, aguardando o evento ApplicationStopping ser acionado. O método Main não retorna imediatamente e o aplicativo permanece em execução até Run ou WaitForShutdown retorna.

Quando um sinal é enviado para o processo, ele inicia a seguinte sequência:

Diagrama da sequência de desligamento de hospedagem.

  1. O controle flui de ConsoleLifetime para ApplicationLifetime para acionar o evento ApplicationStopping. Isso sinaliza WaitForShutdownAsync para desbloquear o código de execução Main. Enquanto isso, o manipulador de sinal POSIX retorna Cancel = true desde que esse sinal POSIX tenha sido tratado.
  2. O código de execução Main começa a ser executado novamente e informa ao host para StopAsync(), o qual, por sua vez, interrompe todos os serviços hospedados e gera quaisquer outros eventos parados.
  3. Por fim, WaitForShutdown sai, permitindo que qualquer código de limpeza de aplicativo seja executado e para que o método Main saia normalmente.

Desligamento de host em cenários de servidor Web

Há vários outros cenários comuns em que o desligamento normal funciona no Kestrel para os protocolos HTTP/1.1 e HTTP/2, e como você pode configurá-lo em ambientes diferentes com um balanceador de carga para esvaziar o tráfego sem problemas. Embora a configuração do servidor Web esteja além do escopo deste artigo, você pode encontrar mais informações na documentação Configurar opções para o servidor Web ASP.NET Core Kestrel.

Quando o Host recebe um sinal de desligamento (por exemplo, CTL+C ou StopAsync), ele notifica o aplicativo sinalizando ApplicationStopping. Você deverá assinar esse evento se tiver operações de execução longa que precisem ser concluídas normalmente.

Em seguida, o Host chama IServer.StopAsync com um tempo limite de desligamento que você pode configurar (30s por padrão). Kestrel (e Http.Sys) fecham suas associações de porta e param de aceitar novas conexões. Eles também informam as conexões atuais para interromper o processamento de novas solicitações. Para HTTP/2 e HTTP/3, uma mensagem GOAWAY preliminar é enviada ao cliente. Para HTTP/1.1, eles interrompem o loop de conexão porque as solicitações são processadas em ordem. O IIS se comporta de modo diferente, rejeitando novas solicitações com um código de status 503.

As solicitações ativas têm até o tempo limite de desligamento para serem concluídas. Se todas foram concluídas antes do tempo limite, o servidor retornará o controle para o host mais cedo. Se o tempo limite expirar, as conexões e solicitações pendentes serão anuladas forçadamente, o que pode causar erros nos logs e nos clientes.

Considerações do Balanceador de carga

Para garantir uma transição suave de clientes para um novo destino ao trabalhar com um balanceador de carga, você pode seguir estas etapas:

  • Abra a nova instância e comece a balancear o tráfego para ela (talvez você já tenha várias instâncias para fins de dimensionamento).
  • Desabilite ou remova a instância antiga na configuração do balanceador de carga para que ela pare de receber novo tráfego.
  • Sinalize a instância antiga para desligar.
  • Aguarde até ela esvaziar ou atingir o tempo limite.

Confira também