¿Qué es .NET Generic Host? - La base de DI, configuración y registro

· Actualizado el: · · C#, .NET, Generic Host, Worker, Diseño

Cuando empieza a escribir una aplicación de consola o un worker en .NET, al principio basta con poner un poco de código en Main. Pero en cuanto la aplicación crece un poco, suelen ir apareciendo cosas como estas.

  • Quiere leer appsettings.json
  • Quiere sobrescribir valores con variables de entorno
  • Quiere registrar información con ILogger
  • No quiere que la creación de servicios esté llena de new
  • Quiere ejecutar un bucle en segundo plano
  • Quiere terminar limpiamente con Ctrl+C o al detener el servicio

Aquí es donde entra en escena Generic Host. Pero este nombre también tiende a generar confusión.

  • En qué se diferencian Host.CreateApplicationBuilder y Host.CreateDefaultBuilder
  • Si IHost es lo mismo que el contenedor de DI
  • Con qué se relaciona BackgroundService
  • Si es algo distinto de WebApplicationBuilder de ASP.NET Core
  • Si vale la pena usarlo incluso en una aplicación de consola

Cuando estas cuestiones se mezclan, Generic Host puede parecer «algo exclusivo de las aplicaciones web» o, al contrario, «algo que hay que convertir siempre en un host». Ambas ideas son un poco imprecisas.

En este artículo, partiendo sobre todo de la sensación práctica actual desde .NET 6 en adelante, ordenamos primero estos cuatro puntos.

  • La verdadera naturaleza de Generic Host
  • Qué se encarga de gestionar en conjunto
  • La relación entre Host.CreateApplicationBuilder / Host.CreateDefaultBuilder / WebApplication.CreateBuilder
  • Por dónde conviene empezar

Índice

  1. Conclusión primero (en una frase)
    • 1.1. Fijemos primero la terminología
  2. Tabla resumen para empezar
    • 2.1. Qué contiene Generic Host
    • 2.2. Diferencias entre los builders
    • 2.3. Por qué hay varios puntos de entrada
  3. Panorama general de Generic Host (diagrama)
  4. Qué ventajas ofrece Generic Host
    • 4.1. Permite concentrar el arranque en un solo lugar
    • 4.2. DI, configuración y registro conectados desde el principio
    • 4.3. Facilita el cierre normal y la ejecución en segundo plano
  5. Configuración mínima
    • 5.1. Ejemplo mínimo en una aplicación de consola
    • 5.2. appsettings.json
    • 5.3. Añadir un BackgroundService
  6. Patrones típicos
    • 6.1. Herramienta de consola de vida corta
    • 6.2. Worker / servicio en segundo plano
    • 6.3. También está presente bajo ASP.NET Core
  7. Casos en los que encaja bien
  8. Casos en los que no encaja / es excesivo
  9. Errores comunes
  10. Resumen
  11. Referencias

1. Conclusión primero (en una frase)

  • Generic Host es la base que reúne el arranque y la vida útil de una aplicación .NET.
  • En ella entran DI, la configuración, el registro, IHostedService / BackgroundService y el procesamiento de cierre de la aplicación.
  • En una aplicación nueva que no es web, lo más natural es empezar por Host.CreateApplicationBuilder(args).
  • El WebApplicationBuilder de ASP.NET Core tampoco es un mundo aparte: es una ventana que amplía la misma idea de host para el entorno web.
  • Es decir, Generic Host no trata solo de un contenedor de DI, sino que es el mecanismo que reúne el punto de ensamblaje de la aplicación y la gestión de su ciclo de vida.

En resumen, en cuanto la aplicación supera un poco el nivel de «leer argumentos, mostrar algo una vez y terminar», Generic Host empieza a rendir bastante. Por el contrario, tampoco es algo que haya que incorporar siempre en herramientas pequeñas que no han crecido hasta ese punto.

1.1. Fijemos primero la terminología

Este artículo va a usar bastantes metáforas más adelante, así que primero dejamos fijada la forma precisa de decir las cosas.

Término Dicho con precisión Metáfora usada en este artículo
DI (inyección de dependencias / Dependency Injection) Una forma de construir clases en la que, en lugar de crear con new a quienes necesita, se le entregan desde fuera. El lugar donde se registran en conjunto esos colaboradores es el contenedor de DI (IServiceProvider), y Generic Host ya lo trae incorporado desde el principio Cableado
Builder (HostApplicationBuilder) Es el objeto que sirve para ensamblar el host. Tiene propiedades como Services, Configuration y Logging, en las que se van registrando cosas. La aplicación no funciona hasta que se llama a Build() Mesa de montaje
Host (IHost) Es el cuerpo de la aplicación ya ensamblado, el resultado de Build(). Contiene el contenedor de DI, la configuración, el registro y los hosted services, y se encarga de todo desde que arranca con Run() / RunAsync() hasta que se detiene Base
Hosted service (IHostedService / BackgroundService) Es el contenedor de un proceso que se mueve al ritmo del inicio y la detención del host. Cuando el host arranca se llama a StartAsync, y si es un BackgroundService, se ejecuta ExecuteAsync Trabajo residente
Lifetime Es la gestión desde el inicio hasta la detención de la aplicación. Recibe señales como Ctrl+C, SIGTERM o la detención del servicio, y unifica la forma de terminar Ciclo de vida

Lo que más se suele confundir es Builder y Host. Builder es quien ensambla, Host es el resultado ya ensamblado, y Build() es la frontera entre ambos. Con solo tener esto claro, expresiones que aparecerán más adelante como «base», «caja», «ventana» o «entrada» se podrán leer sin dudar a qué se refieren.

Si es la primera vez que trabaja con DI, pensarlo así no falla: en lugar de escribir usted mismo la cadena de new, registra al arrancar algo como «cuando se necesite este tipo, entregue esta implementación», y quien lo recibe simplemente lo toma como argumento del constructor. Ese lugar de registro es builder.Services.

2. Tabla resumen para empezar

2.1. Qué contiene Generic Host

Primero conviene separar qué hay dentro de esta caja; resulta bastante más cómodo.

Elemento De qué se encarga Generic Host Qué ventaja aporta
DI Ensambla los servicios a partir de IServiceCollection Facilita reducir la cadena de new
Configuration Reúne appsettings.json, variables de entorno, argumentos de línea de comandos, etc. Facilita manejar las diferencias entre entornos
Logging Construye la base para usar ILogger<T> Facilita cambiar después el destino de los registros
Hosted service Gestiona el inicio y la detención de IHostedService / BackgroundService Facilita separar el procesamiento residente del cuerpo de la aplicación
Lifetime Gestiona el inicio y la detención a través de IHostApplicationLifetime, IHostEnvironment, etc. Facilita unificar cómo termina la aplicación ante Ctrl+C, SIGTERM o la detención del servicio

Lo importante aquí es que Generic Host no es «un simple envoltorio cómodo de DI». En realidad, la forma de verlo que menos falla es como una caja que cablea en conjunto todo lo que rodea la entrada de la aplicación.

2.2. Diferencias entre los builders

Aquí también conviene verlo primero de un vistazo, en una sola tabla.

Punto de entrada Uso principal Estilo de escritura Primera elección
Host.CreateApplicationBuilder(args) Aplicaciones nuevas que no son web, como consola / worker Se escribe directamente en builder.Services / builder.Configuration / builder.Logging Esta, si es un proyecto nuevo
Host.CreateDefaultBuilder(args) Código existente o configuraciones basadas en métodos de extensión antiguos Se encadenan métodos como ConfigureServices Esta, si hay activos existentes
WebApplication.CreateBuilder(args) Aplicaciones web / API de ASP.NET Core Un punto de entrada que añade a Generic Host las necesidades propias de la web Esta, si es web

CreateApplicationBuilder y CreateDefaultBuilder no son un caso en el que uno sea la funcionalidad nueva y el otro algo distinto.

Ambos tienen la misma funcionalidad central y el mismo comportamiento predeterminado. Lo que difiere es sobre todo el estilo de escritura.

En una aplicación nueva que no es web, hoy en día lo más natural es empezar con Host.CreateApplicationBuilder(args). Conviene pensar en WebApplication.CreateBuilder(args) como un punto de entrada que amplía ese mismo flujo para el entorno web; así es más fácil de ordenar.

2.3. Por qué hay varios puntos de entrada

Hay varios puntos de entrada porque el lado web y el lado no web crecieron por separado y después confluyeron; ese es el trasfondo.

  • Originalmente, ASP.NET Core tenía su propio Web Host (IWebHostBuilder) exclusivo para la web, y por separado existía Generic Host (IHostBuilder) para aplicaciones que no son web.
  • Más tarde, el lado de ASP.NET Core se acercó a Generic Host, de modo que tanto la web como el resto de aplicaciones pasaron a apoyarse en la misma idea de host.
  • Además, junto con el estilo de escribir encadenando callbacks (como ConfigureServices), aumentaron los puntos de entrada con el estilo de escribir directamente en propiedades (como builder.Services). Host.CreateApplicationBuilder y WebApplication.CreateBuilder pertenecen a este segundo grupo.

La documentación oficial actual explica que la familia Host.CreateApplicationBuilder (IHostApplicationBuilder) está pensada para proyectos nuevos y es la opción predeterminada de las plantillas actuales, mientras que la familia Host.CreateDefaultBuilder (IHostBuilder) es la forma tradicional que se mantiene por compatibilidad con código existente. También se indica explícitamente que ambas comparten la misma funcionalidad central y el mismo comportamiento predeterminado.

Si viene de código de la época de .NET Framework o .NET Core 3.1, es normal sentir «¿por qué hay dos formas de escribirlo?», pero resulta más fácil de aceptar si lo ve no como dos cosas distintas, una vieja y una nueva, puestas una junto a otra, sino como puntos de entrada que aumentaron durante el proceso de confluencia. Si no tiene motivos para alinearse con activos existentes, en un proyecto nuevo puede usar Host.CreateApplicationBuilder sin problema.

3. Panorama general de Generic Host (diagrama)

A grandes rasgos, el panorama general se ve así en un diagrama.

Panorama general de .NET Generic HostDiagrama que muestra el flujo desde los argumentos, las variables de entorno y appsettings.json hasta Host.CreateApplicationBuilder, la configuración del builder, la construcción del IHost y su ejecución hasta la detenciónargs / variables de entorno / appsettings.jsonHost.CreateApplicationBuilder(args)builder.Configurationbuilder.Servicesbuilder.LoggingIHostedService / BackgroundServicebuilder.Build()IHostRun / RunAsyncInicio, detención, Ctrl+C, SIGTERM

Normalmente, se crea el builder en Program.cs, se añaden servicios a builder.Services, se ajustan builder.Configuration y builder.Logging según haga falta y, por último, se llama a Build() para obtener el IHost y se pone en marcha con Run() / RunAsync().

Un detalle que pasa desapercibido pero es importante es que, en el momento de Host.CreateApplicationBuilder(args), ya viene cargado bastante contenido. Por defecto se incluyen, por ejemplo, cosas como estas.

  • La raíz de contenido es el directorio actual
  • La configuración del host proviene de variables de entorno con el prefijo DOTNET_ y de los argumentos de línea de comandos
  • La configuración de la aplicación proviene de appsettings.json, appsettings.{Environment}.json, los user secrets en Development, las variables de entorno y los argumentos de línea de comandos
  • El registro se envía a Console / Debug / EventSource / EventLog (solo en Windows)
  • En el entorno Development se aplican la validación de scope y la validación de dependencias

Es decir, no se está cableando todo desde cero sin pensar en nada: desde el principio se dispone de «una base que, para un uso normal, ya cubre bastante».

4. Qué ventajas ofrece Generic Host

4.1. Permite concentrar el arranque en un solo lugar

El efecto más discreto y a la vez más grande de Generic Host es que evita que la entrada de la aplicación se disperse.

En cuanto la aplicación crece un poco, suelen ir acumulándose alrededor de Main cosas como estas.

  • Carga de archivos de configuración
  • Sustituciones según el entorno
  • Inicialización del logger
  • Ensamblaje de HttpClient, repositorios y servicios
  • Arranque del procesamiento en segundo plano
  • Limpieza al recibir la señal de cierre

Si conecta todo esto a mano sin un host, al principio puede parecer ligero, pero poco a poco la entrada se va volviendo pegajosa.

Con Generic Host, Program.cs queda claramente definido como «el lugar donde se ensamblan en conjunto las dependencias». Solo con este orden, la facilidad para revisar el código cambia bastante.

4.2. DI, configuración y registro conectados desde el principio

Con Generic Host, DI, configuración y registro quedan sobre la misma base desde el principio.

Por ejemplo, en el lado de las clases se puede recibir con normalidad cosas como estas.

  • ILogger<T>
  • IConfiguration
  • IHostEnvironment
  • IOptions<T>

Lo que resulta útil aquí es que la forma de leer la configuración y la forma de crear los servicios no tienden a convertirse en estilos separados.

Si solo hay uno o dos valores de configuración, basta con leer directamente IConfiguration["Section:Key"] para que funcione. Sin embargo, cuando en el trabajo real la configuración empieza a crecer, es más seguro agrupar por sección en una clase con IOptions<T>. Una referencia orientativa es cuando se superan las cinco claves como cadena de texto. A partir de ese tamaño, los errores de tipeo empiezan a aparecer como fallos que no se detectan hasta tiempo de ejecución, y también se vuelve difícil rastrear qué clave se lee en qué lugar.

De la misma manera, en lugar de fabricar ILoggerFactory a mano en distintos puntos, resulta más claro inyectar ILogger<T> en las clases que lo necesiten.

Lo conveniente de Generic Host es que no trata estos aspectos por separado, sino que los maneja en conjunto como base de toda la aplicación.

4.3. Facilita el cierre normal y la ejecución en segundo plano

Generic Host no solo se ocupa de «cómo arrancar», sino también de «cómo detenerse».

Cuando el host arranca, se llama a StartAsync de cada IHostedService registrado. En los servicios worker, se ejecuta ExecuteAsync del hosted service, incluido BackgroundService.

Aquí, «terminar con normalidad» no significa cortar el procesamiento de golpe, sino terminar siguiendo este orden:

  • Emitir la señal de detención
  • Salir del bucle o de la espera
  • Limpiar conexiones y recursos

En aplicaciones que se ejecutan durante mucho tiempo, esto es bastante importante. Ante eventos como Ctrl+C, SIGTERM o la detención del servicio, resulta más fácil unificar la forma en que se detiene toda la aplicación.

Además, cuando se quiere solicitar el cierre desde el lado de la aplicación, se puede usar IHostApplicationLifetime.StopApplication(). Es una forma de emitir, dentro del contexto del host, la señal de «el trabajo ya terminó, así que quiero que se cierre limpiamente».

5. Configuración mínima

5.1. Ejemplo mínimo en una aplicación de consola

Lo primero importante es que usar Generic Host no obliga a crear siempre un BackgroundService.

Incluso en una herramienta de consola que se ejecuta una sola vez, Generic Host resulta perfectamente útil si lo que quiere es DI, configuración y registro.

Si lo va a añadir después a un proyecto de consola normal, primero haga referencia a Microsoft.Extensions.Hosting.

dotnet add package Microsoft.Extensions.Hosting

Un ejemplo mínimo de Program.cs sería algo así.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<JobRunner>();

using IHost host = builder.Build();

try
{
    JobRunner runner = host.Services.GetRequiredService<JobRunner>();
    await runner.RunAsync();
    return 0;
}
catch (Exception ex)
{
    ILogger logger = host.Services
        .GetRequiredService<ILoggerFactory>()
        .CreateLogger("Program");

    logger.LogError(ex, "Unhandled exception occurred during job execution.");
    return 1;
}

internal sealed class JobRunner(
    ILogger<JobRunner> logger,
    IConfiguration configuration,
    IHostEnvironment hostEnvironment)
{
    public Task RunAsync()
    {
        string message = configuration["Sample:Message"] ?? "(no message)";

        logger.LogInformation("Environment: {EnvironmentName}", hostEnvironment.EnvironmentName);
        logger.LogInformation("Message: {Message}", message);

        return Task.CompletedTask;
    }
}

Al ejecutar dotnet run, en la consola aparece esto (el valor de Message proviene del appsettings.json que colocaremos en el siguiente apartado 5.2).

info: JobRunner[0]
      Environment: Production
info: JobRunner[0]
      Message: hello from Generic Host

Lo que aparece a la derecha de info: es la categoría del registro (aquí, el nombre del tipo, porque se trata de ILogger<JobRunner>) y el ID de evento. El logger de consola predeterminado saca la salida en este formato de «categoría en la primera línea, cuerpo en la segunda». Que Environment sea Production se debe a que ese es el valor predeterminado cuando no se ha establecido ni la variable de entorno DOTNET_ENVIRONMENT ni ASPNETCORE_ENVIRONMENT. Si quiere cambiarlo durante el desarrollo, ejecute la aplicación con DOTNET_ENVIRONMENT=Development establecido.

Si la aplicación no va a permanecer residente durante mucho tiempo, no hace falta llegar a RunAsync(). Basta con llamar a Build(), resolver los servicios necesarios y terminar en cuanto el trabajo esté hecho. Aun así se aprovechan suficientemente las ventajas de Generic Host.

Esto es sorprendentemente importante. No hace falta traer siempre la plantilla de Worker incluso para trabajos de vida corta.

5.2. appsettings.json

Para el ejemplo anterior, basta con un archivo de configuración tan mínimo como este.

{
  "Sample": {
    "Message": "hello from Generic Host"
  }
}

Hay un solo tropiezo clásico. En los proyectos de consola, con solo añadir appsettings.json no se copia a la carpeta de salida. Hay que poner «Copiar al directorio de salida» en «Copiar si es más reciente» en las propiedades del proyecto, o escribir esto en el csproj.

<ItemGroup>
  <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

También conviene conocer el síntoma cuando se olvida hacerlo. Generic Host lee appsettings.json como un archivo opcional, así que si no existe no se produce ninguna excepción. Simplemente no se pueden obtener los valores. En el ejemplo mínimo anterior, se mostraría Message: (no message). Cuando «no hay error pero la configuración no surte efecto», compruebe primero si appsettings.json está en la carpeta de salida.

En este ejemplo se lee directamente configuration["Sample:Message"]. Si solo va a consultar uno o dos valores, esto es suficiente.

Sin embargo, cuando en el trabajo real la configuración empieza a crecer, conviene orientarse hacia esta forma, que ayuda a evitar que las claves de texto queden desperdigadas por todas partes:

  • Separar por sección en clases
  • Inyectar con IOptions<T>
  • Validar en el arranque

Además, los valores predeterminados de Generic Host conectan no solo appsettings.json, sino también appsettings.{Environment}.json, las variables de entorno y los argumentos de línea de comandos, por lo que resulta bastante natural «sustituir valores solo en desarrollo» o «sobrescribir en producción mediante variables de entorno».

5.3. Añadir un BackgroundService

Para un procesamiento que se ejecuta durante mucho tiempo, usar BackgroundService resulta bastante natural.

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddScoped<PollingJob>();
builder.Services.AddHostedService<PollingWorker>();

using IHost host = builder.Build();
await host.RunAsync();

internal sealed class PollingWorker(
    IServiceScopeFactory scopeFactory,
    ILogger<PollingWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            using IServiceScope scope = scopeFactory.CreateScope();
            PollingJob job = scope.ServiceProvider.GetRequiredService<PollingJob>();

            await job.RunAsync(stoppingToken);
            logger.LogInformation("Polling completed.");
        }
    }
}

internal sealed class PollingJob(ILogger<PollingJob> logger)
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Do work here.");
        return Task.CompletedTask;
    }
}

En este ejemplo conviene fijarse en dos puntos.

  1. El cuerpo de BackgroundService es ExecuteAsync
  2. Si necesita una dependencia scoped, cree el scope con IServiceScopeFactory

BackgroundService en sí mismo no tiene un scope predeterminado. Por ejemplo, si quiere usar un servicio scoped como DbContext, la forma segura es resolver el lado del job dentro de un scope, como en el ejemplo anterior.

Por otro lado, la elección de la herramienta de ejecución periódica en sí es otro tema, pero si se escribe con un enfoque basado en async, PeriodicTimer resulta bastante tranquilo. Esto se relaciona también con el artículo sobre temporizadores entre los artículos relacionados.

6. Patrones típicos

6.1. Herramienta de consola de vida corta

Incluso en aplicaciones como lotes (batch), herramientas de conversión o comandos de mantenimiento, que hacen su trabajo una sola vez y terminan, Generic Host se puede usar con normalidad.

Encaja bien en situaciones como estas.

  • Quiere leer un archivo de configuración
  • Quiere emitir registros
  • Quiere inyectar HttpClient o repositorios
  • Quiere devolver un código de salida

En este tipo de aplicaciones, si de entrada trae BackgroundService y RunAsync(), resulta un poco pesado y termina haciendo un uso excesivo de la gestión del ciclo de vida del host.

Para un trabajo de vida corta, basta con resolver y ejecutar JobRunner como en el ejemplo mínimo anterior.

6.2. Worker / servicio en segundo plano

Para procesos como workers residentes, polling, consumo de colas, monitorización o ejecución periódica, la combinación de Generic Host y BackgroundService resulta bastante natural.

Lo que se agradece especialmente es esto.

  • El flujo de inicio y detención queda unificado en el lado del host
  • El registro, la configuración y el DI están disponibles desde el principio
  • Es fácil propagar la cancelación con Ctrl+C o señales de detención
  • Es fácil separar el cuerpo del procesamiento residente de Program.cs

Además, se conecta con facilidad con el contexto de Windows Service o de contenedores. Si va a hacer crecer la aplicación como una aplicación residente, Generic Host es una base bastante natural.

Cuando se convierte en un Windows Service, es menos propenso a incidentes basar la búsqueda de archivos en IHostEnvironment.ContentRootPath en lugar de asumir el directorio actual. Esto se debe a que, en el contexto del host, queda determinada «la ruta base de la aplicación».

6.3. También está presente bajo ASP.NET Core

En las aplicaciones web / API se usa WebApplication.CreateBuilder(args), así que a primera vista puede parecer un mundo aparte de Generic Host.

Pero, en cuanto a la sensación, están bastante conectados.

  • builder.Services
  • builder.Configuration
  • builder.Logging

El estilo de escritura se parece precisamente por eso.

En ASP.NET Core, el arranque del servidor HTTP también forma parte del lifetime del host. Es decir, entender Generic Host también resulta útil en el sentido de que, al leer el Program.cs del lado web, se comprende con más facilidad «por qué aquí se está tocando DI, configuración y registro».

7. Casos en los que encaja bien

Enumeremos algunos escenarios en los que Generic Host suele encajar con comodidad.

  • Aplicaciones de consola que usan configuración, registro y DI
  • Workers de tipo queue consumer, poller, watchdog o scheduler
  • Aplicaciones de ejecución prolongada en las que se quiere limpiar al recibir Ctrl+C o SIGTERM
  • Aplicaciones que en el futuro podrían crecer hasta convertirse en un Windows Service o un residente en contenedor
  • Aplicaciones en las que se quiere alinear el estilo con el mismo conjunto de extensiones que ASP.NET Core

Lo que tienen en común es que «no se quiere tratar con descuido la entrada de la aplicación ni la gestión de su ciclo de vida».

Aun así, con esto solo resulta difícil trazar la línea, así que dejamos también una referencia orientativa. Si se cumplen dos o más de los siguientes puntos, suele resultar más cómodo a la larga adoptar Generic Host desde el principio.

Referencia Línea concreta
Cantidad de configuración Hay tres o más ajustes que cambian según el entorno (destino de conexión, umbrales, destino de salida, etc.)
Registro Es necesario dejarlo en un archivo o en el Event Log. No basta con escribir en la salida estándar y terminar
Forma de ejecución Permanece residente. O se ejecuta al menos una vez al día, a intervalos fijos
Dependencias Hay tres o más colaboradores que se quieren recibir por el constructor. Hay colaboradores que se quieren sustituir en las pruebas
Ciclo de vida Se necesita una limpieza intermedia al recibir Ctrl+C o la detención del servicio
Futuro Existe la posibilidad de ejecutarlo como Windows Service o en un contenedor

8. Casos en los que no encaja / es excesivo

Por el contrario, también hay situaciones en las que no hace falta convertir a Generic Host en protagonista desde el principio.

  • Herramientas pequeñas que solo leen argumentos una vez, muestran una salida y terminan
  • Código de verificación improvisado que se usa solo durante unos minutos
  • Proyectos de biblioteca
  • Casos en los que basta con leer una sola configuración y no hace falta llegar a DI, registro ni gestión del ciclo de vida

En estos casos, escribir directamente en Main en lugar de levantar un host implica menos cantidad de lectura y menos archivos. Como referencia, si ninguno de los puntos de la tabla del apartado 7 se cumple, se puede considerar sin problema que Generic Host no es necesario.

Lo importante es que, aunque Generic Host sea potente, eso no significa que sea obligatorio en todos los ejecutables.

9. Errores comunes

Por último, resumimos los puntos en los que es fácil tropezar al empezar con Generic Host.

  • Ver Generic Host únicamente como un contenedor de DI
    • En realidad, es una base que incluye inicio, detención, configuración, registro y hosted services.
  • Empezar por inercia con Host.CreateDefaultBuilder en una aplicación nueva
    • Si no hay motivos para alinearse con código existente, lo más natural es empezar por Host.CreateApplicationBuilder.
  • Meter directamente un servicio scoped en un BackgroundService
    • Los hosted services no tienen un scope predeterminado. Es más seguro crear el scope con IServiceScopeFactory.
  • No comunicar la detención al host en un worker que termina de una sola vez
    • Si con la plantilla de Worker se hace un «run once», el host sigue ejecutándose sin más a menos que, en cuanto termine el trabajo, se llame a IHostApplicationLifetime.StopApplication().
  • Cortar con Environment.Exit cuando lo que se quiere es un cierre normal
    • Si está usando un host, en los casos en los que se quiere detener limpiamente es más razonable usar StopApplication().
  • Asumir el directorio actual en un Windows Service
    • Es más estable basar la búsqueda de archivos en IHostEnvironment.ContentRootPath.
  • Envolver desde el principio con BackgroundService un CLI de vida corta
    • Para un trabajo de una sola vez, basta con resolver y ejecutar una clase de servicio normal.
  • Meter sin cuidado un temporizador basado en callback para la ejecución periódica de un BackgroundService
    • Si se escribe con un flujo async, PeriodicTimer suele resultar más legible y menos propenso a desorden.

En Generic Host, con solo distinguir de entrada «si es un trabajo de vida corta o un trabajo residente», resulta bastante más fácil no perderse.

10. Resumen

Dicho en una frase, Generic Host es la base que reúne la entrada y la gestión del ciclo de vida de una aplicación .NET.

Repasemos los puntos que conviene tener presentes.

  1. Generic Host incluye no solo DI, sino también configuración, registro, procesamiento de cierre y hosted services
  2. En una aplicación nueva que no es web, lo natural es empezar por Host.CreateApplicationBuilder(args)
  3. Para un trabajo de vida corta, basta con hacer build y ejecutar sin usar BackgroundService
  4. Para un procesamiento residente, BackgroundService y la gestión del lifetime del host resultan bastante efectivos
  5. Como BackgroundService no tiene un scope predeterminado, los servicios scoped requieren crear el scope explícitamente
  6. El WebApplicationBuilder de ASP.NET Core también se apoya, conceptualmente, en el mismo flujo de ideas

Generic Host no es una herramienta para un ritual pesado. Es una herramienta para, en cuanto la configuración, el registro, las dependencias, el arranque y el cierre empiecen a crecer aunque sea un poco, no dejarlos escapar entre las paredes del código, sino reunirlos en la entrada.

Por el contrario, si todavía se trata de una herramienta pequeña que no lo necesita, no hace falta incorporarlo. Cuando se logra hacer esta distinción, Generic Host deja de ser «algo que se pone porque sí» y se convierte en una base práctica con un uso claramente definido.

11. Referencias

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué es Generic Host?
Es la base que reúne el arranque y el ciclo de vida de una aplicación .NET. Incluye DI, la configuración (Configuration), el registro (logging), IHostedService / BackgroundService y el procesamiento de cierre de la aplicación. No es un simple envoltorio de un contenedor de DI: conviene entenderlo como el mecanismo que reúne el punto de ensamblaje de la aplicación y la gestión de su ciclo de vida. Resulta eficaz en cuanto la aplicación empieza a tener algo más de configuración, registro, dependencias, arranque y cierre.
¿Cuál debería usar, Host.CreateApplicationBuilder o Host.CreateDefaultBuilder?
Si se trata de una aplicación nueva que no es web, lo más natural es empezar con Host.CreateApplicationBuilder(args). Ambos tienen la misma funcionalidad central y el mismo comportamiento predeterminado; no se trata de que uno sea la funcionalidad nueva y el otro algo distinto. La diferencia está sobre todo en el estilo de escritura: CreateApplicationBuilder se escribe directamente en propiedades como builder.Services, mientras que CreateDefaultBuilder encadena métodos como ConfigureServices. Si tiene motivos para alinearse con código existente o con una configuración basada en métodos de extensión antiguos, elija CreateDefaultBuilder.
¿Vale la pena usar Generic Host incluso en una aplicación de consola?
Si necesita DI, configuración y registro, resulta perfectamente útil incluso en una herramienta de consola que se ejecuta una sola vez. No es obligatorio crear un BackgroundService: basta con llamar a Build(), resolver los servicios necesarios y terminar en cuanto el trabajo esté hecho, y aun así se obtienen las ventajas de Generic Host. En cambio, para una herramienta pequeña que solo lee argumentos una vez, muestra una salida y termina, o para código de verificación improvisado, resulta excesivo, así que tampoco es algo que deba incorporarse siempre.
¿Cómo se usa un servicio scoped dentro de un BackgroundService?
BackgroundService no tiene un scope predeterminado, por lo que inyectar directamente un servicio scoped en el constructor no es seguro. Lo seguro es inyectar IServiceScopeFactory, crear explícitamente un scope dentro de ExecuteAsync y resolver dentro de él el servicio del trabajo. Esta forma es especialmente importante cuando se quiere usar un servicio scoped como DbContext.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog