Partilhar via


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

Por Jeow Li Huan

Note

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.

Warning

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 10 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 IHostedService interface. Este artigo fornece três exemplos de serviço hospedado:

Modelo de Serviço do Trabalhador

O modelo ASP.NET Core Worker Service fornece um ponto de partida para escrever aplicativos de serviço de longa execução. Um aplicativo criado a partir do modelo Serviço de Trabalho especifica o SDK do Trabalhador 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. Crie um novo projeto.
  2. Selecione Serviço de Trabalhador. Selecione Avançar.
  3. Forneça um nome de 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.

Package

Uma aplicação baseada no modelo de Serviço de Trabalhador utiliza o Microsoft.NET.Sdk.Worker SDK e tem uma referência explícita ao pacote Microsoft.Extensions.Hosting. Por exemplo, consulte o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o Microsoft.NET.Sdk.Web SDK, 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 IHostedService interface 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:

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum outro serviço é iniciado até StartAsync ser concluído.

StopAsync

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

  • Todas as operações em segundo plano restantes que o aplicativo está executando devem ser anuladas.
  • Os métodos chamados em StopAsync devem retornar imediatamente.

No entanto, as tarefas não são abandonadas após o cancelamento ser solicitado — o chamador aguarda que todas as tarefas sejam concluídas.

Se a aplicação encerrar inesperadamente (por exemplo, se o processo da aplicação falhar), StopAsync pode não ser chamado. Portanto, quaisquer métodos chamados ou operações realizadas em StopAsync podem não ocorrer.

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

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

Classe base BackgroundService

BackgroundService é uma classe base para implementar um IHostedService de longa duração.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna um Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum outro serviço é iniciado até que o ExecuteAsync se torne assíncrono, como chamando await. Evite realizar trabalho de inicialização longo e bloqueante no ExecuteAsync. O host bloqueia em StopAsync(CancellationToken) aguardando ExecuteAsync para concluir.

O token de cancelamento é acionado quando IHostedService.StopAsync é chamado. A sua implementação de ExecuteAsync deve terminar imediatamente quando o token de cancelamento for ativado, a fim de desligar o serviço graciosamente. Caso contrário, o serviço desliga-se ingraciosamente no tempo limite de encerramento. Para obter mais informações, consulte a seção Interface IHostedService .

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

Tarefas cronometradas em segundo plano

Uma tarefa em segundo plano cronometrada usa a classe System.Threading.Timer . O temporizador aciona o método DoWork da tarefa. O temporizador é desativado em StopAsync e descartado quando o contentor 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 espera que as execuções de DoWork anteriores terminem, 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 sejam atualizados executionCount simultaneamente.

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

services.AddHostedService<TimedHostedService>();

Consumindo um serviço sob escopo numa tarefa em segundo plano

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

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

  • O serviço é assíncrono. O DoWork método retorna um Taskarquivo . Para fins de demonstração, espera-se um atraso de dez segundos no DoWork método.
  • 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 com escopo da tarefa em segundo plano, com o objetivo de chamar o 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 estão registados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registado com o método AddHostedService de extensão:

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

Tarefas em segundo plano enfileiradas

Uma fila de tarefas em segundo plano é baseada no .NET Framework 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 seguinte:

  • O BackgroundProcessing método retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são retiradas da fila e executadas no BackgroundProcessing.
  • Os itens de trabalho estão à espera de serem completados antes que o serviço pare no 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 MonitorLoop serviço lida com tarefas de enfileiramento para o serviço hospedado sempre que a w chave é selecionada em um dispositivo de entrada:

  • O IBackgroundTaskQueue é injetado no serviço MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem é invocado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de longa duração:
    • Três atrasos de 5 segundos são executados (Task.Delay).
    • Uma try-catch instrução interceta 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 estão registados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registado com o método AddHostedService de extensão:

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 cronometrada assíncrona

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

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.
        await DoWork();

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

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

    private async Task DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        // Simulate work
        await Task.Delay(TimeSpan.FromSeconds(2));

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

AOT nativo

Os modelos do Serviço de Trabalho oferecem suporte à execução nativa do .NET ahead-of-time (AOT) com a flag --aot:

  1. Crie um novo projeto.
  2. Selecione Serviço de Trabalhador. Selecione Avançar.
  3. Forneça um nome de 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 Ativar publicação AOT nativa .
  7. Selecione Criar.

A opção AOT adiciona <PublishAot>true</PublishAot> ao arquivo de 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 IHostedService interface. Este artigo fornece três exemplos de serviço hospedado:

Modelo de Serviço do Trabalhador

O modelo ASP.NET Core Worker Service fornece um ponto de partida para escrever aplicativos de serviço de longa execução. Um aplicativo criado a partir do modelo Serviço de Trabalho especifica o SDK do Trabalhador 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. Crie um novo projeto.
  2. Selecione Serviço de Trabalhador. Selecione Avançar.
  3. Forneça um nome de 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.

Package

Uma aplicação baseada no modelo de Serviço de Trabalhador utiliza o Microsoft.NET.Sdk.Worker SDK e tem uma referência explícita ao pacote Microsoft.Extensions.Hosting. Por exemplo, consulte o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o Microsoft.NET.Sdk.Web SDK, 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 IHostedService interface 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:

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum outro serviço é iniciado até StartAsync ser concluído.

StopAsync

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

  • Todas as operações em segundo plano restantes que o aplicativo está executando devem ser anuladas.
  • Os métodos chamados em StopAsync devem retornar imediatamente.

No entanto, as tarefas não são abandonadas após o cancelamento ser solicitado — o chamador aguarda que todas as tarefas sejam concluídas.

Se a aplicação encerrar inesperadamente (por exemplo, se o processo da aplicação falhar), StopAsync pode não ser chamado. Portanto, quaisquer métodos chamados ou operações realizadas em StopAsync podem não ocorrer.

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

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

Classe base BackgroundService

BackgroundService é uma classe base para implementar um IHostedService de longa duração.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna um Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum outro serviço é iniciado até que o ExecuteAsync se torne assíncrono, como chamando await. Evite realizar trabalho de inicialização longo e bloqueante no ExecuteAsync. O host bloqueia em StopAsync(CancellationToken) aguardando ExecuteAsync para concluir.

O token de cancelamento é acionado quando IHostedService.StopAsync é chamado. A sua implementação de ExecuteAsync deve terminar imediatamente quando o token de cancelamento for ativado, a fim de desligar o serviço graciosamente. Caso contrário, o serviço desliga-se ingraciosamente no tempo limite de encerramento. Para obter mais informações, consulte a seção Interface IHostedService .

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

Tarefas cronometradas em segundo plano

Uma tarefa em segundo plano cronometrada usa a classe System.Threading.Timer . O temporizador aciona o método DoWork da tarefa. O temporizador é desativado em StopAsync e descartado quando o contentor 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 espera que as execuções de DoWork anteriores terminem, 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 sejam atualizados executionCount simultaneamente.

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

services.AddHostedService<TimedHostedService>();

Consumindo um serviço sob escopo numa tarefa em segundo plano

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

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

  • O serviço é assíncrono. O DoWork método retorna um Taskarquivo . Para fins de demonstração, espera-se um atraso de dez segundos no DoWork método.
  • 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 com escopo da tarefa em segundo plano, com o objetivo de chamar o 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 estão registados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registado com o método AddHostedService de extensão:

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

Tarefas em segundo plano enfileiradas

Uma fila de tarefas em segundo plano é baseada no .NET Framework 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 seguinte:

  • O BackgroundProcessing método retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são retiradas da fila e executadas no BackgroundProcessing.
  • Os itens de trabalho estão à espera de serem completados antes que o serviço pare no 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 MonitorLoop serviço lida com tarefas de enfileiramento para o serviço hospedado sempre que a w chave é selecionada em um dispositivo de entrada:

  • O IBackgroundTaskQueue é injetado no serviço MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem é invocado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de longa duração:
    • Três atrasos de 5 segundos são executados (Task.Delay).
    • Uma try-catch instrução interceta 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 estão registados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registado com o método AddHostedService de extensão:

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 cronometrada assíncrona

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

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.
        await DoWork();

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

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

    private async Task DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        // Simulate work
        await Task.Delay(TimeSpan.FromSeconds(2));

        _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 IHostedService interface. Este artigo fornece três exemplos de serviço hospedado:

Visualizar ou descarregar amostra de código (como descarregar)

Modelo de Serviço do Trabalhador

O modelo ASP.NET Core Worker Service fornece um ponto de partida para escrever aplicativos de serviço de longa execução. Um aplicativo criado a partir do modelo Serviço de Trabalho especifica o SDK do Trabalhador 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. Crie um novo projeto.
  2. Selecione Serviço de Trabalhador. Selecione Avançar.
  3. Forneça um nome de projeto no campo Nome do projeto ou aceite o nome do projeto padrão. Selecione Criar.
  4. Na caixa de diálogo Criar um novo serviço de trabalho, selecione Criar.

Package

Uma aplicação baseada no modelo de Serviço de Trabalhador utiliza o Microsoft.NET.Sdk.Worker SDK e tem uma referência explícita ao pacote Microsoft.Extensions.Hosting. Por exemplo, consulte o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o Microsoft.NET.Sdk.Web SDK, 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 IHostedService interface 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:

O comportamento padrão pode ser alterado para que o serviço hospedado StartAsync seja executado após o pipeline do aplicativo ter sido configurado e ApplicationStarted 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 suave. Quando o cancelamento é solicitado no token:

  • Todas as operações em segundo plano restantes que o aplicativo está executando devem ser anuladas.
  • Os métodos chamados em StopAsync devem retornar imediatamente.

No entanto, as tarefas não são abandonadas após o cancelamento ser solicitado — o chamador aguarda que todas as tarefas sejam concluídas.

Se a aplicação encerrar inesperadamente (por exemplo, se o processo da aplicação falhar), StopAsync pode não ser chamado. Portanto, quaisquer métodos chamados ou operações realizadas em StopAsync podem não ocorrer.

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

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

Classe base BackgroundService

BackgroundService é uma classe base para implementar um IHostedService de longa duração.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna um Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum outro serviço é iniciado até que o ExecuteAsync se torne assíncrono, como chamando await. Evite realizar trabalho de inicialização longo e bloqueante no ExecuteAsync. O host bloqueia em StopAsync(CancellationToken) aguardando ExecuteAsync para concluir.

O token de cancelamento é acionado quando IHostedService.StopAsync é chamado. A sua implementação de ExecuteAsync deve terminar imediatamente quando o token de cancelamento for ativado, a fim de desligar o serviço graciosamente. Caso contrário, o serviço desliga-se ingraciosamente no tempo limite de encerramento. Para obter mais informações, consulte a seção Interface 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 é iniciado até StartAsync ser concluído. Tarefas de longa execução devem ser colocadas em ExecuteAsync. Para obter mais informações, consulte a fonte para BackgroundService.

Tarefas cronometradas em segundo plano

Uma tarefa em segundo plano cronometrada usa a classe System.Threading.Timer . O temporizador aciona o método DoWork da tarefa. O temporizador é desativado em StopAsync e descartado quando o contentor 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 espera que as execuções de DoWork anteriores terminem, 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 sejam atualizados executionCount simultaneamente.

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

services.AddHostedService<TimedHostedService>();

Consumindo um serviço sob escopo numa tarefa em segundo plano

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

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

  • O serviço é assíncrono. O DoWork método retorna um Taskarquivo . Para fins de demonstração, espera-se um atraso de dez segundos no DoWork método.
  • 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 com escopo da tarefa em segundo plano, com o objetivo de chamar o 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 estão registados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registado com o método AddHostedService de extensão:

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

Tarefas em segundo plano enfileiradas

Uma fila de tarefas em segundo plano é baseada no .NET Framework 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 seguinte:

  • O BackgroundProcessing método retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são retiradas da fila e executadas no BackgroundProcessing.
  • Os itens de trabalho estão à espera de serem completados antes que o serviço pare no 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 MonitorLoop serviço lida com tarefas de enfileiramento para o serviço hospedado sempre que a w chave é selecionada em um dispositivo de entrada:

  • O IBackgroundTaskQueue é injetado no serviço MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem é invocado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de longa duração:
    • Três atrasos de 5 segundos são executados (Task.Delay).
    • Uma try-catch instrução interceta 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 estão registados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registado com o método AddHostedService de extensão:

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