Host genérico de .NET

En este artículo, obtendrá información sobre los distintos patrones para configurar y compilar un host genérico de .NET disponible en el paquete NuGet Microsoft.Extensions.Hosting. El host genérico de .NET es responsable de la administración del inicio y la duración de la aplicación. Las plantillas de servicio de trabajo crean un host genérico de .NET, HostApplicationBuilder. El host genérico se puede usar con otros tipos de aplicaciones .NET, como aplicaciones de consola.

El host es un objeto que encapsula los recursos y la funcionalidad de vigencia de una aplicación, como:

  • Inserción de dependencias (ID)
  • Registro
  • Configuración
  • Apagado de la aplicación
  • Implementaciones de IHostedService

Cuando se inicia un host, llama a IHostedService.StartAsync en cada implementación de IHostedService registrada en la colección de servicios hospedados del contenedor de servicios. En una aplicación de servicio de trabajo, todas las implementaciones de IHostedService que contienen instancias de BackgroundService tienen los métodos BackgroundService.ExecuteAsync a los que se llama.

La razón principal para incluir todos los recursos interdependientes de la aplicación en un objeto es la administración de la duración: el control sobre el inicio de la aplicación y el apagado estable.

Configuración de un host

Normalmente se configura, compila y ejecuta el host por el código de la clase Program. El método Main realiza las acciones siguientes:

Las plantillas de servicio de trabajo de .NET generan el código siguiente para crear un host genérico:

using Example.WorkerService;

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

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

Para obtener más información sobre los servicios de trabajo, consulte Servicios de trabajo en .NET.

Configuración del generador de hosts

El método CreateApplicationBuilder realiza las acciones siguientes:

  • Establece la raíz de contenido en la ruta de acceso devuelta por GetCurrentDirectory().
  • Carga la configuración de host de:
    • Variables de entorno con el prefijo DOTNET_.
    • Argumentos de la línea de comandos.
  • Carga la configuración de aplicación de:
    • appsettings.json.
    • appsettings.{Environment}.json.
    • Administrador de secretos, cuando la aplicación se ejecuta en el entorno Development.
    • Variables de entorno.
    • Argumentos de la línea de comandos.
  • Agrega los siguientes proveedores de registro:
    • Consola
    • Depurar
    • EventSource
    • EventLog (solo si se ejecuta en Windows)
  • Permite la validación del ámbito y la validación de dependencias si el entorno es Development.

HostApplicationBuilder.Services es una instancia Microsoft.Extensions.DependencyInjection.IServiceCollection. Estos servicios se usan para crear un IServiceProvider que se usa con la inserción de dependencias para resolver los servicios registrados.

Servicios proporcionados por el marco de trabajo

Al llamar a IHostBuilder.Build() o HostApplicationBuilder.Build(), los siguientes servicios se registran automáticamente:

IHostApplicationLifetime

Permite insertar el servicio IHostApplicationLifetime en cualquier clase para controlar las tareas posteriores al inicio y el cierre estable. Tres de las propiedades de la interfaz son tokens de cancelación que se usan para registrar los métodos del controlador de eventos de inicio y detención de las aplicaciones. La interfaz también incluye un método StopApplication().

El ejemplo siguiente es una implementación de IHostedService y IHostedLifecycleService que registra los 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.");
    }
}

La plantilla de servicio de trabajo se puede modificar para agregar la implementación de 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();

La aplicación escribiría la siguiente salida de ejemplo:

// 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.

La salida muestra el orden de todos los distintos eventos de ciclo de vida:

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

Cuando se detiene la aplicación, por ejemplo, con Ctrl+C, se generan los siguientes eventos:

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

IHostLifetime

La implementación de IHostLifetime controla cuándo se inicia el host y cuándo se detiene. Se usa la última implementación registrada. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime es la implementación predeterminada de IHostLifetime. Para obtener más información sobre la mecánica de vigencia del apagado, consulte Apagado del host.

La interfaz IHostLifetime expone el método IHostLifetime.WaitForStartAsync, al que se llama al inicio de IHost.StartAsync, que espera hasta que se complete antes de continuar. Esto se puede usar para retrasar el inicio hasta que lo indique un evento externo.

Además, la interfaz IHostLifetime expone un método IHostLifetime.StopAsync, al que se llama desde IHost.StopAsync para indicar que el host se está deteniendo y es el momento de apagarse.

IHostEnvironment

Permite insertar el servicio IHostEnvironment en una clase para obtener información sobre los valores siguientes:

Además, el servicio IHostEnvironment expone la capacidad de evaluar el entorno con la ayuda de estos métodos de extensión:

Configuración de host

La configuración de host se usa para configurar las propiedades de la implementación de IHostEnvironment.

La configuración del host está disponible en la propiedad IHostApplicationBuilder.Configuration y la implementación del entorno está disponible en la propiedad IHostApplicationBuilder.Environment. Para configurar el host, acceda a la propiedad Configuration y llame a cualquiera de los métodos de extensión disponibles.

Para agregar la configuración del host, considere el ejemplo siguiente:

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
builder.Configuration.AddCommandLine(args);

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

El código anterior:

  • Establece la raíz de contenido en la ruta de acceso devuelta por GetCurrentDirectory().
  • Carga la configuración de host de:
    • hostsettings.json.
    • Variables de entorno con el prefijo PREFIX_.
    • Argumentos de la línea de comandos.

Configuración de aplicaciones

La configuración de la aplicación se crea llamando a ConfigureAppConfiguration en IHostApplicationBuilder. La propiedad públicaIHostApplicationBuilder.Configuration permite a los consumidores leer la configuración existente o realizar cambios en ella mediante los métodos de extensión disponibles.

Para obtener más información, vea Configuración en .NET.

Apagado del host

Hay varias maneras de detener un proceso hospedado. Por lo general, un proceso hospedado se puede detener de las maneras siguientes:

El código de hospedaje no es responsable de controlar estos escenarios. El propietario del proceso debe tratar con ellos igual que cualquier aplicación. Hay varias maneras adicionales en las que se puede detener un proceso de servicio hospedado:

  • Si se usa ConsoleLifetime (UseConsoleLifetime), escucha las siguientes señales e intenta detener el host correctamente.
    • SIGINT (o CTRL+C).
    • SIGQUIT (o CTRL+BREAK en Windows, CTRL+\ en Unix).
    • SIGTERM (enviado por otras aplicaciones, como docker stop).
  • Si la aplicación llama a Environment.Exit.

La lógica de hospedaje integrada controla estos escenarios, en particular la clase ConsoleLifetime. ConsoleLifetime intenta controlar las señales de "apagado" SIGINT, SIGQUIT y SIGTERM para permitir una salida correcta de la aplicación.

Antes de .NET 6, no había ninguna manera de que el código de .NET controlara correctamente SIGTERM. Para superar esta limitación, ConsoleLifetime se suscribiría a System.AppDomain.ProcessExit. Al generar ProcessExit, ConsoleLifetime señalaría al host que detenga y bloquee el subproceso ProcessExit, esperando a que el host se detenga.

El control de la salida del proceso permitiría que el código de limpieza de la aplicación se ejecutara; por ejemplo, IHost.StopAsync y código después de HostingAbstractionsHostExtensions.Run en el método Main.

Sin embargo, hubo otros problemas con este enfoque porque SIGTERM no fue la única manera en que ProcessExit se generó. SIGTERM también se genera cuando el código de la aplicación llama a Environment.Exit. Environment.Exit no es una manera correcta de cerrar un proceso en el modelo de aplicación Microsoft.Extensions.Hosting. Genera el evento ProcessExit y, a continuación, sale del proceso. El final del método Main no se ejecuta. Los subprocesos en segundo plano y en primer plano finalizan, y los bloques finallyno se ejecutan.

Puesto que ConsoleLifetime bloqueaba a ProcessExit mientras esperaba a que el host se apagase, este comportamiento provocaba interbloqueos de Environment.Exit y bloqueos a la espera de la llamada a ProcessExit. Además, dado que el control SIGTERM estaba intentando cerrar el proceso correctamente, ConsoleLifetime establecería ExitCode en 0, lo que obstruyó el código de salida del usuario que se pasó a Environment.Exit.

En .NET 6, se admiten y controlan las señales POSIX. ConsoleLifetime controla SIGTERM correctamente y ya no se involucra cuando se invoca Environment.Exit.

Sugerencia

Para .NET 6+, ConsoleLifetime ya no tiene lógica para controlar el escenario Environment.Exit. Las aplicaciones que llaman a Environment.Exit y necesitan realizar una lógica de limpieza pueden suscribirse automáticamente a ProcessExit. El hospedaje ya no intentará detener correctamente el host en estos escenarios.

Si la aplicación usa hospedaje, y usted quiere detener correctamente el host, puede llamar a IHostApplicationLifetime.StopApplication en lugar de a Environment.Exit.

Proceso de apagado del hospedaje

En el siguiente diagrama de secuencia se muestra cómo se controlan internamente las señales en el código de hospedaje. La mayoría de los usuarios no necesita comprender este proceso. Pero para los desarrolladores que necesitan un conocimiento profundo, una buena visión puede resultar de ayuda para empezar.

Una vez iniciado el host, cuando un usuario llama a Run o WaitForShutdown, se registra un controlador para IApplicationLifetime.ApplicationStopping. La ejecución se pausa en WaitForShutdown, a la espera de que se pueda generar el evento ApplicationStopping. El método Main no se devuelve de inmediato, y la aplicación permanece en ejecución hasta que se devuelve Run o WaitForShutdown.

Cuando se envía una señal al proceso, inicia la secuencia siguiente:

Diagrama de secuencia de cierre de hospedaje.

  1. El control fluye de ConsoleLifetime a ApplicationLifetime para generar el evento ApplicationStopping. Esto indica que WaitForShutdownAsync desbloquee el código de ejecución de Main. Mientras tanto, el controlador de señal POSIX se devuelve con Cancel = true, ya que se ha controlado esta señal POSIX.
  2. El código de ejecución de Main comienza a ejecutarse de nuevo e indica al host que StopAsync(), lo que, a su vez, detiene todos los servicios hospedados y genera cualquier otro evento detenido.
  3. Por último, WaitForShutdown se cierra, lo que permite que cualquier aplicación limpie el código que se va a ejecutar y que el método Main salga correctamente.

Apagado del host en escenarios de servidor web

Hay otros escenarios comunes en los que el apagado correcto funciona en Kestrel para los protocolos HTTP/1.1 y HTTP/2, y cómo puede configurarlo en diferentes entornos con un equilibrador de carga para purgar el tráfico sin problemas. Aunque la configuración del servidor web está fuera del ámbito de este artículo, puede encontrar más información en Configuración de opciones para el servidor web Kestrel de ASP.NET Core.

Cuando el host recibe una señal de apagado (por ejemplo, CTL+C o StopAsync), lo notifica a la aplicación con la señal ApplicationStopping. Debe suscribirse a este evento si tiene operaciones de larga duración que necesiten finalizarse correctamente.

A continuación, el host llama a IServer.StopAsync con un tiempo de espera de apagado que puede configurar (el valor predeterminado es 30 s). Kestrel (y Http.Sys) cierran sus enlaces de puerto y dejan de aceptar nuevas conexiones. También indican a las conexiones actuales que detengan el procesamiento de nuevas solicitudes. Para HTTP/2 y HTTP/3, se envía un mensaje preliminar GOAWAY al cliente. Para HTTP/1.1, detienen el bucle de conexión porque las solicitudes se procesan en orden. IIS se comporta de forma diferente al rechazar nuevas solicitudes con un código de estado 503.

Las solicitudes activas tienen para completarse hasta que se agota el tiempo de espera de apagado. Si se completan antes del tiempo de espera, el servidor devuelve el control al host antes. Si expira el tiempo de espera, las conexiones y solicitudes pendientes se anulan de manera forzosa, lo que puede provocar errores en los registros y en los clientes.

Consideraciones sobre el equilibrador de carga

Para garantizar una transición fluida de los clientes a un nuevo destino al trabajar con un equilibrador de carga, puede seguir estos pasos:

  • Abra la nueva instancia y empiece a equilibrar el tráfico (es posible que ya tenga varias instancias con fines de escalado).
  • Deshabilite o quite la instancia anterior en la configuración del equilibrador de carga para que deje de recibir tráfico nuevo.
  • Señale la instancia anterior para apagarla.
  • Espere a que se purgue o a que se agote el tiempo de espera.

Vea también