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.

27 comentarios en «Worker Service: Cómo crear un servicio .Net Core 3 multiplataforma»

  1. 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

    Responder
    • 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!

      Responder
  2. 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.

    Responder
    • 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!!

      Responder
    • 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!

      Responder
          • 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

            Responder
          • 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!

            Responder
  3. 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)?

    Responder
    • 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!!

      Responder
  4. 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.

    Responder
    • 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

      Responder
  5. excelente artículo, muchas gracias!

    Una duda…

    veo que se pueden crear varios workers y tiempo de depuración funcionan sin problemas, cada uno se hostea y se ejecuta de manera independiente y asíncrona.

    ¿Qué sucede al momento de montar los workers como servicios de windows?
    ¿debería de generar un compilado por cada servicio para poder realizar su registro por medio de la herramienta sc?

    ¡Saludos!

    Responder
    • Buenas Jorge,
      Muchas gracias por tu comentario! 🙂

      Cuando ejecutas diferentes BackgroundService que registras en el mismo host, cada uno de ellos se ejecuta de manera independiente de los demás (que no aislada, ya que el host es el mismo). Esto quiere decir que se ejecutará en su propia tarea dentro del host.

      Si quieres montar un servicio de windows no hay ningún problema si tienes varios BackgroundService registrados, funcionarán igual que cuando estas ejecutando desde Visual Studio. No es necesario que crees un .exe por cada BackgroundService ya que realmente son servicios distintos, hospedados en la misma aplicación.

      Espero haber resuelto tu duda (y si no es así no tengas problemas en seguir preguntando 🙂 )

      Un saludo

      Responder
  6. Excelente articulo.

    Estoy tratandod de crear un servicio que revisa los datos de una tabla en sql server y en funcion de los valores de los registros procesarlos. en la seccion Configure services del createhostbuilder configuro el acceso de datos mediante el uso de Services.adddbcontext,

    .ConfigureServices((hostContext, services) =>
    {
    services.AddDbContext(options => options.UseSqlServer(hostContext.Configuration.GetConnectionString(«Database»)));
    services.AddHostedService();
    })

    el constrictor del worker

    public Worker(DataAccess context, ILogger logger,IConfiguration configuration)
    {
    this.logger = logger;
    this.Configuration = configuration;
    this.context = context;
    }

    para luego hacer el di en el contructor del worker pero se presenta un error

    Error while validating the service descriptor ‘ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: SampleService.Worker’: Cannot consume scoped service ‘SampleService.Context.DataAccess’ from singleton ‘Microsoft.Extensions.Hosting.IHostedService’.

    alguna idea de como resolverlo. Muchas gracias

    Responder
    • Buena Luise,
      Por lo que veo tienes un error con la duración del tiempo de vida de los objetos que se inyectan. El ServiceWorker es Singleton mientras que tu acceso a datos es Scoped.
      Para solucionar esto tienes 2 opciones, o cambias tu acceso a datos a como mínimo Transient, o inyectas directamente el ServiceProvider y generas el scope dentro de tu HostedService. Cómo ejemplo puedes seguir esta respuesta de StackOverflow donde resuelven un problema similar.

      Un abrazo!

      Responder
  7. Hola gracias por el tutorial.

    tengo las siguiente consultas:

    1.- EN base de datos tengo una tabla con algunos parámetros como la hora de inicio y hora final del proceso, ¿como se hace para indicarle al servicio que comience su ejecución en x hora y termine en x hora?

    2. ¿qUE DIFEENCIA HAY ENTRE EL MÉTODO ExecuteAsync Y STARTASYNC?

    MUCHAS GRACIAS.

    Responder
    • Buenas tardes danilo ortíz,
      Gracias por tu comentario!!

      En primer lugar voy por la segunda pregunta. La diferencia entre ExecuteAsync y StartAsync es que el servicio se considera en marcha mientras dure el ExecuteAsync y es obligatorio definir que hace. StartAsync y StopAsync son dos métodos que podemos implementar para definir como tiene que arrancar y como tiene que parar. Siempre tienes que definir que hacer durante la ejecución, pero además puedes definir que hacer durante el arranque y la parada. Evidentemente podrías escribir todo el código dentro de ExecuteAsync, pero al separarlo en las 3 partes tienes mejor control del ciclo de vida del servicio.

      Para la primera pregunta, la verdad no hay una forma específica (que yo conozca) para eso. Se me ocurre que dentro del método ExecuteAsync donde tendrás el bucle de lo que ejecuta el servicio, añadas una condición donde compruebes que estas dentro del rango de horas donde quieres trabajar 🙂

      Un saludo

      Responder
  8. Hola buenas, he seguido tu guia paso a paso y me funciona correctamente PERO, SI QUISIERA QUE ARRANCASE EL SERVICIO PERO LA LLAMADA LA WORKER FUSES PROGRAMADA A ALGUNA HORA PRECISA ¿COMO LO HARIAS?, TENGO UN PROYECTO QUE ES UNA TAREA SCHEDULADA PERO AL PASARLE EL » ScheduleCalled(hostContext,services); » JUSTO DEBAJO DEL » ConfigureServices((hostContext, services) » ME ARRANCA EL SERVICIO PERO NUNCA HACE LA LLAMADA AL WORKER.

    gRACIAS DE ANTEMANO

    Responder
    • Buenas Tomas,
      Siento la tardanza, ha sido una semana complicada…
      Depende un poco de la precisión que necesites. Yo suelo utilizar la librería Cronos para interpretar expresiones cron y tirar un Task.Delay directamente cuando no necesito un sistema de jobs potente.
      Un saludo

      Responder
  9. Hola buenas, he seguido todos los pasos y cuando intento instalar el servicio en otro ordenador distinto al que he programado el servicio me sale el siguiente error:
    An assembly specified in the application dependencies manifest was not found:
    package: ‘Microsoft.Win32.Registry’, version: ‘5.0.0’
    path: ‘runtimes/win/lib/netstandard2.0/Microsoft.Win32.Registry.dll’

    He intentado descargar ese dll y ponerlo manualmente en esa ruta pero sigue sin funcionar, ¿alguna idea?

    Gracias.

    Responder

Deja un comentario