Worker Service: Cómo crear un servicio .Net Core 3 multiplataforma

Tiempo de lectura: 5 minutos
La imagen muestra el logo de .Net Core 3.0

Actualizado el 13-01-2020 para añadir el soporte oficial a Systemd

Hace unos meses, hablamos sobre como crear servicios multiplataforma con .Net Core 2, donde veíamos que había que hacer algunas «perrerías» para poder conseguirlo… Ha llegado .Net Core 3, y me ha parecido interesante hablar sobre la nueva manera que nos trae de conseguir esto.

En primer lugar, tenemos una nueva plantilla disponible para esto: Worker Service. Usando esta plantilla, vamos a poder tener un ejemplo para construir nuestro servicio. Aunque perfectamente podemos hacerlo desde una aplicación de consola añadiendo nosotros el código, vamos a «estrenar» la plantilla (si nos dan trabajo hecho, tampoco vamos a decir que no… xD )

Creando la solución

Lo primero por supuesto, es crear nuestra solución, y en ella, vamos a elegir la plantilla «Servicio de trabajo» (Worker Service en inglés):

La imagen muestra la plantilla seleccionada

Esto nos va a crear una solución con dos archivos, Program.cs y Worker.cs. Si abrimos Program.cs, vamos a poder comprobar que nuestro proyecto no es más que una aplicación de consola que crea un «Host» (al igual que hacíamos en .Net Core 2.X).

namespace PostWorker
{
    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>();
                });
    }
}

El segundo fichero que tenemos, Worker.cs, tiene una pinta parecida a esta:

namespace PostWorker
{
    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);
            }
        }
    }
}

La principal novedad al crear nuestras tareas dentro del servicio, es que ahora heredamos de la clase abstracta «BackgroundService» (y no de IHostedService como hacíamos en .Net Core 2.X). Esto simplemente es para aportarnos una capa de abstracción sobre su funcionamiento, y realmente es esta clase la que implementa IHostedService y nos ofrece 3 métodos para trabajar con ella:

protected abstract Task ExecuteAsync(CancellationToken stoppingToken)
public virtual Task StartAsync(CancellationToken cancellationToken)
public virtual Task StopAsync(CancellationToken cancellationToken)        

Para crear nuestro servicio básico, tal como lo hace el ejemplo, basta con implementar el primero de ellos (que además al ser abstracto estamos obligados a implementar). Pero vamos a profundizar un poco más…

El primero de ellos, va a ser lo que queremos que se ejecute dentro de nuestro servicio, con la implementación por defecto, cuando arranque el servicio, se va a llamar directamente a ese método en una tarea. De igual manera, cuando el servicio se pare, se cancelará el token. Es importante tener en cuenta el CancellationToken como sistema de control del ciclo de vida, para evitar tener tareas bloqueadas al parar nuestro servicio.

Si necesitamos una funcionalidad un poco más avanzada, como ejecutar algo de código previo al arranque o en la parada, lo que vamos a tener que hacer es sobrescribir los métodos StartAsync y StopAsync según necesitemos, ya que no es obligatorio hacerlo con los dos. Un caso en el que nos podría hacer falta, es cuando necesitamos diferenciar las labores de arranque del servicio con las propias de ejecución. Por ejemplo, si quieres loguear cuando arranca o para tu servicio .Net Core 3:

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

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

        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Worker starts");
            await base.StartAsync(cancellationToken);
        }

        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Worker stops");
            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);
            }
        }
    }
}

Es importante señalar, que podemos registrar tantos «Workers» como necesitemos, basta con que lo añadamos en el inyector de dependencias. Por ejemplo, si tuviésemos una clase Worker1 y otra Worker2, nuestro Program.cs se vería algo así:

namespace PostWorker
{
    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<Worker1>();
                    services.AddHostedService<Worker2>();                    
                });
    }
}

Con esto, ya tenemos un código perfectamente funcional como servicio, de hecho, si lo instalamos en Linux como servicio (daemon), podremos comprobar que funciona perfectamente. En cambio, si lo instalamos en Windows, para nuestra sorpresa (o no), simplemente esto no va a funcionar.

Instalando nuestro servicio .Net Core 3 en Windows

Si alguna vez has creado un servicio en Windows, sabes que tiene sus cositas… En .Net Core 2.X, teníamos que hacer ciertas perrerías para conseguirlo, pero por suerte, ahora tenemos un paquete NuGet que nos permite hacerlo sin dificultades (básicamente, hace las perrerías por nosotros). Este paquete es «Microsoft.Extensions.Hosting.WindowsServices«, basta con que lo añadamos a nuestro proyecto y que añadamos su uso en Program.cs con «UseWindowsService» sobre el HostBuilder:

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

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

Este paquete lo que va a hacer, es registrar los servicios necesarios en el inyector de dependencias si corremos sobre Windows, y no hacer nada en el resto de los casos.

Mucho ojo, lo que hay que registrar como servicio es el binario (.exe) y no la librería (.dll)

Instalando nuestro servicio .Net Core 3 en Systemd (Linux)

Pese a que funciona correctamente sobre Linux sin tener que hacer nada, Microsoft ha creado un paquete NuGet para trabajar bajo Systemd. Este paquete es Microsoft.Extensions.Hosting.Systemd y mejora el tratamiento de las señales del sistema por lo que es altamente recomendable utilizarlo al igual que hacemos con el de Windows (aunque el de Windows sea obligatorio y este opcional).

Para poder utilizarlo basta con que añadamos la llamada al método para que se registren las dependencias necesarias.

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

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

Ambos paquetes (tanto el de Windows como el de Linux) registran las dependencias en caso de que se este ejecutando en el sistema que las necesita. Es decir, el hecho de tener ambos sistemas operativos configurados no implica ningún problema más allá de añadir una pequeña dependencia más.

Con esto, ya tenemos nuestro servicio en .Net Core 3 listo para instalarlo en cualquier máquina, independientemente del sistema operativo que utilicemos. Si quieres probarlo tú mismo, he dejado el código en GitHub para que puedas salsearlo sin problemas.

17 pensamientos en “Worker Service: Cómo crear un servicio .Net Core 3 multiplataforma

  1. BikerDEV

    Hola al crear el servicio con el comando sc.exe nombreDelServicio bindPath=pathDelEjecutable
    y lo inicio desde servicio para ver que funcione, se inicia pero al momento se detiene esto por que motivo es ? gracias

    1. JorTurFer Autor

      Buenas BikerDEV,
      Siento oír que no te arranca… Aquí podemos tener varios problemas que lo impiden arrancar, desde que falte alguna configuración o paquete hasta que falte un permiso (asumiendo que la ruta al ejecutable es la correcta).
      Lo primero que te recomendaría es ir al visor de eventos de Windows ya que todas las aplicaciones .Net registran si ha habido fallos que las hayan cerrado:
      La imagen muestra una captura de pantalla del visor de eventos con las zonas señaladas

      Ahí vas a poder ver la razón por la que se ha producido el fallo que ha parado el servicio e información de la pila. Con eso es posible ir tirando del hilo para ver porque ha fallado.
      Si quieres puedes enviarme la información a través del formulario de contacto y seguimos por email, así no hay información sensible de manera pública en los comentarios.

      Un abrazo!

  2. Raúl

    Hola JorTurFer. Estoy aprendiendo en esto de los Workers Services en Net Core.
    Me bajé tu servicio. Lo instalé con SC CREATE xxx binpath:….
    Pero cuando lo quiero arrancar me sale el error: Error 1053: El servicio no respondió a tiempo a la solicitud de inicio o de control.
    Probé de meterme en el regedit y poner la palabra ServicesPipeTimeout en 86400000. Nada.
    En el visor de eventos dice:

    Aplicación: PostWorker.dll
    Versión de Framework: v4.0.30319
    Descripción: el proceso terminó debido a una excepción no controlada.
    Información de la excepción: código de la excepción e0434352, dirección de la excepción 00007FFE53AAA859

    Nombre de la aplicación con errores: PostWorker.dll, versión: 1.0.0.0, marca de tiempo: 0xf7611013
    Nombre del módulo con errores: KERNELBASE.dll, versión: 10.0.18362.719, marca de tiempo: 0xb31987d3
    Código de excepción: 0xe0434352
    Desplazamiento de errores: 0x000000000003a859
    Identificador del proceso con errores: 0x848
    Hora de inicio de la aplicación con errores: 0x01d602e94f73c5e2
    Ruta de acceso de la aplicación con errores: C:\Users\Raúl Marina\Downloads\ServicioNetCore3-master\PostWorker\bin\Release\netcoreapp3.1\PostWorker.dll
    Ruta de acceso del módulo con errores: C:\WINDOWS\System32\KERNELBASE.dll
    Identificador del informe: a75ede1b-6b90-43ba-8de8-c7fe5153431e

    Depósito con errores 1468785531493006916, tipo 4
    Nombre de evento: APPCRASH
    Respuesta: No disponible
    Identificador de archivo .cab: 0

    Firma del problema:
    P1: PostWorker.dll
    P2: 1.0.0.0
    P3: f7611013
    P4: KERNELBASE.dll
    P5: 10.0.18362.719
    P6: b31987d3
    P7: e0434352
    P8: 000000000003a859
    P9:
    P10:

    Me das una mano? Desde ya, muchas gracias.

    1. JorTurFer Autor

      Buenas noches Raúl,
      Siento leer que te ha dado problemas…
      De esa traza de error que has puesto me da la sensación de que a la hora de registrar el servicio has puesto la ruta hasta la dll y no hasta el exe.
      ¿Podrías confirmar si el error viene de ahí? Lo que vas a necesitar es borrar el servicio con sc delete "NombreServicio" y volverlo a registrar con sc create "NombreServicio" binpath="Ruta hasta el exe"
      Si después de hacerlo te da problemas responde otra vez y lo seguimos comprobando. Si vuelve a fallar pega por favor el otro error del visor de eventos, el que tiene en la columna source ‘.NET Runtime‘.

      Quedo a la espera de tus noticias
      Un abrazo!!

    1. JorTurFer Autor

      Buenas Raúl,
      Me alegro de leer que ya esta solucionado! 🙂
      Eso es porque siempre hay que registrar como servicio el exe, en la entrada donde se hacia con .Net Core 2 estaba más claro pero aquí igual resultaba algo confuso que es lo que hay que registrar como servicio, por si acaso he actualizado la entrada y lo he dejado aclarado para los siguientes.

      Un abrazo!

      1. Raúl

        Muchas gracias JorTurFer! Lo que sí no puedo hacer es referenciarlo como servicio conectado a una api… Cómo hago eso? Me podés ayudar?

          1. JorTurFer Autor

            Buenas noches Raúl,
            Creo que estas mezclando conceptos. A lo que tu te refieres es a un web service. La entrada hace referencia a un servicio de windows/daemon de Linux, no a un web service. Si lo que quieres es hacer esto segundo, tendrás que utilizar .Net Framework.
            Te dejo un enlace a documentación sobre como hacerlo. Si necesitas que te pase más info avísame.

            Un abrazo

          2. JorTurFer Autor

            Buenas noches Raul,
            He tenido un problema que me ha obligado a restaurar una copia de seguridad así que tu último comentario se ha perdido, lo voy a poner aquí como referencia:

            «Apa… No sabía esto. Yo en realidad tengo hecho el servicio en net framework pero en la empresa me han pedido que use net core y ahí buscando encontré está página que está muy bien explicada. Entonces no hay manera de hacerlo con tecnología Net Core?»

            Respondiendo a tu pregunta, que tipo de servicio quieres hacer? un servicio para el sistema operativo o un servicio web? Si es un servicio web, .Net Core no soporta los web services como tal de .Net Framework, aunque hay alternativas que también funcionan muy bien como gRPC o APIs REST. Si por el contrario es un servicio para el sistema operativo (que por la pregunta de wsdl entiendo que no), esta entrada te vale perfectamente.

            Si quieres coméntame por mail más sobre tu proyecto y lo vemos mejor

            Un abrazo!

  3. Juan

    JorTurFer, gracias por el articulo. Tengo alguna forma de consumir(me refiero a consultar un método publico) en este servicio ? Es decir, me gustaría que el mismo este corriendo en una maquina windows que registra cuando se abre una puerta. El servicio me obtiene esa información, pero quiero saber como puedo obtener esa información sin tener que consultar el txt que estoy escribiendo.
    La pregunta es si puedo consultar un método que exista en el servicio, por ejemplo «ConsultarSiAbrioPuerta()» desde una aplicación que estoy corriendo en la misma maquina(una web en iis)?

    1. JorTurFer Autor

      Buenos días Juan,
      La respuesta es sí y no. No directamente ya que el servicio no expone nada pero si se puede hacer. Lo que podrías hacer utilizar una API Rest para que añadas a este servicio para que la web pueda consultar los datos cuando le haga falta, o igual más fácil aun, añadir una API Rest en la web que ya tienes para que el servicio que esta leyendo los datos se los envíe a la web directamente y ya los tengas ahí.

      Otra opción es que el servicio que registra los datos los mande también a una base de datos y que la web los lea de ahí directamente. Depende un poco de lo que quieras buscar es mejor una aproximación u otra.

      En cualquier caso yo personalmente evitaría que la web lea el fichero que genera el servicio de lectura ya que pueden llegar a producirse bloqueos y otros problemas.

      Un abrazo!!

  4. Miguel

    Excelente articulo.

    Actualmente estoy creando un Worker que se conecte a un servicio Web para descargar ciertos datos y procesarlos. Ya tengo una version Net Framework del servicio pero al parecer en Net Core la forma de consumir el servicio Web es diferente. ¿sabes de algun sitio donde pueda encontrar un ejemplo parecido al mio? De antemano muchas gracias.

    PD. Estoy tratando de implementar IHttpClientFactory.

    1. JorTurFer Autor

      Hola Miguel,
      Gracias por tu comentario! 🙂
      Depende un poco de que entendamos por servicio web… Si estamos entendiendo servicio web por una API REST, no deberías tener ningún problema utilizando los verbos HTTP, hay mucha documentación al respecto pero si quieres dime y te busco alguna en concreto.
      Si en cambio entendemos servicio web por los clásicos WCF, la cosa esta más complicada pero no debería ser imposible. La verdad es que no lo he hecho nunca pero según esta entrada de medium debería poder hacerse sin problemas. Revisando un poco la documentación oficial, parece que hay soporte expreso para WCF. Puedes ver como hacerlo en Uso de la herramienta WCF Web Service Reference Provider

      Espero haber resuelto tus dudas
      Un abrazo

Deja una respuesta

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