Фоновые задачи со встроенными службами в ASP.NET Core

Автор: Джау Ли Хуань (Jeow Li Huan)

Note

Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.

Warning

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.

В ASP.NET Core фоновые задачи реализуются как размещенные службы. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService. Эта статья содержит три примера хостинговых сервисов:

Шаблон службы рабочей роли

Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания долго выполняющихся служебных приложений. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:

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

Чтобы использовать шаблон как основу для приложения хостинговых услуг, выполните указанные ниже действия.

  1. Создание проекта
  2. Выберите службу Worker. Нажмите кнопку Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Нажмите кнопку Далее.
  4. В диалоговом окне Дополнительные сведения выберите Платформа. Нажмите кнопку "Создать".

Package

Приложение, основанное на шаблоне Worker Service, использует SDK-пакет для Microsoft.NET.Sdk.Worker и имеет явную ссылку на пакет Microsoft.Extensions.Hosting. Например, см. файл проекта примера приложения (BackgroundTasksSample.csproj).

Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на пакет Microsoft. Extensions. Hosting указывается неявным образом из общей платформы. Явная ссылка на пакет в файле проекта приложения не требуется.

Интерфейс IHostedService

Интерфейс IHostedService определяет два метода для объектов, которые управляются узлом:

StartAsync

StartAsync(CancellationToken) содержит логику для запуска фоновой задачи. StartAsync вызывается перед:

  • Настраивается конвейер обработки запросов приложения.
  • Запускается сервер и активируется IApplicationLifetime.ApplicationStarted.

StartAsync следует ограничить короткими задачами, так как размещенные службы выполняются последовательно, и никакие другие службы не запускаются до завершения StartAsync.

StopAsync

Маркер отмены по умолчанию имеет 30-секундное время ожидания, указывающее на то, что процесс завершения работы больше не должен быть упорядоченным. При запросе отмены на токене:

  • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
  • Методы, вызываемые в StopAsync, должны незамедлительно возвращать результат.

Однако после запроса на отмену выполнение задач не останавливается — вызывающий ожидает завершения всех задач.

Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.

Чтобы продлить время ожидания завершения работы по умолчанию 30 секунд, задайте следующую настройку:

  • ShutdownTimeout при использовании универсального хоста. Подробную информацию можно найти в разделе Общий хост .NET в ASP.NET Core.
  • Параметр конфигурации узла для времени ожидания завершения работы при использовании веб-узла. Подробные сведения см. в статье Веб-узел ASP.NET Core.

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.

Базовый класс BackgroundService

BackgroundService — это базовый класс для реализации долго выполняющегося процесса IHostedService.

ExecuteAsync(CancellationToken) вызывается в пуле потоков для запуска фоновой службы. Реализация возвращает значение Task, представляющее все время существования фоновой службы. Узлы хостов в StopAsync(CancellationToken) ожидают завершения ExecuteAsync.

ExecuteAsync(CancellationToken) вызывается для запуска фоновой службы. Реализация возвращает значение Task, представляющее все время существования фоновой службы. Дальнейшие службы не запустятся до тех пор, пока ExecuteAsync не станет асинхронной, например, путем вызова await. Избегайте выполнения длительной блокирующей инициализации в ExecuteAsync. Хост блокирует в StopAsync(CancellationToken) ожидая завершения ExecuteAsync.

Токен отмены активируется при вызове IHostedService.StopAsync. При выдаче токена отмены реализация ExecuteAsync должна быстро завершиться для корректного завершения работы службы. В противном случае сервис некорректно прекращает работу при истечении времени ожидания завершения. Дополнительные сведения см. в разделе об интерфейсе IHostedService.

Дополнительные сведения см. в описании исходного кода BackgroundService.

Фоновые задачи с заданным временем

Для фоновых задач с заданным временем используется класс System.Threading.Timer. Таймер запускает метод DoWork задачи. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом Dispose:

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

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

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

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

        return Task.CompletedTask;
    }

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

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

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

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

        return Task.CompletedTask;
    }

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

Timer не ждет завершения предыдущих выполнений DoWork, поэтому данный подход может оказаться неподходящим для некоторых сценариев. Interlocked.Increment используется для увеличения значений счетчика выполнения в виде атомарной операции. Благодаря этому несколько потоков не будут обновлять executionCount одновременно.

Служба зарегистрирована в IHostBuilder.ConfigureServices (Program.cs) с методом расширения AddHostedService:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задаче

Чтобы использовать службы с заданной областью в BackgroundService, создайте область. По умолчанию для хостинг-сервиса не создается объем.

Служба фоновой задачи с ограниченной областью содержит логику фоновой задачи. В следующем примере :

  • Служба является асинхронной. Метод DoWork возвращает Task. В демонстрационных целях в методе DoWork ожидается задержка в десять секунд.
  • В службу внедряется ILogger.
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);
        }
    }
}

Размещенная служба создает область для разрешения службы фоновой задачи с заданной областью, чтобы вызвать ее метод DoWork: DoWork возвращает объект Task, ожидаемый в 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);
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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

Очередь фоновых задач

Фоновая очередь задач основана на .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;
    }
}

В следующем примере QueueHostedService:

  • Метод BackgroundProcessing возвращает объект Task, ожидаемый в ExecuteAsync:
  • Фоновые задачи в очереди выводятся из очереди и выполняются в BackgroundProcessing:
  • Рабочие элементы ожидают завершения перед остановкой службы в 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);
    }
}

Сервис MonitorLoop обрабатывает задания постановки в очередь для размещенного сервиса при выборе на устройстве ввода клавиши w.

  • В службу IBackgroundTaskQueue внедряется MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem вызывается для добавления рабочего элемента в очередь.
  • Рабочий элемент имитирует долго выполняющуюся фоновую задачу:
    • Выполняются три 5-секундные задержки (Task.Delay).
    • Оператор try-catch перехватывает OperationCanceledException, если задача отменена.
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);
        }
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения 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 запущен в Program.cs:

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

Асинхронная фоновая задача с таймером

Следующий код создает асинхронную фоновую задачу с установленным временным интервалом.

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

Шаблоны службы рабочих процессов поддерживают нативный код .NET с использованием AOT с флагом --aot.

  1. Создание проекта
  2. Выберите службу Worker. Нажмите кнопку Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Нажмите кнопку Далее.
  4. В диалоговом окне Дополнительные сведения выполните следующие действия.
  5. Выберите платформу.
  6. Установите флажок "Включить собственную публикацию AOT".
  7. Нажмите кнопку "Создать".

Параметр AOT добавляет <PublishAot>true</PublishAot> в файл проекта.


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

Дополнительные ресурсы

В ASP.NET Core фоновые задачи реализуются как размещенные службы. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService. Эта статья содержит три примера хостинговых сервисов:

Шаблон службы рабочей роли

Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания долго выполняющихся служебных приложений. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:

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

Чтобы использовать шаблон как основу для приложения хостинговых услуг, выполните указанные ниже действия.

  1. Создание проекта
  2. Выберите службу Worker. Нажмите кнопку Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Нажмите кнопку Далее.
  4. В диалоговом окне Дополнительные сведения выберите Платформа. Нажмите кнопку "Создать".

Package

Приложение, основанное на шаблоне Worker Service, использует SDK-пакет для Microsoft.NET.Sdk.Worker и имеет явную ссылку на пакет Microsoft.Extensions.Hosting. Например, см. файл проекта примера приложения (BackgroundTasksSample.csproj).

Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на пакет Microsoft. Extensions. Hosting указывается неявным образом из общей платформы. Явная ссылка на пакет в файле проекта приложения не требуется.

Интерфейс IHostedService

Интерфейс IHostedService определяет два метода для объектов, которые управляются узлом:

StartAsync

StartAsync(CancellationToken) содержит логику для запуска фоновой задачи. StartAsync вызывается перед:

  • Настраивается конвейер обработки запросов приложения.
  • Запускается сервер и активируется IApplicationLifetime.ApplicationStarted.

StartAsync следует ограничить короткими задачами, так как размещенные службы выполняются последовательно, и никакие другие службы не запускаются до завершения StartAsync.

StopAsync

Маркер отмены по умолчанию имеет 30-секундное время ожидания, указывающее на то, что процесс завершения работы больше не должен быть упорядоченным. При запросе отмены токена:

  • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
  • Методы, вызываемые в StopAsync, должны быстро завершать выполнение.

Однако, после запроса на отмену, выполнение задач не прекращается — вызывающая сторона ожидает завершения всех задач.

Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.

Чтобы продлить время ожидания завершения работы по умолчанию 30 секунд, задайте следующую настройку:

  • ShutdownTimeout при использовании Generic Host. Подробную информацию можно найти в разделе Общий хост .NET в ASP.NET Core.
  • Параметр конфигурации узла для времени ожидания завершения работы при использовании веб-узла. Подробные сведения см. в статье Веб-узел ASP.NET Core.

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.

Базовый класс BackgroundService

BackgroundService — это базовый класс для реализации долго выполняющегося процесса IHostedService.

ExecuteAsync(CancellationToken) вызывается для запуска фоновой службы. Реализация возвращает значение Task, представляющее все время существования фоновой службы. Дальнейшие службы не запустятся до тех пор, пока ExecuteAsync не станет асинхронной, например, путем вызова await. Избегайте выполнения длительной блокирующей инициализации в ExecuteAsync. Хост блокирует в StopAsync(CancellationToken) ожидая завершения ExecuteAsync.

Токен отмены активируется при вызове IHostedService.StopAsync. При выдаче токена отмены реализация ExecuteAsync должна быстро завершиться для корректного завершения работы службы. В противном случае сервис некорректно прекращает работу при истечении времени ожидания завершения. Дополнительные сведения см. в разделе об интерфейсе IHostedService.

Дополнительные сведения см. в описании исходного кода BackgroundService.

Фоновые задачи с заданным временем

Для фоновых задач с заданным временем используется класс System.Threading.Timer. Таймер запускает метод DoWork задачи. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом Dispose:

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

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

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

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

        return Task.CompletedTask;
    }

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

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

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

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

        return Task.CompletedTask;
    }

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

Timer не ждет завершения предыдущих выполнений DoWork, поэтому данный подход может оказаться неподходящим для некоторых сценариев. Interlocked.Increment используется для увеличения значений счетчика выполнения в виде атомарной операции. Благодаря этому несколько потоков не будут обновлять executionCount одновременно.

Служба зарегистрирована в IHostBuilder.ConfigureServices (Program.cs) с методом расширения AddHostedService:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задаче

Чтобы использовать службы с заданной областью в BackgroundService, создайте область. По умолчанию для хостинг-сервиса не создается объем.

Служба фоновой задачи с ограниченной областью содержит логику фоновой задачи. В следующем примере :

  • Служба является асинхронной. Метод DoWork возвращает Task. В демонстрационных целях в методе DoWork ожидается задержка в десять секунд.
  • В службу внедряется ILogger.
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);
        }
    }
}

Размещенная служба создает область для разрешения службы фоновой задачи с заданной областью, чтобы вызвать ее метод DoWork: DoWork возвращает объект Task, ожидаемый в 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);
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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

Очередь фоновых задач

Фоновая очередь задач основана на .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;
    }
}

В следующем примере QueueHostedService:

  • Метод BackgroundProcessing возвращает объект Task, ожидаемый в ExecuteAsync:
  • Фоновые задачи в очереди выводятся из очереди и выполняются в BackgroundProcessing:
  • Рабочие элементы должны быть обработаны перед остановкой службы в 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);
    }
}

Служба MonitorLoop выполняет задачи по постановке в очередь для обслуживаемой службы при выборе на устройстве ввода клавиши w:

  • IBackgroundTaskQueue внедряется в службу MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem вызывается для добавления рабочего элемента в очередь.
  • Рабочий элемент имитирует долго выполняющуюся фоновую задачу:
    • Выполняются три 5-секундные задержки (Task.Delay).
    • Оператор try-catch перехватывает OperationCanceledException, если задача отменена.
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);
        }
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения 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 запущен в Program.cs:

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

Асинхронная фоновая задача с таймером

Следующий код создает асинхронную фоновую задачу с установленным временным интервалом.

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

Дополнительные ресурсы

В ASP.NET Core фоновые задачи реализуются как размещенные службы. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService. Эта статья содержит три примера хостинговых сервисов:

Просмотреть или скачать образец кода (описание загрузки)

Шаблон службы рабочей роли

Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания долго выполняющихся служебных приложений. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:

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

Чтобы использовать шаблон как основу для приложения хостинговых услуг, выполните указанные ниже действия.

  1. Создание проекта
  2. Выберите службу Worker. Нажмите кнопку Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Нажмите кнопку "Создать".
  4. В диалоговом окне Создать новую службу Worker выберите Создать.

Package

Приложение, основанное на шаблоне Worker Service, использует SDK-пакет для Microsoft.NET.Sdk.Worker и имеет явную ссылку на пакет Microsoft.Extensions.Hosting. Например, см. файл проекта примера приложения (BackgroundTasksSample.csproj).

Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на пакет Microsoft. Extensions. Hosting указывается неявным образом из общей платформы. Явная ссылка на пакет в файле проекта приложения не требуется.

Интерфейс IHostedService

Интерфейс IHostedService определяет два метода для объектов, которые управляются узлом:

StartAsync

StartAsync содержит логику для запуска фоновой задачи. StartAsync вызывается перед:

  • Настраивается конвейер обработки запросов приложения.
  • Запускается сервер и активируется IApplicationLifetime.ApplicationStarted.

Поведение по умолчанию можно изменить таким образом, чтобы размещенная служба StartAsync выполнялась после настройки конвейера приложения и вызова ApplicationStarted. Чтобы изменить поведение по умолчанию, добавьте размещенную службу (VideosWatcher в следующем примере) после вызова 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

Токен отмены использует заданное по умолчанию 5-секундное время ожидания, указывающее, что процесс завершения работы больше не должен быть нормальным. При запросе отмены на токене:

  • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
  • Методы, вызываемые в StopAsync, должны незамедлительно возвращать результат.

Однако после запроса на отмену выполнение задач не останавливается — вызывающий ожидает завершения всех задач.

Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.

Чтобы увеличить время ожидания завершения работы по умолчанию (пять секунд), установите следующие значения:

  • ShutdownTimeout при использовании универсального хоста. Подробную информацию можно найти в разделе Общий хост .NET в ASP.NET Core.
  • Параметр конфигурации узла для времени ожидания завершения работы при использовании веб-узла. Подробные сведения см. в статье Веб-узел ASP.NET Core.

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.

Базовый класс BackgroundService

BackgroundService — это базовый класс для реализации долго выполняющегося процесса IHostedService.

ExecuteAsync(CancellationToken) вызывается для запуска фоновой службы. Реализация возвращает значение Task, представляющее все время существования фоновой службы. Дальнейшие службы не запустятся до тех пор, пока ExecuteAsync не станет асинхронной, например, путем вызова await. Избегайте выполнения длительной блокирующей инициализации в ExecuteAsync. Хост блокирует в StopAsync(CancellationToken) ожидая завершения ExecuteAsync.

Токен отмены активируется при вызове IHostedService.StopAsync. При выдаче токена отмены реализация ExecuteAsync должна быстро завершиться для корректного завершения работы службы. В противном случае сервис некорректно прекращает работу при истечении времени ожидания завершения. Дополнительные сведения см. в разделе об интерфейсе IHostedService.

StartAsync следует ограничить короткими задачами, так как размещенные службы выполняются последовательно, и никакие другие службы не запускаются до завершения StartAsync. Длительные задачи должны размещаться в ExecuteAsync. Дополнительные сведения см. в описании BackgroundService.

Фоновые задачи с заданным временем

Для фоновых задач с заданным временем используется класс System.Threading.Timer. Таймер запускает метод DoWork задачи. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом Dispose:

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

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

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

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

        return Task.CompletedTask;
    }

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

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

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

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

        return Task.CompletedTask;
    }

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

Timer не ждет завершения предыдущих выполнений DoWork, поэтому данный подход может оказаться неподходящим для некоторых сценариев. Interlocked.Increment используется для увеличения значений счетчика выполнения в виде атомарной операции. Благодаря этому несколько потоков не будут обновлять executionCount одновременно.

Служба зарегистрирована в IHostBuilder.ConfigureServices (Program.cs) с методом расширения AddHostedService:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задаче

Чтобы использовать службы с заданной областью в BackgroundService, создайте область. По умолчанию для хостинг-сервиса не создается объем.

Служба фоновой задачи с ограниченной областью содержит логику фоновой задачи. В следующем примере :

  • Служба является асинхронной. Метод DoWork возвращает Task. В демонстрационных целях в методе DoWork ожидается задержка в десять секунд.
  • В службу внедряется ILogger.
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);
        }
    }
}

Размещенная служба создает область для разрешения службы фоновой задачи с заданной областью, чтобы вызвать ее метод DoWork: DoWork возвращает объект Task, ожидаемый в 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);
    }
}

Услуги регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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

Фоновые задачи в очереди

Фоновая очередь задач основана на .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;
    }
}

В следующем примере QueueHostedService:

  • Метод BackgroundProcessing возвращает объект Task, ожидаемый в ExecuteAsync:
  • Фоновые задачи в очереди выводятся из очереди и выполняются в BackgroundProcessing:
  • Рабочие элементы должны быть обработаны перед остановкой службы в 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);
    }
}

Служба MonitorLoop выполняет задачи по постановке в очередь для обслуживаемой службы при выборе на устройстве ввода клавиши w:

  • IBackgroundTaskQueue внедряется в службу MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem вызывается для добавления рабочего элемента в очередь.
  • Рабочий элемент имитирует долго выполняющуюся фоновую задачу:
    • Выполняются три 5-секундные задержки (Task.Delay).
    • Оператор try-catch перехватывает OperationCanceledException, если задача отменена.
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);
        }
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения 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 запущен в Program.Main:

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

Дополнительные ресурсы