Tarefas em segundo plano com serviços hospedados no ASP.NET Core

Por Jeow Li Huan

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

No ASP.NET Core, as tarefas em segundo plano podem ser implementadas como serviços hospedados. Um serviço hospedado é uma classe com lógica de tarefa em segundo plano que implementa a interface IHostedService. Este artigo fornece três exemplos de serviço hospedado:

  • Tarefa em segundo plano que é executada com um temporizador.
  • Serviço hospedado que ativa um serviço com escopo. O serviço com escopo pode usar a DI (injeção de dependência).
  • Tarefas em segundo plano na fila que são executadas sequencialmente.

Modelo de serviço de trabalho

O modelo de Serviço de Trabalho do ASP.NET Core fornece um ponto inicial para escrever aplicativos de serviço de execução prolongada. Um aplicativo criado a partir do modelo do Serviço de Trabalho especifica o SDK de Trabalho em seu arquivo de projeto:

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

Para usar o modelo como base para um aplicativo de serviços hospedados:

  1. Criar um novo projeto.
  2. Selecione Serviço de Trabalho. Selecione Avançar.
  3. Forneça um nome ao projeto no campo Nome do projeto ou aceite o nome do projeto padrão. Selecione Avançar.
  4. Na caixa de diálogo Informações adicionais, escolha uma Estrutura. Selecione Criar.

Pacote

Um aplicativo baseado no modelo do Serviço de Trabalho usa o SDK Microsoft.NET.Sdk.Worker e tem uma referência explícita de pacote para o pacote Microsoft.Extensions.Hosting. Por exemplo, confira o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o SDK Microsoft.NET.Sdk.Web, o pacote Microsoft.Extensions.Hosting é referenciado implicitamente a partir da estrutura compartilhada. Não é necessária uma referência explícita de pacote no arquivo de projeto do aplicativo.

Interface IHostedService

A interface IHostedService define dois métodos para objetos que são gerenciados pelo host:

StartAsync

StartAsync(CancellationToken) contém a lógica para iniciar a tarefa em segundo plano. StartAsync é chamado antes de:

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum outro serviço será iniciado até que StartAsync seja executado até a conclusão.

StopAsync

O token de cancelamento tem um tempo limite padrão de 30 segundos para indicar que o processo de desligamento não deve mais ser normal. Quando for solicitado um cancelamento no token:

  • Todas as demais operações em segundo plano que o aplicativo estiver executando deverão ser anuladas.
  • Todos os métodos chamados em StopAsync deverão retornar imediatamente.

No entanto, as tarefas não são abandonadas após a solicitação de cancelamento – o chamador aguarda a conclusão de todas as tarefas.

Se o aplicativo for desligado inesperadamente (por exemplo, em uma falha do processo do aplicativo), StopAsync não poderá ser chamado. Portanto, os métodos chamados ou operações realizadas em StopAsync talvez não ocorram.

Para estender o tempo limite de desligamento padrão de 30 segundos, defina:

O serviço hospedado é ativado uma única vez na inicialização do aplicativo e desligado normalmente durante o desligamento do aplicativo. Se um erro for gerado durante a execução da tarefa em segundo plano, Dispose deverá ser chamado mesmo se StopAsync não for chamado.

Classe base BackgroundService

BackgroundService é uma classe base para implementar um IHostedService de execução prolongada.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna uma Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum outro serviço será iniciado até que ExecuteAsync se torne assíncrono, como ao chamar await. Evite executar trabalhos longos de inicialização de bloqueio em ExecuteAsync. O host bloqueia em StopAsync(CancellationToken) aguardando a conclusão de ExecuteAsync.

O token de cancelamento é disparado quando IHostedService.StopAsync for chamado. Sua implementação de ExecuteAsync deve ser concluída imediatamente quando o token de cancelamento for acionado para desligar normalmente o serviço. Caso contrário, o serviço será desligado sem êxito no tempo limite de desligamento. Para obter mais informações, confira a seção Interface do IHostedService.

Para obter mais informações, confira o código-fonte BackgroundService.

Tarefas em segundo plano temporizadas

Uma tarefa em segundo plano temporizada usa a classe System.Threading.Timer. O temporizador dispara o método DoWork da tarefa. O temporizador é desabilitado em StopAsync e descartado quando o contêiner de serviço é descartado em 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();
    }
}

O Timer não aguarda a conclusão das execuções anteriores de DoWork, portanto, a abordagem mostrada pode não ser adequada para todos os cenários. Interlocked.Increment é usado para incrementar o contador de execução como uma operação atômica, o que garante que vários threads não atualizem executionCount simultaneamente.

O serviço está registrado em IHostBuilder.ConfigureServices (Program.cs) com o método de extensão AddHostedService:

services.AddHostedService<TimedHostedService>();

Consumindo um serviço com escopo em uma tarefa em segundo plano

Para usar serviços com escopo em um BackgroundService, crie um escopo. Por padrão, nenhum escopo é criado para um serviço hospedado.

O serviço da tarefa em segundo plano com escopo contém a lógica da tarefa em segundo plano. No exemplo a seguir:

  • O serviço é assíncrono. O método DoWork retorna uma Task. Para fins de demonstração, um atraso de dez segundos é aguardado no método DoWork.
  • Um ILogger é injetado no serviço.
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);
        }
    }
}

O serviço hospedado cria um escopo para resolver o serviço da tarefa em segundo plano com escopo para chamar seu método DoWork. DoWork retorna um Task, que é aguardado em 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);
    }
}

Os serviços são registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado está registrado com o método de extensão AddHostedService:

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

Tarefas em segundo plano na fila

Uma fila de tarefas em segundo plano é baseada no .NET 4.x QueueBackgroundWorkItem:

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;
    }
}

No exemplo QueueHostedService a seguir:

  • O método BackgroundProcessing retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são removidas da fila e executadas em BackgroundProcessing.
  • Os itens de trabalho são aguardados antes que o serviço pare em 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);
    }
}

Um serviço MonitorLoop lida com tarefas de enfileiramento para o serviço hospedado sempre que a chave w é selecionada em um dispositivo de entrada:

  • A IBackgroundTaskQueue é injetada no serviço MonitorLoop.
  • O IBackgroundTaskQueue.QueueBackgroundWorkItem é chamado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de execução longa:
    • São executados três atrasos de 5 segundos (Task.Delay).
    • Uma instrução try-catch interceptará OperationCanceledException se a tarefa for cancelada.
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);
        }
    }
}

Os serviços são registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado está registrado com o método de extensão 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 é iniciado em Program.cs:

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

Tarefa em segundo plano assíncrona cronometrada

O código a seguir cria uma tarefa em segundo plano assíncrona cronometrada:

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

Os modelos do Serviço de Trabalho oferecem suporte ao .NET nativo antecipado (AOT) com o --aot sinalizador:

  1. Criar um novo projeto.
  2. Selecione Serviço de Trabalho. Selecione Avançar.
  3. Forneça um nome ao projeto no campo Nome do projeto ou aceite o nome do projeto padrão. Selecione Avançar.
  4. Na caixa de diálogo Informações adicionais:
  5. Escolha uma Estrutura.
  6. Marque a caixa de seleção Habilitar a publicação AOT nativo.
  7. Selecione Criar.

A opção AOT é adicionada <PublishAot>true</PublishAot> ao arquivo do projeto:


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

No ASP.NET Core, as tarefas em segundo plano podem ser implementadas como serviços hospedados. Um serviço hospedado é uma classe com lógica de tarefa em segundo plano que implementa a interface IHostedService. Este artigo fornece três exemplos de serviço hospedado:

  • Tarefa em segundo plano que é executada com um temporizador.
  • Serviço hospedado que ativa um serviço com escopo. O serviço com escopo pode usar a DI (injeção de dependência).
  • Tarefas em segundo plano na fila que são executadas sequencialmente.

Modelo de serviço de trabalho

O modelo de Serviço de Trabalho do ASP.NET Core fornece um ponto inicial para escrever aplicativos de serviço de execução prolongada. Um aplicativo criado a partir do modelo do Serviço de Trabalho especifica o SDK de Trabalho em seu arquivo de projeto:

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

Para usar o modelo como base para um aplicativo de serviços hospedados:

  1. Criar um novo projeto.
  2. Selecione Serviço de Trabalho. Selecione Avançar.
  3. Forneça um nome ao projeto no campo Nome do projeto ou aceite o nome do projeto padrão. Selecione Avançar.
  4. Na caixa de diálogo Informações adicionais, escolha uma Estrutura. Selecione Criar.

Pacote

Um aplicativo baseado no modelo do Serviço de Trabalho usa o SDK Microsoft.NET.Sdk.Worker e tem uma referência explícita de pacote para o pacote Microsoft.Extensions.Hosting. Por exemplo, confira o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o SDK Microsoft.NET.Sdk.Web, o pacote Microsoft.Extensions.Hosting é referenciado implicitamente a partir da estrutura compartilhada. Não é necessária uma referência explícita de pacote no arquivo de projeto do aplicativo.

Interface IHostedService

A interface IHostedService define dois métodos para objetos que são gerenciados pelo host:

StartAsync

StartAsync(CancellationToken) contém a lógica para iniciar a tarefa em segundo plano. StartAsync é chamado antes de:

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum outro serviço será iniciado até que StartAsync seja executado até a conclusão.

StopAsync

O token de cancelamento tem um tempo limite padrão de 30 segundos para indicar que o processo de desligamento não deve mais ser normal. Quando for solicitado um cancelamento no token:

  • Todas as demais operações em segundo plano que o aplicativo estiver executando deverão ser anuladas.
  • Todos os métodos chamados em StopAsync deverão retornar imediatamente.

No entanto, as tarefas não são abandonadas após a solicitação de cancelamento – o chamador aguarda a conclusão de todas as tarefas.

Se o aplicativo for desligado inesperadamente (por exemplo, em uma falha do processo do aplicativo), StopAsync não poderá ser chamado. Portanto, os métodos chamados ou operações realizadas em StopAsync talvez não ocorram.

Para estender o tempo limite de desligamento padrão de 30 segundos, defina:

O serviço hospedado é ativado uma única vez na inicialização do aplicativo e desligado normalmente durante o desligamento do aplicativo. Se um erro for gerado durante a execução da tarefa em segundo plano, Dispose deverá ser chamado mesmo se StopAsync não for chamado.

Classe base BackgroundService

BackgroundService é uma classe base para implementar um IHostedService de execução prolongada.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna uma Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum outro serviço será iniciado até que ExecuteAsync se torne assíncrono, como ao chamar await. Evite executar trabalhos longos de inicialização de bloqueio em ExecuteAsync. O host bloqueia em StopAsync(CancellationToken) aguardando a conclusão de ExecuteAsync.

O token de cancelamento é disparado quando IHostedService.StopAsync for chamado. Sua implementação de ExecuteAsync deve ser concluída imediatamente quando o token de cancelamento for acionado para desligar normalmente o serviço. Caso contrário, o serviço será desligado sem êxito no tempo limite de desligamento. Para obter mais informações, confira a seção Interface do IHostedService.

Para obter mais informações, confira o código-fonte BackgroundService.

Tarefas em segundo plano temporizadas

Uma tarefa em segundo plano temporizada usa a classe System.Threading.Timer. O temporizador dispara o método DoWork da tarefa. O temporizador é desabilitado em StopAsync e descartado quando o contêiner de serviço é descartado em 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();
    }
}

O Timer não aguarda a conclusão das execuções anteriores de DoWork, portanto, a abordagem mostrada pode não ser adequada para todos os cenários. Interlocked.Increment é usado para incrementar o contador de execução como uma operação atômica, o que garante que vários threads não atualizem executionCount simultaneamente.

O serviço está registrado em IHostBuilder.ConfigureServices (Program.cs) com o método de extensão AddHostedService:

services.AddHostedService<TimedHostedService>();

Consumindo um serviço com escopo em uma tarefa em segundo plano

Para usar serviços com escopo em um BackgroundService, crie um escopo. Por padrão, nenhum escopo é criado para um serviço hospedado.

O serviço da tarefa em segundo plano com escopo contém a lógica da tarefa em segundo plano. No exemplo a seguir:

  • O serviço é assíncrono. O método DoWork retorna uma Task. Para fins de demonstração, um atraso de dez segundos é aguardado no método DoWork.
  • Um ILogger é injetado no serviço.
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);
        }
    }
}

O serviço hospedado cria um escopo para resolver o serviço da tarefa em segundo plano com escopo para chamar seu método DoWork. DoWork retorna um Task, que é aguardado em 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);
    }
}

Os serviços são registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado está registrado com o método de extensão AddHostedService:

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

Tarefas em segundo plano na fila

Uma fila de tarefas em segundo plano é baseada no .NET 4.x QueueBackgroundWorkItem:

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;
    }
}

No exemplo QueueHostedService a seguir:

  • O método BackgroundProcessing retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são removidas da fila e executadas em BackgroundProcessing.
  • Os itens de trabalho são aguardados antes que o serviço pare em 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);
    }
}

Um serviço MonitorLoop lida com tarefas de enfileiramento para o serviço hospedado sempre que a chave w é selecionada em um dispositivo de entrada:

  • A IBackgroundTaskQueue é injetada no serviço MonitorLoop.
  • O IBackgroundTaskQueue.QueueBackgroundWorkItem é chamado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de execução longa:
    • São executados três atrasos de 5 segundos (Task.Delay).
    • Uma instrução try-catch interceptará OperationCanceledException se a tarefa for cancelada.
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);
        }
    }
}

Os serviços são registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado está registrado com o método de extensão 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 é iniciado em Program.cs:

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

Tarefa em segundo plano assíncrona cronometrada

O código a seguir cria uma tarefa em segundo plano assíncrona cronometrada:

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 adicionais

No ASP.NET Core, as tarefas em segundo plano podem ser implementadas como serviços hospedados. Um serviço hospedado é uma classe com lógica de tarefa em segundo plano que implementa a interface IHostedService. Este artigo fornece três exemplos de serviço hospedado:

  • Tarefa em segundo plano que é executada com um temporizador.
  • Serviço hospedado que ativa um serviço com escopo. O serviço com escopo pode usar a DI (injeção de dependência).
  • Tarefas em segundo plano na fila que são executadas sequencialmente.

Exibir ou baixar código de exemplo (como baixar)

Modelo de serviço de trabalho

O modelo de Serviço de Trabalho do ASP.NET Core fornece um ponto inicial para escrever aplicativos de serviço de execução prolongada. Um aplicativo criado a partir do modelo do Serviço de Trabalho especifica o SDK de Trabalho em seu arquivo de projeto:

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

Para usar o modelo como base para um aplicativo de serviços hospedados:

  1. Criar um novo projeto.
  2. Selecione Serviço de Trabalho. Selecione Avançar.
  3. Forneça um nome ao projeto no campo Nome do projeto ou aceite o nome do projeto padrão. Selecione Criar.
  4. Na caixa de diálogo Criar um serviço de trabalho, selecione Criar.

Pacote

Um aplicativo baseado no modelo do Serviço de Trabalho usa o SDK Microsoft.NET.Sdk.Worker e tem uma referência explícita de pacote para o pacote Microsoft.Extensions.Hosting. Por exemplo, confira o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o SDK Microsoft.NET.Sdk.Web, o pacote Microsoft.Extensions.Hosting é referenciado implicitamente a partir da estrutura compartilhada. Não é necessária uma referência explícita de pacote no arquivo de projeto do aplicativo.

Interface IHostedService

A interface IHostedService define dois métodos para objetos que são gerenciados pelo host:

StartAsync

StartAsync contém a lógica para iniciar a tarefa em segundo plano. StartAsync é chamado antes de:

O comportamento padrão pode ser alterado para que o serviço hospedado StartAsync seja executado depois de o pipeline do aplicativo ter sido configurado e ApplicationStarted seja chamado. Para alterar o comportamento padrão, adicione o serviço hospedado (VideosWatcher, no exemplo a seguir) depois de chamar 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

O token de cancelamento tem um tempo limite padrão de cinco segundos para indicar que o processo de desligamento não deve mais ser normal. Quando for solicitado um cancelamento no token:

  • Todas as demais operações em segundo plano que o aplicativo estiver executando deverão ser anuladas.
  • Todos os métodos chamados em StopAsync deverão retornar imediatamente.

No entanto, as tarefas não são abandonadas após a solicitação de cancelamento – o chamador aguarda a conclusão de todas as tarefas.

Se o aplicativo for desligado inesperadamente (por exemplo, em uma falha do processo do aplicativo), StopAsync não poderá ser chamado. Portanto, os métodos chamados ou operações realizadas em StopAsync talvez não ocorram.

Para estender o tempo limite de desligamento padrão de cinco segundos, defina:

O serviço hospedado é ativado uma única vez na inicialização do aplicativo e desligado normalmente durante o desligamento do aplicativo. Se um erro for gerado durante a execução da tarefa em segundo plano, Dispose deverá ser chamado mesmo se StopAsync não for chamado.

Classe base BackgroundService

BackgroundService é uma classe base para implementar um IHostedService de execução prolongada.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna uma Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum outro serviço será iniciado até que ExecuteAsync se torne assíncrono, como ao chamar await. Evite executar trabalhos longos de inicialização de bloqueio em ExecuteAsync. O host bloqueia em StopAsync(CancellationToken) aguardando a conclusão de ExecuteAsync.

O token de cancelamento é disparado quando IHostedService.StopAsync for chamado. Sua implementação de ExecuteAsync deve ser concluída imediatamente quando o token de cancelamento for acionado para desligar normalmente o serviço. Caso contrário, o serviço será desligado sem êxito no tempo limite de desligamento. Para obter mais informações, confira a seção Interface do IHostedService.

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum outro serviço será iniciado até que StartAsync seja executado até a conclusão. Tarefas de execução prolongada devem ser colocadas em ExecuteAsync. Para obter mais informações, confira a fonte de BackgroundService.

Tarefas em segundo plano temporizadas

Uma tarefa em segundo plano temporizada usa a classe System.Threading.Timer. O temporizador dispara o método DoWork da tarefa. O temporizador é desabilitado em StopAsync e descartado quando o contêiner de serviço é descartado em 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();
    }
}

O Timer não aguarda a conclusão das execuções anteriores de DoWork, portanto, a abordagem mostrada pode não ser adequada para todos os cenários. Interlocked.Increment é usado para incrementar o contador de execução como uma operação atômica, o que garante que vários threads não atualizem executionCount simultaneamente.

O serviço está registrado em IHostBuilder.ConfigureServices (Program.cs) com o método de extensão AddHostedService:

services.AddHostedService<TimedHostedService>();

Consumindo um serviço com escopo em uma tarefa em segundo plano

Para usar serviços com escopo em um BackgroundService, crie um escopo. Por padrão, nenhum escopo é criado para um serviço hospedado.

O serviço da tarefa em segundo plano com escopo contém a lógica da tarefa em segundo plano. No exemplo a seguir:

  • O serviço é assíncrono. O método DoWork retorna uma Task. Para fins de demonstração, um atraso de dez segundos é aguardado no método DoWork.
  • Um ILogger é injetado no serviço.
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);
        }
    }
}

O serviço hospedado cria um escopo para resolver o serviço da tarefa em segundo plano com escopo para chamar seu método DoWork. DoWork retorna um Task, que é aguardado em 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);
    }
}

Os serviços são registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado está registrado com o método de extensão AddHostedService:

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

Tarefas em segundo plano na fila

Uma fila de tarefas em segundo plano é baseada no .NET 4.x QueueBackgroundWorkItem:

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;
    }
}

No exemplo QueueHostedService a seguir:

  • O método BackgroundProcessing retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são removidas da fila e executadas em BackgroundProcessing.
  • Os itens de trabalho são aguardados antes que o serviço pare em 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);
    }
}

Um serviço MonitorLoop lida com tarefas de enfileiramento para o serviço hospedado sempre que a chave w é selecionada em um dispositivo de entrada:

  • A IBackgroundTaskQueue é injetada no serviço MonitorLoop.
  • O IBackgroundTaskQueue.QueueBackgroundWorkItem é chamado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de execução longa:
    • São executados três atrasos de 5 segundos (Task.Delay).
    • Uma instrução try-catch interceptará OperationCanceledException se a tarefa for cancelada.
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);
        }
    }
}

Os serviços são registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado está registrado com o método de extensão 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 é iniciado em Program.Main:

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

Recursos adicionais