¿Qué es .NET Generic Host? - La base de DI, configuración y registro
· Actualizado el: · Go Komura · 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+Co 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.CreateApplicationBuilderyHost.CreateDefaultBuilder - Si
IHostes lo mismo que el contenedor de DI - Con qué se relaciona
BackgroundService - Si es algo distinto de
WebApplicationBuilderde 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
- Conclusión primero (en una frase)
- 1.1. Fijemos primero la terminología
- 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
- Panorama general de Generic Host (diagrama)
- 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
- 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
- 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
- Casos en los que encaja bien
- Casos en los que no encaja / es excesivo
- Errores comunes
- Resumen
- 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/BackgroundServicey 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
WebApplicationBuilderde 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 (comobuilder.Services).Host.CreateApplicationBuilderyWebApplication.CreateBuilderpertenecen 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.
flowchart LR
accTitle: Panorama general de .NET Generic Host
accDescr: Diagrama 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ón
Args["args / variables de entorno / appsettings.json"] --> Builder["Host.CreateApplicationBuilder(args)"]
Builder --> Config["builder.Configuration"]
Builder --> Services["builder.Services"]
Builder --> Logging["builder.Logging"]
Services --> Hosted["IHostedService / BackgroundService"]
Builder --> Build["builder.Build()"]
Build --> Host["IHost"]
Host --> Run["Run / RunAsync"]
Run --> Lifetime["Inicio, detención, Ctrl+C, SIGTERM"]
Lifetime --> Hosted
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
Developmentse 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>IConfigurationIHostEnvironmentIOptions<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.
- El cuerpo de
BackgroundServiceesExecuteAsync - 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
HttpCliento 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+Co 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.Servicesbuilder.Configurationbuilder.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+Co 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.CreateDefaultBuilderen una aplicación nueva- Si no hay motivos para alinearse con código existente, lo más natural es empezar por
Host.CreateApplicationBuilder.
- Si no hay motivos para alinearse con código existente, lo más natural es empezar por
- 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.
- Los hosted services no tienen un scope predeterminado. Es más seguro crear el scope con
- 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().
- 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
- Cortar con
Environment.Exitcuando 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().
- Si está usando un host, en los casos en los que se quiere detener limpiamente es más razonable usar
- Asumir el directorio actual en un Windows Service
- Es más estable basar la búsqueda de archivos en
IHostEnvironment.ContentRootPath.
- Es más estable basar la búsqueda de archivos en
- Envolver desde el principio con
BackgroundServiceun 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,PeriodicTimersuele resultar más legible y menos propenso a desorden.
- Si se escribe con un flujo
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.
- Generic Host incluye no solo DI, sino también configuración, registro, procesamiento de cierre y hosted services
- En una aplicación nueva que no es web, lo natural es empezar por
Host.CreateApplicationBuilder(args) - Para un trabajo de vida corta, basta con hacer build y ejecutar sin usar
BackgroundService - Para un procesamiento residente,
BackgroundServicey la gestión del lifetime del host resultan bastante efectivos - Como
BackgroundServiceno tiene un scope predeterminado, los servicios scoped requieren crear el scope explícitamente - El
WebApplicationBuilderde 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
- .NET Generic Host - .NET
- Worker Services en .NET
- Usar servicios scoped dentro de un BackgroundService - .NET
- Configuración en .NET
- Patrón de opciones en .NET
- .NET Generic Host en ASP.NET Core
- Crear un Windows Service con BackgroundService - .NET
- Artículo relacionado: Los 3 temporizadores de .NET y cuándo usar cada uno - PeriodicTimer/Timer/DispatcherTimer
- Artículo relacionado: Tabla práctica de async/await en C# - Task.Run y ConfigureAwait
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Por qué usar .NET Generic Host y BackgroundService en una aplicación de escritorio
Resumimos cómo usar Generic Host y BackgroundService en herramientas y apps residentes de Windows para organizar el inicio, el procesamie...
Buenas prácticas de multithreading en la práctica — Edición .NET: qué decidir antes de aumentar los hilos
Reglas de diseño en .NET/C# para evitar fallos y bloqueos intermitentes con hilos: usar Task en lugar de hilos propios, reducir el estado...
Diseño de códigos en sistemas empresariales ── Cómo definir códigos de producto y cliente, y el dígito de control
Guía práctica para diseñar códigos de producto y cliente en sistemas empresariales: código significativo frente a secuencial, fórmulas de...
Cómo elegir la comunicación entre procesos en Windows — canalizaciones con nombre / TCP / gRPC / memoria compartida / COM: tabla de decisión
Cómo elegir la comunicación entre procesos en Windows: comparamos en una tabla de decisión las canalizaciones con nombre, el TCP local, g...
Cómo crear y operar un servicio de Windows — de la diferencia con el Programador de tareas a la creación de servicios con BackgroundService
¿Un proceso en segundo plano debe ser servicio de Windows o basta con el Programador de tareas? Tabla de decisión, creación con .NET Work...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Generic Host y arquitectura de aplicaciones
Generic Host, BackgroundService, DI, configuración, registro y ciclo de vida de la aplicación.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Es un tema sobre construir aplicaciones Windows que incluyen procesamiento en segundo plano, manejo del cierre, registro y configuración, por lo que como proyecto de implementación encaja bien con el desarrollo de aplicaciones Windows.
Consultoría técnica y revisión de diseño
Si está en la etapa de querer ordenar el DI, el ciclo de vida y la separación de responsabilidades antes de implementar, podemos empezar por definir el enfoque mediante consultoría técnica y revisión de diseño.
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.