Novedades .NET Core 2.1: Generic host

Ahora que .NET Core 2.1 ya es oficial ya podemos desgranar algunas de sus novedades más interesantes. La verdad es que, por fin, se vislumbra una madurez en la plataforma. Realmente a no ser que haya algún motivo de fuerza mayor (librería no disponible), .NET Core 2.1 debería ser la opción por defecto a la hora de empezar cualquier proyecto nuevo.

En este post quiero hablaros del host genérico de .NET Core 2.1. En 2.0 usábamos el siguiente código de base para crear un WebHost que hospedaba nuestra aplicación:

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .Build();

El método CreateDefaultBuilder nos daba una instancia de IWebHostBuilder preparada para añadir la configuración y servicios más habituales. Es este método el que hace la magia de que, entre otras cosas, el fichero de configuración sea appsettings.json y no otro o bien de que las variables de entorno también formen parte de la configuración o que el logging esté habilitado por defecto. Por supuesto, no estamos obligados a usar este método y usar un objeto WebHostBuilder configurado a nuestro gusto, con nuestras necesidades concretas.

Pero al final lo que obtenemos es siempre un IWebHost que es el objeto que termina hospedando nuestra aplicación. Este modelo funciona muy bien en aplicaciones web, pero nos puede interesar tener un modelo similar de hosting en aplicaciones que no sean web.

Pongamos p. ej. una aplicación de consola que deba hacer algo hasta que alguien la pare. Si creamos una aplicación de consola usando NetCore 2.1 la plantilla por defecto no ha cambiado nada respecto a NetCore 2.0:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Y ya. Eso es todo lo que obtenemos. Si, como decía antes quiero hacer una aplicación de consola que realice cierta tarea hasta que sea parada, todo eso corre de mi parte. Pero, no sería interesante poder usar el modelo de hosting y levantar un host que hospede mi aplicación? Vale, no expondrá ningún endpoint http porque no es una aplicación web, pero el host gestionará el ciclo de vida de mi aplicación.

Pues eso es, precisamente lo que nos permite usar el host genérico de NetCore 2.1:

async static Task Main(string[] args)
{
    var builder = new HostBuilder();
    await builder.RunConsoleAsync();
}

Debes añadir el paquete Microsoft.Extensions.Hosting. Luego para que esto te compile asegúrate de que el compilador está en modo C# 7.1 como mínimo, ya que estamos usando async Main. Para ello edita el fichero .csproj y añade dentro de la etiqueta <PropertyGroup>:

<LangVersion>7.1</LangVersion>

(Si tienes VS puedes ir a las «propiedades del proyecto -> Build -> Advanced -> Language Version» y seleccionar al menos 7.1. El resultado es el mismo).

Si ahora ejecutas este código verás que la aplicación se pone en marcha y se queda «eternamente» esperando:

Aplicación en marcha

En este caso hemos usado el método de extensión RunConsoleAsync que levanta el host con soporte de consola y lo mantiene levantado hasta que llega SIGTERM o SIGINT (Ctrl+C).

Por supuesto la aplicación no hace nada porque hemos levantado un host vacío. Veamos ahora como configurarlo, y como verás es casi idéntico a como configuramos el IWebHost usando el WebHostBuilder:

var builder = new HostBuilder()
    .ConfigureAppConfiguration(cfg =>
    {
        cfg.AddJsonFile("appsettings.json", optional: true);
        cfg.AddEnvironmentVariables();
    })
    .ConfigureServices(sc =>
    {
        sc.AddTransient<IGuidProvider, GuidProvider>();
    })
    .ConfigureLogging(lb =>
    {
        lb.AddConsole();
    });

Seguramente te sonaran todos esos métodos, ya que son iguales a los que venimos usando en ASP.NET Core. En este caso hemos registrado un servicio (IGuidProvider), hemos configurado el login y la configuración usando appsettings.json y las variables de entorno. Pongo, por completitud, el código de IGuidProvider, aunque por el nombre ya debes deducir lo qué hace 😛

public interface IGuidProvider
{
    Guid Id { get; }
}

public class GuidProvider : IGuidProvider
{
    public Guid Id { get; }
    public GuidProvider() => Id = Guid.NewGuid();
}

Solo comentar que deberás añadir algunos paquetes adicionales:

Si pones en marcha la aplicación, de nuevo sigue sin hacer nada. Claro, hemos configurado el host pero no le hemos dicho qué hospeda. Del mismo modo que una aplicación ASP.NET Core hospeda middlewares, una aplicación con el host genérico hospeda objetos IHostedService. Como parte de proceso de inicialización del host se inician (uno tras otro) los IHostedService que tengamos:

public class GuidConsumerService : IHostedService
{
    private readonly Guid _myId;
    private readonly ILogger<GuidConsumerService> _logger;
    public GuidConsumerService(IGuidProvider gp, ILogger<GuidConsumerService> logger)
    {
        _myId = gp.Id;
        _logger = logger;
    }
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting worker " + _myId);
        return Task.CompletedTask; 
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Stopping worker " + _myId);
        return Task.CompletedTask;
    }
}

Una vez tenemos el IHostedService creado lo añadimos al host usando el método AddHostedService dentro de ConfigureServices:

.ConfigureServices(sc =>
{
    sc.AddTransient<IGuidProvider, GuidProvider>();
    sc.AddHostedService<GuidConsumerService>();
    sc.AddHostedService<GuidConsumerService>();
})

Si ahora iniciamos la aplicación de nuevo veremos lo siguiente:

hosted services en marcha

(La inicialización de cada IHostedService es asíncrona, así que el orden de los mensajes puede variar).

Un ejemplo típico es tener un IHostedService que realice algo cada cierto tiempo, entonces en este caso puedes usar un temporizador:

public class GuidConsumerService : IHostedService, IDisposable
{
    private readonly Guid _myId;
    private readonly ILogger<GuidConsumerService> _logger;
    private Timer _timer;
    public GuidConsumerService(IGuidProvider gp, ILogger<GuidConsumerService> logger)
    {
        _myId = gp.Id;
        _logger = logger;
    }
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting worker " + _myId);
        _timer = new Timer(DoSomething, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
        return Task.CompletedTask;
    }

    private void DoSomething(object state)
    {
        _logger.LogInformation($"I am {_myId} doing stuff!");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Stopping worker " + _myId);
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose() => _timer?.Dispose();
}

Otros usos de IHostedService son, p. ej. levantar un servicio que escuche en un RabbitMQ o en un Azure Service Bus y haga algo cada vez que se recibe un mensaje. En este caso en StartAsync habría el código para suscribirnos a la cola o al topic deseado y en StopAsync nos desuscribiríamos.

En este post hemos usado el método de extensión RunConsoleAsync pero lo mismo podríamos haber conseguido usando directamente IHost:

var builder = new HostBuilder()
    // TODA LA CONFIGURACIÓN DE ANTES
    .UseConsoleLifetime();
  await builder.Build().RunAsync()

En resúmen: el host genérico de Net Core 2.1 es un mecanismo ideal que nos permite nuevos escenarios que antes eran más complicados: ahora hacer aplicaciones self-hosted  que interaccionen con servicios de mensajería, de eventos o que tengan que realizar una tarea periódica ¡es más fácil que nunca! La gran ventaja es que toda la infraestructura que ya conocemos de ASP.NET Core (configuración, DI, logging, etc) la reaprovechamos completamente.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *