Compartir vía


Tareas en segundo plano con servicios hospedados en ASP.NET Core

Por Jeow Li Huan

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados. Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService. En este artículo se incluyen tres ejemplos de servicio hospedado:

  • Una tarea en segundo plano que se ejecuta según un temporizador.
  • Un servicio hospedado que activa un servicio con ámbito. El servicio con ámbito puede usar la inserción de dependencias (DI).
  • Tareas en segundo plano en cola que se ejecutan en secuencia.

Plantilla Worker Service

La plantilla Worker Service de ASP.NET Core sirve de punto de partida para escribir aplicaciones de servicio de larga duración. Una aplicación creada a partir de la plantilla Worker Service especifica el SDK de trabajo en su archivo del proyecto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Para usar la plantilla como base de una aplicación de servicios hospedados:

  1. Cree un nuevo proyecto.
  2. Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
  3. Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Siguiente.
  4. En el cuadro de diálogo Información adicional, elija Marco de trabajo. Seleccione Crear.

Paquete

Una aplicación basada en la plantilla Worker Service usa el SDK de Microsoft.NET.Sdk.Worker y tiene una referencia de paquete explícita al paquete Microsoft.Extensions.Hosting. Por ejemplo, consulte el archivo del proyecto de la aplicación de ejemplo (BackgroundTasksSample.csproj).

En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web, desde el marco compartido se hace una referencia implícita al paquete Microsoft.Extensions.Hosting. No se requiere una referencia de paquete explícita en el archivo del proyecto de la aplicación.

Interfaz IHostedService

La interfaz IHostedService define dos métodos para los objetos administrados por el host:

StartAsync

StartAsync(CancellationToken) contiene la lógica para iniciar la tarea en segundo plano. Se llama a StartAsyncantes de que:

StartAsync debe limitarse a tareas de ejecución corta porque los servicios hospedados se ejecutan secuencialmente, y no se inician más servicios hasta que StartAsync se ejecuta hasta su finalización.

StopAsync

El token de cancelación tiene un tiempo de espera predeterminado de 30 segundos para indicar que el proceso de cierre ya no debería ser estable. Cuando se solicita la cancelación en el token:

  • Se deben anular las operaciones restantes en segundo plano que realiza la aplicación.
  • Los métodos llamados en StopAsync deberían devolver contenido al momento.

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

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. Por lo tanto, los métodos llamados o las operaciones llevadas a cabo en StopAsync podrían no producirse.

Para ampliar el tiempo de espera predeterminado de apagado de 30 segundos, establezca:

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

Clase base BackgroundService

BackgroundService es una clase base para implementar un IHostedService de larga duración.

Se llama a ExecuteAsync(CancellationToken) para ejecutar el servicio en segundo plano. La implementación devuelve Task, que representa toda la duración del servicio en segundo plano. No se inicia ningún servicio hasta que ExecuteAsync se convierte en asincrónico, mediante una llamada a await. Evite realizar un trabajo de inicialización de bloqueo prolongado en ExecuteAsync. El host se bloquea en StopAsync(CancellationToken) a la espera de que ExecuteAsync se complete.

El token de cancelación se desencadena cuando se llama a IHostedService.StopAsync. La implementación de ExecuteAsync debe finalizar rápidamente cuando se active el token de cancelación para cerrar correctamente el servicio. De lo contrario, el servicio se cierra de manera abrupta durante el tiempo de expiración del cierre. Para más información, consulte la sección sobre la interfaz IHostedService.

Para obtener más información, vea el código fuente de BackgroundService.

Tareas en segundo plano temporizadas

Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer. El temporizador activa el método DoWork de la tarea. El temporizador está deshabilitado en StopAsync y se desecha cuando el contenedor de servicios se elimina en Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

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

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

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

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

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

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

        return Task.CompletedTask;
    }

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

Timer no espera a que finalicen las ejecuciones anteriores de DoWork, por lo que es posible que el enfoque mostrado no sea adecuado para todos los escenarios. Interlocked.Increment se usa para incrementar el contador de ejecución como una operación atómica, lo que garantiza que varios subprocesos no actualicen executionCount simultáneamente.

El servicio se registra en IHostBuilder.ConfigureServices (Program.cs) con el método de extensión AddHostedService:

services.AddHostedService<TimedHostedService>();

Consumir un servicio con ámbito en una tarea en segundo plano

Para usar servicios con ámbito dentro de un BackgroundService, cree un ámbito. No se crean ámbitos de forma predeterminada para los servicios hospedados.

El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano. En el ejemplo siguiente:

  • El servicio es asincrónico. El método DoWork devuelve un objeto Task. Para fines de demostración, se espera un retraso de diez segundos en el método DoWork.
  • ILogger se inserta en el servicio.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

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

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

El servicio hospedado crea un ámbito con el fin de resolver el servicio de tareas en segundo plano con ámbito para llamar a su método DoWork. DoWork devuelve Task, que se espera en ExecuteAsync:

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

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

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

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

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

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

        await base.StopAsync(stoppingToken);
    }
}

Los servicios se registran en IHostBuilder.ConfigureServices (Program.cs). El servicio hospedado se registra en con el método de extensión AddHostedService:

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

Tareas en segundo plano en cola

Una cola de tareas en segundo plano se basa en QueueBackgroundWorkItem de .NET 4.x:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

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

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

En el ejemplo QueueHostedService siguiente:

  • El método BackgroundProcessing devuelve Task, que se espera en ExecuteAsync.
  • Las tareas en segundo plano que están en cola se quitan de ella y se ejecutan en BackgroundProcessing.
  • Se esperan elementos de trabajo antes de que el servicio se detenga en StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

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

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un servicio MonitorLoop controla las tareas de puesta en cola para el servicio hospedado cada vez que se selecciona la tecla w en un dispositivo de entrada:

  • IBackgroundTaskQueue se inserta en el servicio MonitorLoop.
  • Se llama a IBackgroundTaskQueue.QueueBackgroundWorkItem para poner en cola el elemento de trabajo.
  • El elemento de trabajo simula una tarea en segundo plano de larga duración:
    • Se ejecutan tres retrasos de 5 segundos (Task.Delay).
    • Una instrucción try-catch captura OperationCanceledException si se cancela la tarea.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue,
        ILogger<MonitorLoop> logger,
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " 
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Los servicios se registran en IHostBuilder.ConfigureServices (Program.cs). El servicio hospedado se registra en con el método de extensión AddHostedService:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop se inicia en Program.cs:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Tarea en segundo plano asincrónica temporizada

El código siguiente crea una tarea en segundo plano asincrónica temporizada:

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
    }
}

AOT nativo

Las plantillas de servicio de trabajo admiten .NET ahead-of-time (AOT) nativa con la marca --aot:

  1. Cree un nuevo proyecto.
  2. Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
  3. Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Next (Siguiente).
  4. En el cuadro de diálogo Información adicional:
  5. Seleccione un marco.
  6. Active la casilla Habilitar publicación nativa mediante AOT.
  7. Seleccione Crear.

La opción AOT agrega <PublishAot>true</PublishAot> al archivo del proyecto:


<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
+   <PublishAot>true</PublishAot>
    <UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" />
  </ItemGroup>
</Project>

Recursos adicionales

En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados. Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService. En este artículo se incluyen tres ejemplos de servicio hospedado:

  • Una tarea en segundo plano que se ejecuta según un temporizador.
  • Un servicio hospedado que activa un servicio con ámbito. El servicio con ámbito puede usar la inserción de dependencias (DI).
  • Tareas en segundo plano en cola que se ejecutan en secuencia.

Plantilla Worker Service

La plantilla Worker Service de ASP.NET Core sirve de punto de partida para escribir aplicaciones de servicio de larga duración. Una aplicación creada a partir de la plantilla Worker Service especifica el SDK de trabajo en su archivo del proyecto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Para usar la plantilla como base de una aplicación de servicios hospedados:

  1. Cree un nuevo proyecto.
  2. Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
  3. Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Siguiente.
  4. En el cuadro de diálogo Información adicional, elija Marco de trabajo. Seleccione Crear.

Paquete

Una aplicación basada en la plantilla Worker Service usa el SDK de Microsoft.NET.Sdk.Worker y tiene una referencia de paquete explícita al paquete Microsoft.Extensions.Hosting. Por ejemplo, consulte el archivo del proyecto de la aplicación de ejemplo (BackgroundTasksSample.csproj).

En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web, desde el marco compartido se hace una referencia implícita al paquete Microsoft.Extensions.Hosting. No se requiere una referencia de paquete explícita en el archivo del proyecto de la aplicación.

Interfaz IHostedService

La interfaz IHostedService define dos métodos para los objetos administrados por el host:

StartAsync

StartAsync(CancellationToken) contiene la lógica para iniciar la tarea en segundo plano. Se llama a StartAsyncantes de que:

StartAsync debe limitarse a tareas de ejecución corta porque los servicios hospedados se ejecutan secuencialmente, y no se inician más servicios hasta que StartAsync se ejecuta hasta su finalización.

StopAsync

El token de cancelación tiene un tiempo de espera predeterminado de 30 segundos para indicar que el proceso de cierre ya no debería ser estable. Cuando se solicita la cancelación en el token:

  • Se deben anular las operaciones restantes en segundo plano que realiza la aplicación.
  • Los métodos llamados en StopAsync deberían devolver contenido al momento.

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

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. Por lo tanto, los métodos llamados o las operaciones llevadas a cabo en StopAsync podrían no producirse.

Para ampliar el tiempo de espera predeterminado de apagado de 30 segundos, establezca:

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

Clase base BackgroundService

BackgroundService es una clase base para implementar un IHostedService de larga duración.

Se llama a ExecuteAsync(CancellationToken) para ejecutar el servicio en segundo plano. La implementación devuelve Task, que representa toda la duración del servicio en segundo plano. No se inicia ningún servicio hasta que ExecuteAsync se convierte en asincrónico, mediante una llamada a await. Evite realizar un trabajo de inicialización de bloqueo prolongado en ExecuteAsync. El host se bloquea en StopAsync(CancellationToken) a la espera de que ExecuteAsync se complete.

El token de cancelación se desencadena cuando se llama a IHostedService.StopAsync. La implementación de ExecuteAsync debe finalizar rápidamente cuando se active el token de cancelación para cerrar correctamente el servicio. De lo contrario, el servicio se cierra de manera abrupta durante el tiempo de expiración del cierre. Para más información, consulte la sección sobre la interfaz IHostedService.

Para obtener más información, vea el código fuente de BackgroundService.

Tareas en segundo plano temporizadas

Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer. El temporizador activa el método DoWork de la tarea. El temporizador está deshabilitado en StopAsync y se desecha cuando el contenedor de servicios se elimina en Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

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

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

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

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

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

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

        return Task.CompletedTask;
    }

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

Timer no espera a que finalicen las ejecuciones anteriores de DoWork, por lo que es posible que el enfoque mostrado no sea adecuado para todos los escenarios. Interlocked.Increment se usa para incrementar el contador de ejecución como una operación atómica, lo que garantiza que varios subprocesos no actualicen executionCount simultáneamente.

El servicio se registra en IHostBuilder.ConfigureServices (Program.cs) con el método de extensión AddHostedService:

services.AddHostedService<TimedHostedService>();

Consumir un servicio con ámbito en una tarea en segundo plano

Para usar servicios con ámbito dentro de un BackgroundService, cree un ámbito. No se crean ámbitos de forma predeterminada para los servicios hospedados.

El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano. En el ejemplo siguiente:

  • El servicio es asincrónico. El método DoWork devuelve un objeto Task. Para fines de demostración, se espera un retraso de diez segundos en el método DoWork.
  • ILogger se inserta en el servicio.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

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

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

El servicio hospedado crea un ámbito con el fin de resolver el servicio de tareas en segundo plano con ámbito para llamar a su método DoWork. DoWork devuelve Task, que se espera en ExecuteAsync:

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

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

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

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

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

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

        await base.StopAsync(stoppingToken);
    }
}

Los servicios se registran en IHostBuilder.ConfigureServices (Program.cs). El servicio hospedado se registra en con el método de extensión AddHostedService:

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

Tareas en segundo plano en cola

Una cola de tareas en segundo plano se basa en QueueBackgroundWorkItem de .NET 4.x:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

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

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

En el ejemplo QueueHostedService siguiente:

  • El método BackgroundProcessing devuelve Task, que se espera en ExecuteAsync.
  • Las tareas en segundo plano que están en cola se quitan de ella y se ejecutan en BackgroundProcessing.
  • Se esperan elementos de trabajo antes de que el servicio se detenga en StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

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

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un servicio MonitorLoop controla las tareas de puesta en cola para el servicio hospedado cada vez que se selecciona la tecla w en un dispositivo de entrada:

  • IBackgroundTaskQueue se inserta en el servicio MonitorLoop.
  • Se llama a IBackgroundTaskQueue.QueueBackgroundWorkItem para poner en cola el elemento de trabajo.
  • El elemento de trabajo simula una tarea en segundo plano de larga duración:
    • Se ejecutan tres retrasos de 5 segundos (Task.Delay).
    • Una instrucción try-catch captura OperationCanceledException si se cancela la tarea.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue,
        ILogger<MonitorLoop> logger,
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " 
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Los servicios se registran en IHostBuilder.ConfigureServices (Program.cs). El servicio hospedado se registra en con el método de extensión AddHostedService:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop se inicia en Program.cs:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Tarea en segundo plano asincrónica temporizada

El código siguiente crea una tarea en segundo plano asincrónica temporizada:

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
    }
}

Recursos adicionales

En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados. Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService. En este artículo se incluyen tres ejemplos de servicio hospedado:

  • Una tarea en segundo plano que se ejecuta según un temporizador.
  • Un servicio hospedado que activa un servicio con ámbito. El servicio con ámbito puede usar la inserción de dependencias (DI).
  • Tareas en segundo plano en cola que se ejecutan en secuencia.

Vea o descargue el código de ejemplo (cómo descargarlo)

Plantilla Worker Service

La plantilla Worker Service de ASP.NET Core sirve de punto de partida para escribir aplicaciones de servicio de larga duración. Una aplicación creada a partir de la plantilla Worker Service especifica el SDK de trabajo en su archivo del proyecto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Para usar la plantilla como base de una aplicación de servicios hospedados:

  1. Cree un nuevo proyecto.
  2. Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
  3. Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Crear.
  4. En el cuadro de diálogo Crear un servicio de Worker, seleccione Crear.

Paquete

Una aplicación basada en la plantilla Worker Service usa el SDK de Microsoft.NET.Sdk.Worker y tiene una referencia de paquete explícita al paquete Microsoft.Extensions.Hosting. Por ejemplo, consulte el archivo del proyecto de la aplicación de ejemplo (BackgroundTasksSample.csproj).

En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web, desde el marco compartido se hace una referencia implícita al paquete Microsoft.Extensions.Hosting. No se requiere una referencia de paquete explícita en el archivo del proyecto de la aplicación.

Interfaz IHostedService

La interfaz IHostedService define dos métodos para los objetos administrados por el host:

StartAsync

StartAsync contiene la lógica para iniciar la tarea en segundo plano. Se llama a StartAsyncantes de que:

El comportamiento predeterminado se puede cambiar para que el StartAsync del servicio hospedado se ejecute después de que se haya configurado la canalización de la aplicación y se haya llamado a ApplicationStarted. Para cambiar el comportamiento predeterminado, agregue el servicio hospedado (VideosWatcher en el ejemplo siguiente) después de llamar a ConfigureWebHostDefaults:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            .ConfigureServices(services =>
            {
                services.AddHostedService<VideosWatcher>();
            });
}

StopAsync

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. Cuando se solicita la cancelación en el token:

  • Se deben anular las operaciones restantes en segundo plano que realiza la aplicación.
  • Los métodos llamados en StopAsync deberían devolver contenido al momento.

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

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. Por lo tanto, los métodos llamados o las operaciones llevadas a cabo en StopAsync podrían no producirse.

Para ampliar el tiempo de espera predeterminado de apagado de 5 segundos, establezca:

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

Clase base BackgroundService

BackgroundService es una clase base para implementar un IHostedService de larga duración.

Se llama a ExecuteAsync(CancellationToken) para ejecutar el servicio en segundo plano. La implementación devuelve Task, que representa toda la duración del servicio en segundo plano. No se inicia ningún servicio hasta que ExecuteAsync se convierte en asincrónico, mediante una llamada a await. Evite realizar un trabajo de inicialización de bloqueo prolongado en ExecuteAsync. El host se bloquea en StopAsync(CancellationToken) a la espera de que ExecuteAsync se complete.

El token de cancelación se desencadena cuando se llama a IHostedService.StopAsync. La implementación de ExecuteAsync debe finalizar rápidamente cuando se active el token de cancelación para cerrar correctamente el servicio. De lo contrario, el servicio se cierra de manera abrupta durante el tiempo de expiración del cierre. Para más información, consulte la sección sobre la interfaz IHostedService.

StartAsync debe limitarse a tareas de ejecución corta porque los servicios hospedados se ejecutan secuencialmente, y no se inician más servicios hasta que StartAsync se ejecuta hasta su finalización. Las tareas de larga duración deben colocarse en ExecuteAsync. Para obtener más información, vea el origen para BackgroundService.

Tareas en segundo plano temporizadas

Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer. El temporizador activa el método DoWork de la tarea. El temporizador está deshabilitado en StopAsync y se desecha cuando el contenedor de servicios se elimina en Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer _timer;

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

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

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

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

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

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

        return Task.CompletedTask;
    }

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

Timer no espera a que finalicen las ejecuciones anteriores de DoWork, por lo que es posible que el enfoque mostrado no sea adecuado para todos los escenarios. Interlocked.Increment se usa para incrementar el contador de ejecución como una operación atómica, lo que garantiza que varios subprocesos no actualicen executionCount simultáneamente.

El servicio se registra en IHostBuilder.ConfigureServices (Program.cs) con el método de extensión AddHostedService:

services.AddHostedService<TimedHostedService>();

Consumir un servicio con ámbito en una tarea en segundo plano

Para usar servicios con ámbito dentro de un BackgroundService, cree un ámbito. No se crean ámbitos de forma predeterminada para los servicios hospedados.

El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano. En el ejemplo siguiente:

  • El servicio es asincrónico. El método DoWork devuelve un objeto Task. Para fines de demostración, se espera un retraso de diez segundos en el método DoWork.
  • ILogger se inserta en el servicio.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

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

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

El servicio hospedado crea un ámbito con el fin de resolver el servicio de tareas en segundo plano con ámbito para llamar a su método DoWork. DoWork devuelve Task, que se espera en ExecuteAsync:

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

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

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

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

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

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

        await base.StopAsync(stoppingToken);
    }
}

Los servicios se registran en IHostBuilder.ConfigureServices (Program.cs). El servicio hospedado se registra en con el método de extensión AddHostedService:

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

Tareas en segundo plano en cola

Una cola de tareas en segundo plano se basa en QueueBackgroundWorkItem de .NET 4.x:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

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

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

En el ejemplo QueueHostedService siguiente:

  • El método BackgroundProcessing devuelve Task, que se espera en ExecuteAsync.
  • Las tareas en segundo plano que están en cola se quitan de ella y se ejecutan en BackgroundProcessing.
  • Se esperan elementos de trabajo antes de que el servicio se detenga en StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

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

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un servicio MonitorLoop controla las tareas de puesta en cola para el servicio hospedado cada vez que se selecciona la tecla w en un dispositivo de entrada:

  • IBackgroundTaskQueue se inserta en el servicio MonitorLoop.
  • Se llama a IBackgroundTaskQueue.QueueBackgroundWorkItem para poner en cola el elemento de trabajo.
  • El elemento de trabajo simula una tarea en segundo plano de larga duración:
    • Se ejecutan tres retrasos de 5 segundos (Task.Delay).
    • Una instrucción try-catch captura OperationCanceledException si se cancela la tarea.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue, 
        ILogger<MonitorLoop> logger, 
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Los servicios se registran en IHostBuilder.ConfigureServices (Program.cs). El servicio hospedado se registra en con el método de extensión AddHostedService:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx => {
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop se inicia en Program.Main:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Recursos adicionales