Implementar tareas en segundo plano en microservicios con IHostedService y la clase BackgroundServiceImplement background tasks in microservices with IHostedService and the BackgroundService class

Las tareas en segundo plano y los trabajos programados son algo que quizá tenga que implementar a la larga en una aplicación basada en microservicio o en cualquier tipo de aplicación.Background tasks and scheduled jobs are something you might need to implement, eventually, in a microservice based application or in any kind of application. La diferencia al utilizar una arquitectura de microservicios es que permite implementar un proceso/contenedor único de microservicio para hospedar estas tareas en segundo plano, por lo que se puede escalar o reducir verticalmente según sea necesario, e incluso es posible asegurarse de que se ejecuta una sola instancia de ese proceso o contenedor de microservicio.The difference when using a microservices architecture is that you can implement a single microservice process/container for hosting these background tasks so you can scale it down/up as you need or you can even make sure that it runs a single instance of that microservice process/container.

Desde un punto de vista genérico, en .NET Core este tipo de tareas se llaman Servicios hospedados, puesto que son servicios o lógica que se hospedan en el host, la aplicación o el microservicio.From a generic point of view, in .NET Core we called these type of tasks Hosted Services, because they are services/logic that you host within your host/application/microservice. Observe que, en este caso, el servicio hospedado simplemente significa una clase con la lógica de la tarea de segundo plano.Note that in this case, the hosted service simply means a class with the background task logic.

Desde la versión 2.0 de .NET Core, el marco proporciona una nueva interfaz denominada IHostedService que le ayuda a implementar fácilmente servicios hospedados.Since .NET Core 2.0, the framework provides a new interface named IHostedService helping you to easily implement hosted services. La idea básica es que pueda registrar varias tareas en segundo plano (servicios hospedados), que se ejecutan en segundo plano mientras se ejecuta el host o host web, tal como se muestra en la imagen 6-26.The basic idea is that you can register multiple background tasks (hosted services) that run in the background while your web host or host is running, as shown in the image 6-26.

Diagrama que compara IWebHost de ASP.NET Core e IHost de .NET Core.

Figura 6-26.Figure 6-26. Uso de IHostedService en un WebHost frente a un HostUsing IHostedService in a WebHost vs. a Host

ASP.NET Core 1.x y 2.x admiten IWebHost para los procesos en segundo plano en aplicaciones web.ASP.NET Core 1.x and 2.x support IWebHost for background processes in web apps. .NET Core 2.1 admite IHost para los procesos en segundo plano con aplicaciones de consola planas..NET Core 2.1 supports IHost for background processes with plain console apps. Observe la diferencia entre WebHost y Host.Note the difference made between WebHost and Host.

WebHost (clase base que implementa IWebHost) en ASP.NET Core 2.0 es el artefacto de infraestructura que se utiliza para proporcionar características de servidor HTTP al proceso, por ejemplo, si se va a implementar una aplicación web MVC o un servicio de API Web.A WebHost (base class implementing IWebHost) in ASP.NET Core 2.0 is the infrastructure artifact you use to provide HTTP server features to your process, such as if you are implementing an MVC web app or Web API service. Proporciona todas las ventajas de la nueva infraestructura de ASP.NET Core, lo que le permite usar la inserción de dependencias, insertar middleware en la canalización de solicitudes, etc., además de utilizar de manera precisa IHostedServices para tareas en segundo plano.It provides all the new infrastructure goodness in ASP.NET Core, enabling you to use dependency injection, insert middlewares in the request pipeline, etc. and precisely use these IHostedServices for background tasks.

Un Host (clase base que implementa IHost) se presentó en .NET Core 2.1.A Host (base class implementing IHost) was introduced in .NET Core 2.1. Básicamente, Host permite disponer de una infraestructura similar a la que se tiene con WebHost (inserción de dependencias, servicios hospedados, etc.), pero en este caso tan solo quiere tener un proceso sencillo y más ligero como host, sin ninguna relación con las características de servidor HTTP, MVC o API Web.Basically, a Host allows you to have a similar infrastructure than what you have with WebHost (dependency injection, hosted services, etc.), but in this case, you just want to have a simple and lighter process as the host, with nothing related to MVC, Web API or HTTP server features.

Por lo tanto, puede elegir y crear un proceso de host especializado con IHost para controlar los servicios hospedados y nada más, como por ejemplo un microservicio hecho solo para hospedar IHostedServices, o bien puede ampliar un WebHost de ASP.NET Core existente, como por ejemplo una aplicación MVC o API Web de ASP.NET Core.Therefore, you can choose and either create a specialized host-process with IHost to handle the hosted services and nothing else, such a microservice made just for hosting the IHostedServices, or you can alternatively extend an existing ASP.NET Core WebHost, such as an existing ASP.NET Core Web API or MVC app.

Cada enfoque tiene ventajas e inconvenientes dependiendo de sus necesidades empresariales y de escalabilidad.Each approach has pros and cons depending on your business and scalability needs. La conclusión es básicamente que, si las tareas en segundo plano no tienen nada que ver con HTTP (IWebHost), debe usar IHost.The bottom line is basically that if your background tasks have nothing to do with HTTP (IWebHost) you should use IHost.

Registro de servicios hospedados en Host o WebHostRegistering hosted services in your WebHost or Host

Vamos a profundizar más en la interfaz IHostedService puesto que su uso es muy similar en un WebHost o en un Host.Let’s drill down further on the IHostedService interface since its usage is pretty similar in a WebHost or in a Host.

SignalR es un ejemplo de un artefacto con servicios hospedados, pero también puede utilizarlo para cosas mucho más sencillas como las siguientes:SignalR is one example of an artifact using hosted services, but you can also use it for much simpler things like:

  • Una tarea en segundo plano que sondea una base de datos en busca de cambios.A background task polling a database looking for changes.
  • Una tarea programada que actualiza una caché periódicamente.A scheduled task updating some cache periodically.
  • Una implementación de QueueBackgroundWorkItem que permite que una tarea se ejecute en un subproceso en segundo plano.An implementation of QueueBackgroundWorkItem that allows a task to be executed on a background thread.
  • Procesar los mensajes de una cola de mensajes en el segundo plano de una aplicación web mientras se comparten servicios comunes como ILogger.Processing messages from a message queue in the background of a web app while sharing common services such as ILogger.
  • Una tarea en segundo plano iniciada con Task.Run().A background task started with Task.Run().

Básicamente, puede descargar cualquiera de esas acciones a una tarea en segundo plano basada en IHostedService.You can basically offload any of those actions to a background task based on IHostedService.

La forma de agregar uno o varios elementos IHostedServices en WebHost o Host es registrarlos a través del método de extensión AddHostedService en un elemento WebHost de ASP.NET Core (o en un elemento Host en .NET Core 2.1 y versiones posteriores).The way you add one or multiple IHostedServices into your WebHost or Host is by registering them up through the AddHostedService extension method in an ASP.NET Core WebHost (or in a Host in .NET Core 2.1 and above). Básicamente, tiene que registrar los servicios hospedados dentro del conocido método ConfigureServices() de la clase Startup, como se muestra en el código siguiente de un WebHost de ASP.NET típico.Basically, you have to register the hosted services within the familiar ConfigureServices() method of the Startup class, as in the following code from a typical ASP.NET WebHost.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    //Other DI registrations;

    // Register Hosted Services
    services.AddHostedService<GracePeriodManagerService>();
    services.AddHostedService<MyHostedServiceB>();
    services.AddHostedService<MyHostedServiceC>();
    //...
}

En el código, el servicio hospedado GracePeriodManagerService es código real del microservicio de negocios de pedido en eShopOnContainers, mientras que los otros dos son solo dos ejemplos adicionales.In that code, the GracePeriodManagerService hosted service is real code from the Ordering business microservice in eShopOnContainers, while the other two are just two additional samples.

La ejecución de la tarea en segundo plano IHostedService se coordina con la duración de la aplicación (host o microservicio para este propósito).The IHostedService background task execution is coordinated with the lifetime of the application (host or microservice, for that matter). Las tareas se registran cuando se inicia la aplicación y, cuando se esté cerrando la aplicación, tendrá la oportunidad de limpiar o realizar alguna acción correcta.You register tasks when the application starts and you have the opportunity to do some graceful action or clean-up when the application is shutting down.

Sin usar IHostedService, siempre se puede iniciar un subproceso en segundo plano para ejecutar cualquier tarea.Without using IHostedService, you could always start a background thread to run any task. La diferencia está precisamente en el tiempo de cierre de la aplicación, cuando ese subproceso simplemente terminaría sin tener la oportunidad de ejecutar las acciones de limpieza correcta.The difference is precisely at the app’s shutdown time when that thread would simply be killed without having the opportunity to run graceful clean-up actions.

Interfaz de IHostedServiceThe IHostedService interface

Al registrar un IHostedService, .NET Core llamará a los métodos StartAsync() y StopAsync() de su tipo IHostedService durante el inicio y la detención de la aplicación respectivamente.When you register an IHostedService, .NET Core will call the StartAsync() and StopAsync() methods of your IHostedService type during application start and stop respectively. En concreto, se llama a start después de que el servidor se inicie y se desencadene IApplicationLifetime.ApplicationStarted.Specifically, start is called after the server has started and IApplicationLifetime.ApplicationStarted is triggered.

IHostedService, tal como se define en .NET Core, se parece a lo siguiente.The IHostedService as defined in .NET Core, looks like the following.

namespace Microsoft.Extensions.Hosting
{
    //
    // Summary:
    //     Defines methods for objects that are managed by the host.
    public interface IHostedService
    {
        //
        // Summary:
        // Triggered when the application host is ready to start the service.
        Task StartAsync(CancellationToken cancellationToken);
        //
        // Summary:
        // Triggered when the application host is performing a graceful shutdown.
        Task StopAsync(CancellationToken cancellationToken);
    }
}

Como puede imaginarse, es posible crear varias implementaciones de IHostedService y registrarlas en el método ConfigureService() en el contenedor de DI, tal y como se ha mostrado anteriormente.As you can imagine, you can create multiple implementations of IHostedService and register them at the ConfigureService() method into the DI container, as shown previously. Todos los servicios hospedados se iniciarán y detendrán junto con la aplicación o microservicio.All those hosted services will be started and stopped along with the application/microservice.

Los desarrolladores son responsables de controlar la acción de detención o los servicios cuando el host activa el método StopAsync().As a developer, you are responsible for handling the stopping action of your services when StopAsync() method is triggered by the host.

Implementación de IHostedService con una clase de servicio hospedado personalizado que se deriva de la clase base BackgroundServiceImplementing IHostedService with a custom hosted service class deriving from the BackgroundService base class

Puede seguir adelante y crear una clase de servicio hospedado personalizado desde cero e implementar IHostedService, tal y como se debe hacer cuando se usa .NET Core 2.0.You could go ahead and create your custom hosted service class from scratch and implement the IHostedService, as you need to do when using .NET Core 2.0.

Pero como la mayoría de las tareas en segundo plano tienen necesidades similares en relación con la administración de tokens de cancelación y otras operaciones habituales, hay una clase base abstracta práctica denominada BackgroundService de la que puede derivar (disponible desde .NET Core 2.1).However, since most background tasks will have similar needs in regard to the cancellation tokens management and other typical operations, there is a convenient abstract base class you can derive from, named BackgroundService (available since .NET Core 2.1).

Esta clase proporciona el trabajo principal necesario para configurar la tarea en segundo plano.That class provides the main work needed to set up the background task.

El código siguiente es la clase base abstracta de BackgroundService tal y como se implementa en .NET Core.The next code is the abstract BackgroundService base class as implemented in .NET Core.

// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it,
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }

    }

    public virtual void Dispose()
    {
        _stoppingCts.Cancel();
    }
}

Al derivar de la clase base abstracta anterior, y gracias a la implementación heredada, solo tiene que implementar el método ExecuteAsync() en su clase de servicio hospedado personalizado propio, como en el siguiente ejemplo simplificado de código de eShopOnContainers, en el que se sondea una base de datos y se publican eventos de integración en el bus de eventos cuando es necesario.When deriving from the previous abstract base class, thanks to that inherited implementation, you just need to implement the ExecuteAsync() method in your own custom hosted service class, as in the following simplified code from eShopOnContainers which is polling a database and publishing integration events into the Event Bus when needed.

public class GracePeriodManagerService : BackgroundService
{
    private readonly ILogger<GracePeriodManagerService> _logger;
    private readonly OrderingBackgroundSettings _settings;

    private readonly IEventBus _eventBus;

    public GracePeriodManagerService(IOptions<OrderingBackgroundSettings> settings,
                                     IEventBus eventBus,
                                     ILogger<GracePeriodManagerService> logger)
    {
        //Constructor’s parameters validations...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogDebug($"GracePeriodManagerService is starting.");

        stoppingToken.Register(() =>
            _logger.LogDebug($" GracePeriod background task is stopping."));

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogDebug($"GracePeriod task doing background work.");

            // This eShopOnContainers method is querying a database table
            // and publishing events into the Event Bus (RabbitMQ / ServiceBus)
            CheckConfirmedGracePeriodOrders();

            await Task.Delay(_settings.CheckUpdateTime, stoppingToken);
        }

        _logger.LogDebug($"GracePeriod background task is stopping.");
    }

    .../...
}

En este caso concreto de eShopOnContainers, se ejecuta un método de aplicación que consulta una tabla de base de datos en la que busca pedidos con un estado específico y al aplicar los cambios, está publicando eventos de integración a través del bus de eventos (de forma subyacente puede estar utilizando RabbitMQ o Azure Service Bus).In this specific case for eShopOnContainers, it's executing an application method that's querying a database table looking for orders with a specific state and when applying changes, it is publishing integration events through the event bus (underneath it can be using RabbitMQ or Azure Service Bus).

Por supuesto, en su lugar puede ejecutar cualquier otra tarea en segundo plano empresarial.Of course, you could run any other business background task, instead.

De forma predeterminada, el token de cancelación se establece con un tiempo de espera de 5 segundos, aunque se puede cambiar ese valor al compilar su WebHost mediante la extensión UseShutdownTimeout de IWebHostBuilder.By default, the cancellation token is set with a 5 second timeout, although you can change that value when building your WebHost using the UseShutdownTimeout extension of the IWebHostBuilder. Esto significa que se espera que nuestro servicio se cancele en 5 segundos o, en caso contrario, se terminará de manera repentina.This means that our service is expected to cancel within 5 seconds otherwise it will be more abruptly killed.

El código siguiente cambiaría ese tiempo a 10 segundos.The following code would be changing that time to 10 seconds.

WebHost.CreateDefaultBuilder(args)
    .UseShutdownTimeout(TimeSpan.FromSeconds(10))
    ...

Diagrama de clases de resumenSummary class diagram

En la siguiente ilustración se muestra un resumen visual de las clases y las interfaces implicadas al implementar IHostedServices.The following image shows a visual summary of the classes and interfaces involved when implementing IHostedServices.

Diagrama que muestra que IWebHost y IHost pueden hospedar muchos servicios.

Figura 6-27.Figure 6-27. Diagrama de clases que muestra las distintas clases e interfaces relacionadas con IHostedServiceClass diagram showing the multiple classes and interfaces related to IHostedService

Diagrama de clases: IWebHost y IHost pueden hospedar muchos servicios, que heredan de BackgroundService, que implementa IHostedService.Class diagram: IWebHost and IHost can host many services, which inherit from BackgroundService, which implements IHostedService.

Impresiones y consideraciones sobre implementaciónDeployment considerations and takeaways

Es importante tener en cuenta que la forma de implementar su ASP.NET Core WebHost o .NET Core Host puede afectar a la solución final.It is important to note that the way you deploy your ASP.NET Core WebHost or .NET Core Host might impact the final solution. Por ejemplo, si implementa su WebHost en IIS o en un servicio de Azure App Service normal, el host se puede cerrar debido a reciclajes del grupo de aplicaciones.For instance, if you deploy your WebHost on IIS or a regular Azure App Service, your host can be shut down because of app pool recycles. Pero si va a implementar el host como un contenedor en un orquestador como Kubernetes o Service Fabric, puede controlar el número garantizado de instancias activas del host.But if you are deploying your host as a container into an orchestrator like Kubernetes or Service Fabric, you can control the assured number of live instances of your host. Además, podría considerar otros métodos en la nube pensados especialmente para estos escenarios, como Azure Functions.In addition, you could consider other approaches in the cloud especially made for these scenarios, like Azure Functions. Por último, si necesita que el servicio se ejecute todo el tiempo y se implemente en Windows Server, podría usar un servicio de Windows.Finally, if you need the service to be running all the time and are deploying on a Windows Server you could use a Windows Service.

Pero incluso para un elemento WebHost implementado en un grupo de aplicaciones, hay escenarios, como el relleno o el vaciado de la memoria caché de la aplicación, en los que sería también aplicable.But even for a WebHost deployed into an app pool, there are scenarios like repopulating or flushing application’s in-memory cache that would be still applicable.

La interfaz IHostedService proporciona una manera cómoda de iniciar tareas en segundo plano en una aplicación web de ASP.NET (en .NET Core 2.0) o en cualquier proceso o host (a partir de .NET Core 2.1 con IHost).The IHostedService interface provides a convenient way to start background tasks in an ASP.NET Core web application (in .NET Core 2.0) or in any process/host (starting in .NET Core 2.1 with IHost). La principal ventaja es la oportunidad de obtener con la cancelación correcta un código de limpieza de sus tareas en segundo plano cuando se está cerrando el propio host.Its main benefit is the opportunity you get with the graceful cancellation to clean-up code of your background tasks when the host itself is shutting down.

Recursos adicionalesAdditional resources