Tareas en segundo plano con servicios hospedados en ASP.NET CoreBackground tasks with hosted services in ASP.NET Core

Por Luke LathamBy Luke Latham

En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados.In ASP.NET Core, background tasks can be implemented as hosted services. Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService.A hosted service is a class with background task logic that implements the IHostedService interface. En este tema se incluyen tres ejemplos de servicio hospedado:This topic provides three hosted service examples:

  • Una tarea en segundo plano que se ejecuta según un temporizador.Background task that runs on a timer.
  • Un servicio hospedado que activa un servicio con ámbito.Hosted service that activates a scoped service. El servicio con ámbito puede usar la inserción de dependencias.The scoped service can use dependency injection.
  • Tareas en segundo plano en cola que se ejecutan en secuencia.Queued background tasks that run sequentially.

Vea o descargue el código de ejemplo (cómo descargarlo)View or download sample code (how to download)

La aplicación de ejemplo se ofrece en dos versiones:The sample app is provided in two versions:

  • Host web: el host web resulta útil para hospedar aplicaciones web.Web Host – Web Host is useful for hosting web apps. El código de ejemplo que se muestra en este tema corresponde a la versión de host web del ejemplo.The example code shown in this topic is from Web Host version of the sample. Para más información, vea el sitio web Host de web.For more information, see the Web Host topic.
  • Host genérico: el host genérico es nuevo en ASP.NET Core 2.1.Generic Host – Generic Host is new in ASP.NET Core 2.1. Para más información, vea el sitio web Host genérico.For more information, see the Generic Host topic.

PackagePackage

Haga referencia al metapaquete Microsoft.AspNetCore.App o agregue una referencia de paquete al paquete Microsoft.Extensions.Hosting.Reference the Microsoft.AspNetCore.App metapackage or add a package reference to the Microsoft.Extensions.Hosting package.

Interfaz IHostedServiceIHostedService interface

Los servicios hospedados implementan la interfaz IHostedService.Hosted services implement the IHostedService interface. Esta interfaz define dos métodos para los objetos administrados por el host:The interface defines two methods for objects that are managed by the host:

  • StartAsync(CancellationToken): StartAsync contiene la lógica para iniciar la tarea en segundo plano.StartAsync(CancellationToken)StartAsync contains the logic to start the background task. Al utilizar el host web, se llama a StartAsync después de que el servidor se haya iniciado y se haya activado IApplicationLifetime.ApplicationStarted.When using the Web Host, StartAsync is called after the server has started and IApplicationLifetime.ApplicationStarted is triggered. Al utilizar el host genérico, se llama a StartAsync antes de que se desencadene ApplicationStarted.When using the Generic Host, StartAsync is called before ApplicationStarted is triggered.

  • StopAsync(CancellationToken): se activa cuando el host está realizando un cierre estable.StopAsync(CancellationToken) – Triggered when the host is performing a graceful shutdown. StopAsync contiene la lógica para finalizar la tarea en segundo plano.StopAsync contains the logic to end the background task. Implemente IDisposable y los finalizadores (destructores) para desechar los recursos no administrados.Implement IDisposable and finalizers (destructors) to dispose of any unmanaged resources.

    El token de cancelación tiene un tiempo de espera predeterminado de cinco segundos para indicar que el proceso de cierre ya no debería ser estable.The cancellation token has a default five second timeout to indicate that the shutdown process should no longer be graceful. Cuando se solicita la cancelación en el token:When cancellation is requested on the token:

    • Se deben anular las operaciones restantes en segundo plano que realiza la aplicación.Any remaining background operations that the app is performing should be aborted.
    • Los métodos llamados en StopAsync deberían devolver contenido al momento.Any methods called in StopAsync should return promptly.

    No obstante, las tareas no se abandonan después de solicitar la cancelación, sino que el autor de la llamada espera a que se completen todas las tareas.However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.

    Si la aplicación se cierra inesperadamente (por ejemplo, porque se produzca un error en el proceso de la aplicación), puede que no sea posible llamar a StopAsync.If the app shuts down unexpectedly (for example, the app's process fails), StopAsync might not be called. Por lo tanto, los métodos llamados o las operaciones llevadas a cabo en StopAsync podrían no producirse.Therefore, any methods called or operations conducted in StopAsync might not occur.

    Para ampliar el tiempo de espera predeterminado de apagado de 5 segundos, establezca:To extend the default five second shutdown timeout, set:

El servicio hospedado se activa una vez al inicio de la aplicación y se cierra de manera estable cuando dicha aplicación se cierra.The hosted service is activated once at app startup and gracefully shut down at app shutdown. Si se produce un error durante la ejecución de una tarea en segundo plano, hay que llamar a Dispose, aun cuando no se haya llamado a StopAsync.If an error is thrown during background task execution, Dispose should be called even if StopAsync isn't called.

Tareas en segundo plano temporizadasTimed background tasks

Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer.A timed background task makes use of the System.Threading.Timer class. El temporizador activa el método DoWork de la tarea.The timer triggers the task's DoWork method. El temporizador está deshabilitado en StopAsync y se desecha cuando el contenedor de servicios se elimina en Dispose:The timer is disabled on StopAsync and disposed when the service container is disposed on Dispose:

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

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

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

El servicio se registra en Startup.ConfigureServices con el método de extensión AddHostedService:The service is registered in Startup.ConfigureServices with the AddHostedService extension method:

services.AddHostedService<TimedHostedService>();

Consumir un servicio con ámbito en una tarea en segundo planoConsuming a scoped service in a background task

Para usar servicios con ámbito en un elemento IHostedService, cree un ámbito.To use scoped services within an IHostedService, create a scope. No se crean ámbitos de forma predeterminada para los servicios hospedados.No scope is created for a hosted service by default.

El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano.The scoped background task service contains the background task's logic. En el siguiente ejemplo, ILogger se inserta en el servicio:In the following example, an ILogger is injected into the service:

internal interface IScopedProcessingService
{
    void DoWork();
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public void DoWork()
    {
        _logger.LogInformation("Scoped Processing Service is working.");
    }
}

El servicio hospedado crea un ámbito con objeto de resolver el servicio de tareas en segundo plano con ámbito para llamar a su método DoWork:The hosted service creates a scope to resolve the scoped background task service to call its DoWork method:

internal class ConsumeScopedServiceHostedService : IHostedService
{
    private readonly ILogger _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is starting.");

        DoWork();

        return Task.CompletedTask;
    }

    private void DoWork()
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            scopedProcessingService.DoWork();
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        return Task.CompletedTask;
    }
}

Los servicios se registran en Startup.ConfigureServices.The services are registered in Startup.ConfigureServices. La implementación IHostedService se registra con el método de extensión AddHostedService:The IHostedService implementation is registered with the AddHostedService extension method:

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Tareas en segundo plano en colaQueued background tasks

Las colas de tareas en segundo plano se basan en QueueBackgroundWorkItem de .NET 4.x (está previsto que se integre en ASP.NET Core 3.0):A background task queue is based on the .NET 4.x QueueBackgroundWorkItem (tentatively scheduled to be built-in for ASP.NET Core 3.0):

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

    Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private ConcurrentQueue<Func<CancellationToken, Task>> _workItems = 
        new ConcurrentQueue<Func<CancellationToken, Task>>();
    private SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void QueueBackgroundWorkItem(
        Func<CancellationToken, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        _workItems.Enqueue(workItem);
        _signal.Release();
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _workItems.TryDequeue(out var workItem);

        return workItem;
    }
}

En QueueHostedService, las tareas en segundo plano en la cola se quitan de la cola y se ejecutan como un servicio BackgroundService, que es una clase base para implementar IHostedService de ejecución prolongada:In QueueHostedService, background tasks in the queue are dequeued and executed as a BackgroundService, which is a base class for implementing a long running IHostedService:

public class QueuedHostedService : BackgroundService
{
    private readonly ILogger _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILoggerFactory loggerFactory)
    {
        TaskQueue = taskQueue;
        _logger = loggerFactory.CreateLogger<QueuedHostedService>();
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected async override Task ExecuteAsync(
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Queued Hosted Service is starting.");

        while (!cancellationToken.IsCancellationRequested)
        {
            var workItem = await TaskQueue.DequeueAsync(cancellationToken);

            try
            {
                await workItem(cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                   $"Error occurred executing {nameof(workItem)}.");
            }
        }

        _logger.LogInformation("Queued Hosted Service is stopping.");
    }
}

Los servicios se registran en Startup.ConfigureServices.The services are registered in Startup.ConfigureServices. La implementación IHostedService se registra con el método de extensión AddHostedService:The IHostedService implementation is registered with the AddHostedService extension method:

services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();

En la clase de modelo de página de índice:In the Index page model class:

  • Se inserta IBackgroundTaskQueue en el constructor y se asigna a Queue.The IBackgroundTaskQueue is injected into the constructor and assigned to Queue.
  • Se inserta IServiceScopeFactory y se asigna a _serviceScopeFactory.An IServiceScopeFactory is injected and assigned to _serviceScopeFactory. Se usa el generador se para crear instancias de IServiceScope, que se usa para crear servicios dentro de un ámbito.The factory is used to create instances of IServiceScope, which is used to create services within a scope. Se crea un ámbito para poder usar el elemento AppDbContext de la aplicación (un servicio con ámbito) para escribir registros de base de datos en IBackgroundTaskQueue (un servicio de singleton).A scope is created in order to use the app's AppDbContext (a scoped service) to write database records in the IBackgroundTaskQueue (a singleton service).
public class IndexModel : PageModel
{
    private readonly AppDbContext _db;
    private readonly ILogger _logger;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public IndexModel(AppDbContext db, IBackgroundTaskQueue queue, 
        ILogger<IndexModel> logger, IServiceScopeFactory serviceScopeFactory)
    {
        _db = db;
        _logger = logger;
        Queue = queue;
        _serviceScopeFactory = serviceScopeFactory;
    }

    public IBackgroundTaskQueue Queue { get; }

Cuando se hace clic en el botón Agregar tarea en la página de índice, se ejecuta el método OnPostAddTask.When the Add Task button is selected on the Index page, the OnPostAddTask method is executed. Se llama a QueueBackgroundWorkItem para poner en cola el elemento de trabajo:QueueBackgroundWorkItem is called to enqueue the work item:

public IActionResult OnPostAddTaskAsync()
{
    Queue.QueueBackgroundWorkItem(async token =>
    {
        var guid = Guid.NewGuid().ToString();

        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var scopedServices = scope.ServiceProvider;
            var db = scopedServices.GetRequiredService<AppDbContext>();

            for (int delayLoop = 1; delayLoop < 4; delayLoop++)
            {
                try
                {
                    db.Messages.Add(
                        new Message() 
                        { 
                            Text = $"Queued Background Task {guid} has " +
                                $"written a step. {delayLoop}/3"
                        });
                    await db.SaveChangesAsync();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, 
                        "An error occurred writing to the " +
                        $"database. Error: {ex.Message}");
                }

                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
        }

        _logger.LogInformation(
            $"Queued Background Task {guid} is complete. 3/3");
    });

    return RedirectToPage();
}

Recursos adicionalesAdditional resources