0

[Article] Worker Services con Asp.Net Core

Desde la salida de Asp.Net Core 3 tenemos una nueva característica llamada Worker Services. Estos servicios no son realmente una novedad, hace mucho tiempo que existe el concepto y seguramente lo has usado antes cuando nuestras aplicaciones web necesitaban ejecutar alguna tarea periódicamente en segundo plano como por ejemplo, una notificación vía correo electrónico.

Antes de .Net Core 3.0 podríamos hacerlos, pero ahora desde esta versión, tenemos plantillas disponibles para crearlas. Todo este tipo de tareas en segundo plano también puede ser implementadas como un Servicio de Windows o bien un Daemon en Linux.

En este posts veremos la creación de un Worker Service que periódicamente registrará en el log algo particular y cómo será consumido. Para esto usaremos VS Code. Podemos utilizar Visual Studio 2019. Empecemos.

Worker Services

Para empezar creamos el proyecto plantilla desde la consola con el comando:

dotnet new worker

Vamos abrir el código con el VSCode veremos algo así:

Como podemos ver, luego de crear el proyecto, que tenemos 3 archivos fundamentales: appsettings.json, programs.cs y worker.cs. Empecemos viendo el archivo program.cs. Dentro encontraremos la configuración de cómo debe estar hosteado el servicio y la configuración de la inyección de dependencias del mismo.

   public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }

Si necesitamos agregar algún servicio extra, como por ejemplo, para enviar mail o algún tipo de notificación, deberemos inyectar desde esta sección.

Veamos ahora nuestro worker.cs.

public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }

Vemos que nuestra clase hereda de BackgroundService que es la que implementa IHostedService. Debe implementar ExecuteAsync que sera el método que se ejecuta en segundo plano. Debemos tener en cuenta que debemos usar siempre el token de cancelación para poder finalizar rápidamente y cerrar el servicio correctamente.

public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }

Podemos ejecutarlo para ver cómo funciona la plantilla por default desde la consola con dotnet new. Veremos como el servicio es llamada 1 vez por segundo.

Podemos implementar eventos de inicialización y finalización del servicio para que nos informe esto eventos. Para esto crearemos la clase WorkerExtend.cs

public class WorkerExtend : BackgroundService
    {
        private readonly ILogger<WorkerExtend> _logger;

        public WorkerExtend(ILogger<WorkerExtend> logger)
        {
            _logger = logger;
        }

        public override async Task StartAsync(CancellationToken cancellationToken)
        {            
            _logger.LogInformation("Worker Start: {time}", DateTimeOffset.Now);
            await base.StartAsync(cancellationToken);
        }

        public override async Task StopAsync(CancellationToken cancellationToken)
        {            
            _logger.LogInformation("Worker Stop: {time}", DateTimeOffset.Now);
            await base.StopAsync(cancellationToken);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }

        public override void Dispose()
        {
            
        }
    }
}

Lo ejecutaremos desde la consola y veremos las salidas de los eventos correspondientes.

Ahora bien. Antes comente que podemos implementarlo como un Servicio de Windows. Para por hacerlo solo debemos agregar en el archivo program.cs la inyección para hacerlo.

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseWindowsService()
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<WorkerExtend>();
                });

Luego para implementarlo podemos usar la herramienta sc.exe de la siguiente forma.

sc.exe create DemoWS binpath= PathToThePublishFolder\YourWorkerClassName.exe
sc.exe start YourWorkerClassName

Conclusión

En versiones anteriores de .Net Core podíamos trabajar con Workers tengamos eso presentes. La ventaja de esta nueva plantilla es que nos permite tener pequeños servicios por separado fáciles de mantener. Ejecutar estos servicios en segundo plano tiene un costo muy pequeño de procesamiento eso los hace bastante útiles de utilizar aunque sean de larga ejecución. Espero que disfruten esta funcionalidad. Nos vemos en siguientes post.

Referencias

Fernando Sonego

Deja una respuesta

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