Фоновые задачи с размещенными службами в ASP.NET CoreBackground tasks with hosted services in ASP.NET Core

Авторы: Luke Latham (Люк Латэм) и Jeow Li Huan (Чжоу Ли Хуан)By Luke Latham and Jeow Li Huan

В ASP.NET Core фоновые задачи реализуются как размещенные службы.In ASP.NET Core, background tasks can be implemented as hosted services. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService.A hosted service is a class with background task logic that implements the IHostedService interface. Этот раздел содержит три примера размещенных служб:This topic provides three hosted service examples:

Просмотреть или скачать образец кода (как скачивать)View or download sample code (how to download)

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

Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания длительно выполняющихся приложений служб.The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:An app created from the Worker Service template specifies the Worker SDK in its project file:

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

Чтобы использовать шаблон в качестве основы для приложения размещенных служб, выполните указанные ниже действия.To use the template as a basis for a hosted services app:

  1. Создайте новый проект.Create a new project.
  2. Выберите службу рабочей роли.Select Worker Service. Выберите Далее.Select Next.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию.Provide a project name in the Project name field or accept the default project name. Выберите Создать.Select Create.
  4. В диалоговом окне Создать службу рабочей роли выберите Создать.In the Create a new Worker service dialog, select Create.

ПакетPackage

Приложение, основанное на шаблоне рабочей службы, использует пакет SDK для Microsoft.NET.Sdk.Worker и имеет явную ссылку на пакет Microsoft.Extensions.Hosting.An app based on the Worker Service template uses the Microsoft.NET.Sdk.Worker SDK and has an explicit package reference to the Microsoft.Extensions.Hosting package. Например, ознакомьтесь с файлом проекта в примере приложения (BackgroundTasksSample csproj).For example, see the sample app's project file (BackgroundTasksSample.csproj).

Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на пакет Microsoft. Extensions. Hosting указывается неявным образом из общей платформы.For web apps that use the Microsoft.NET.Sdk.Web SDK, the Microsoft.Extensions.Hosting package is referenced implicitly from the shared framework. Явная ссылка на пакет в файле проекта приложения не требуется.An explicit package reference in the app's project file isn't required.

Интерфейс IHostedServiceIHostedService interface

Интерфейс IHostedService определяет два метода для объектов, которые управляются узлом:The IHostedService interface defines two methods for objects that are managed by the host:

  • StartAsync(CancellationToken)StartAsync содержит логику для запуска фоновой задачи.StartAsync(CancellationToken)StartAsync contains the logic to start the background task. Первым вызывается StartAsync:StartAsync is called before:

    Поведение по умолчанию можно изменить таким образом, чтобы StartAsync размещенной службы выполнялся после настройки конвейера приложения и вызова ApplicationStarted.The default behavior can be changed so that the hosted service's StartAsync runs after the app's pipeline has been configured and ApplicationStarted is called. Чтобы изменить поведение по умолчанию, добавьте размещенную службу (VideosWatcher в следующем примере) после вызова ConfigureWebHostDefaults:To change the default behavior, add the hosted service (VideosWatcher in the following example) after calling 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(CancellationToken) – запускается, когда происходит нормальное завершение работы узла.StopAsync(CancellationToken) – Triggered when the host is performing a graceful shutdown. StopAsync содержит логику для завершения фоновой задачи.StopAsync contains the logic to end the background task. Реализуйте IDisposable и методы завершения (деструкторы) для освобождения неуправляемых ресурсов.Implement IDisposable and finalizers (destructors) to dispose of any unmanaged resources.

    Токен отмены использует заданное по умолчанию 5-секундное время ожидания, указывающее, что процесс завершения работы больше не должен быть нормальным.The cancellation token has a default five second timeout to indicate that the shutdown process should no longer be graceful. При запросе отмены происходит следующее:When cancellation is requested on the token:

    • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;Any remaining background operations that the app is performing should be aborted.
    • должны быть незамедлительно возвращены все методы, вызываемые в StopAsync.Any methods called in StopAsync should return promptly.

    Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.

    Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться.If the app shuts down unexpectedly (for example, the app's process fails), StopAsync might not be called. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.Therefore, any methods called or operations conducted in StopAsync might not occur.

    Чтобы увеличить время ожидания завершения работы по умолчанию (пять секунд), установите следующие значения:To extend the default five second shutdown timeout, set:

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения.The hosted service is activated once at app startup and gracefully shut down at app shutdown. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.If an error is thrown during background task execution, Dispose should be called even if StopAsync isn't called.

Базовый класс BackgroundServiceBackgroundService base class

BackgroundService — это базовый класс для реализации долго выполняющегося интерфейса IHostedService.BackgroundService is a base class for implementing a long running IHostedService.

ExecuteAsync(CancellationToken) вызывается для запуска фоновой службы.ExecuteAsync(CancellationToken) is called to run the background service. Реализация возвращает значение Task, представляющее все время существования фоновой службы.The implementation returns a Task that represents the entire lifetime of the background service. Дальнейшие службы не запустятся до тех пор, пока ExecuteAsync не станет асинхронной, например, путем вызова await.No further services are started until ExecuteAsync becomes asynchronous, such as by calling await. Старайтесь не выполнять функцию в течение длительного времени, так как инициализация в ExecuteAsync будет заблокирована.Avoid performing long, blocking initialization work in ExecuteAsync. Блоки узлов в StopAsync(CancellationToken) ожидают завершения ExecuteAsync.The host blocks in StopAsync(CancellationToken) waiting for ExecuteAsync to complete.

Токен отмены активируется при вызове IHostedService.StopAsync.The cancellation token is triggered when IHostedService.StopAsync is called. При выдаче токена отмены реализация ExecuteAsync должна быстро завершиться для корректного завершения работы службы.Your implementation of ExecuteAsync should finish promptly when the cancellation token is fired in order to gracefully shut down the service. В противном случае служба некорректно завершает работу при истечении времени ожидания завершения работы.Otherwise, the service ungracefully shuts down at the shutdown timeout. Дополнительные сведения см. в разделе об интерфейсе IHostedService.For more information, see the IHostedService interface section.

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

Для фоновых задач с заданным временем используется класс System.Threading.Timer.A timed background task makes use of the System.Threading.Timer class. Таймер запускает метод DoWork задачи.The timer triggers the task's DoWork method. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом Dispose:The timer is disabled on StopAsync and disposed when the service container is disposed on 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();
    }
}

Служба регистрируется в IHostBuilder.ConfigureServices (Program.cs) с использованием метода расширения AddHostedService:The service is registered in IHostBuilder.ConfigureServices (Program.cs) with the AddHostedService extension method:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задачеConsuming a scoped service in a background task

Чтобы использовать службы с заданной областью в BackgroundService, создайте область.To use scoped services within a BackgroundService, create a scope. Для размещенной службы по умолчанию не создается область.No scope is created for a hosted service by default.

Служба фоновой задачи с заданной областью содержит логику фоновой задачи.The scoped background task service contains the background task's logic. В следующем примере:In the following example:

  • Служба является асинхронной.The service is asynchronous. Метод DoWork возвращает значение Task.The DoWork method returns a Task. В демонстрационных целях в методе DoWork ожидается задержка в десять секунд.For demonstration purposes, a delay of ten seconds is awaited in the DoWork method.
  • В службу вставляется ILogger.An ILogger is injected into the service.
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:The hosted service creates a scope to resolve the scoped background task service to call its DoWork method. DoWork возвращает объект Task, ожидаемый в ExecuteAsync:DoWork returns a Task, which is awaited in 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 Task.CompletedTask;
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs).The services are registered in IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:The hosted service is registered with the AddHostedService extension method:

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

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

Очередь фоновых задач основывается на методе QueueBackgroundWorkItem в .NET 4.x (предварительно запланировано встроить в ASP.NET Core):A background task queue is based on the .NET 4.x QueueBackgroundWorkItem (tentatively scheduled to be built-in for ASP.NET Core):

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

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

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

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

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

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

        return workItem;
    }
}

В следующем примере QueueHostedService:In the following QueueHostedService example:

  • Метод BackgroundProcessing возвращает объект Task, ожидаемый в ExecuteAsync:The BackgroundProcessing method returns a Task, which is awaited in ExecuteAsync.
  • Фоновые задачи в очереди выводятся из очереди и выполняются в BackgroundProcessing:Background tasks in the queue are dequeued and executed in BackgroundProcessing.
  • Рабочие элементы ожидают остановки службы через StopAsync.Work items are awaited before the service stops in 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:A MonitorLoop service handles enqueuing tasks for the hosted service whenever the w key is selected on an input device:

  • В службу MonitorLoop внедряется IBackgroundTaskQueue.The IBackgroundTaskQueue is injected into the MonitorLoop service.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem вызывается для постановки рабочего элемента в очередь:IBackgroundTaskQueue.QueueBackgroundWorkItem is called to enqueue a work item.
  • Рабочий элемент имитирует долго выполняющуюся фоновую задачу:The work item simulates a long-running background task:
    • Выполняется три 5-секундных задержки (Task.Delay).Three 5-second delays are executed (Task.Delay).
    • Оператор try-catch перехватывается OperationCanceledException, если задача отменена.A try-catch statement traps OperationCanceledException if the task is cancelled.
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("Monitor Loop is starting.");

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

    public void Monitor()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                _taskQueue.QueueBackgroundWorkItem(async 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).The services are registered in IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:The hosted service is registered with the AddHostedService extension method:

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

В ASP.NET Core фоновые задачи реализуются как размещенные службы.In ASP.NET Core, background tasks can be implemented as hosted services. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService.A hosted service is a class with background task logic that implements the IHostedService interface. Этот раздел содержит три примера размещенных служб:This topic provides three hosted service examples:

Просмотреть или скачать образец кода (как скачивать)View or download sample code (how to download)

ПакетPackage

Добавьте ссылку на метапакет Microsoft.AspNetCore.App или добавьте ссылку на пакет в пакет Microsoft.Extensions.Hosting.Reference the Microsoft.AspNetCore.App metapackage or add a package reference to the Microsoft.Extensions.Hosting package.

Интерфейс IHostedServiceIHostedService interface

Размещенные службы реализуют интерфейс IHostedService.Hosted services implement the IHostedService interface. Этот интерфейс определяет два метода для объектов, которые управляются узлом:The interface defines two methods for objects that are managed by the host:

  • StartAsync(CancellationToken)StartAsync содержит логику для запуска фоновой задачи.StartAsync(CancellationToken)StartAsync contains the logic to start the background task. При использовании Web Host StartAsync вызывается после запуска сервера и активации IApplicationLifetime.ApplicationStarted.When using the Web Host, StartAsync is called after the server has started and IApplicationLifetime.ApplicationStarted is triggered. При использовании универсального узла StartAsync вызывается до активации ApplicationStarted.When using the Generic Host, StartAsync is called before ApplicationStarted is triggered.

  • StopAsync(CancellationToken) – запускается, когда происходит нормальное завершение работы узла.StopAsync(CancellationToken) – Triggered when the host is performing a graceful shutdown. StopAsync содержит логику для завершения фоновой задачи.StopAsync contains the logic to end the background task. Реализуйте IDisposable и методы завершения (деструкторы) для освобождения неуправляемых ресурсов.Implement IDisposable and finalizers (destructors) to dispose of any unmanaged resources.

    Токен отмены использует заданное по умолчанию 5-секундное время ожидания, указывающее, что процесс завершения работы больше не должен быть нормальным.The cancellation token has a default five second timeout to indicate that the shutdown process should no longer be graceful. При запросе отмены происходит следующее:When cancellation is requested on the token:

    • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;Any remaining background operations that the app is performing should be aborted.
    • должны быть незамедлительно возвращены все методы, вызываемые в StopAsync.Any methods called in StopAsync should return promptly.

    Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.

    Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться.If the app shuts down unexpectedly (for example, the app's process fails), StopAsync might not be called. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.Therefore, any methods called or operations conducted in StopAsync might not occur.

    Чтобы увеличить время ожидания завершения работы по умолчанию (пять секунд), установите следующие значения:To extend the default five second shutdown timeout, set:

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения.The hosted service is activated once at app startup and gracefully shut down at app shutdown. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.If an error is thrown during background task execution, Dispose should be called even if StopAsync isn't called.

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

Для фоновых задач с заданным временем используется класс System.Threading.Timer.A timed background task makes use of the System.Threading.Timer class. Таймер запускает метод DoWork задачи.The timer triggers the task's DoWork method. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом Dispose:The timer is disabled on StopAsync and disposed when the service container is disposed on Dispose:

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

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

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

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

        return Task.CompletedTask;
    }

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

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

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

        return Task.CompletedTask;
    }

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

Служба зарегистрирована в Startup.ConfigureServices с методом расширения AddHostedService:The service is registered in Startup.ConfigureServices with the AddHostedService extension method:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задачеConsuming a scoped service in a background task

Чтобы использовать службы с заданной областью в IHostedService, создайте область.To use scoped services within an IHostedService, create a scope. Для размещенной службы по умолчанию не создается область.No scope is created for a hosted service by default.

Служба фоновой задачи с заданной областью содержит логику фоновой задачи.The scoped background task service contains the background task's logic. В следующем примере в службу вставляется ILogger:In the following example, an ILogger is injected into the service:

internal interface IScopedProcessingService
{
    void DoWork();
}

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

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

Размещенная служба создает область для разрешения службы фоновой задачи с заданной областью, чтобы вызвать ее метод DoWork:The hosted service creates a scope to resolve the scoped background task service to call its DoWork method:

internal class ConsumeScopedServiceHostedService : IHostedService
{
    private readonly ILogger _logger;

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

    public IServiceProvider Services { get; }

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

        DoWork();

        return Task.CompletedTask;
    }

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

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

            scopedProcessingService.DoWork();
        }
    }

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

        return Task.CompletedTask;
    }
}

Службы регистрируются в Startup.ConfigureServices.The services are registered in Startup.ConfigureServices. Реализация IHostedService зарегистрирована с методом расширения AddHostedService:The IHostedService implementation is registered with the AddHostedService extension method:

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

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

Очередь фоновых задач основывается на методе QueueBackgroundWorkItem в .NET Framework 4.x (предварительно запланировано встроить в ASP.NET Core):A background task queue is based on the .NET Framework 4.x QueueBackgroundWorkItem (tentatively scheduled to be built-in for ASP.NET Core):

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

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

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

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

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

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

        return workItem;
    }
}

В QueueHostedService фоновые задачи в очереди из очереди выводятся из очереди и выполняются в качестве BackgroundService — базового класса для реализации длительного выполнения IHostedService:In QueueHostedService, background tasks in the queue are dequeued and executed as a BackgroundService, which is a base class for implementing a long running IHostedService:

public class QueuedHostedService : BackgroundService
{
    private readonly ILogger _logger;

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

    public IBackgroundTaskQueue TaskQueue { get; }

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

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

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

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

Службы регистрируются в Startup.ConfigureServices.The services are registered in Startup.ConfigureServices. Реализация IHostedService зарегистрирована с методом расширения AddHostedService:The IHostedService implementation is registered with the AddHostedService extension method:

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

В классе модели страницы индексов:In the Index page model class:

  • IBackgroundTaskQueue вставляется в конструктор и присваивается Queue.The IBackgroundTaskQueue is injected into the constructor and assigned to Queue.
  • IServiceScopeFactory вставляется и присваивается _serviceScopeFactory.An IServiceScopeFactory is injected and assigned to _serviceScopeFactory. Фабрика используется для создания экземпляров IServiceScope, который используется для создания служб с заданной областью.The factory is used to create instances of IServiceScope, which is used to create services within a scope. Для использования AppDbContext приложения создается область (служба с заданной областью) для записи данных из базы данных в IBackgroundTaskQueue (отдельная служба).A scope is created in order to use the app's AppDbContext (a scoped service) to write database records in the IBackgroundTaskQueue (a singleton service).
public class IndexModel : PageModel
{
    private readonly AppDbContext _db;
    private readonly ILogger _logger;
    private readonly IServiceScopeFactory _serviceScopeFactory;

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

    public IBackgroundTaskQueue Queue { get; }

Если нажать кнопку Добавить задачу на странице индексов, выполняется метод OnPostAddTask.When the Add Task button is selected on the Index page, the OnPostAddTask method is executed. QueueBackgroundWorkItem вызывается для постановки рабочего элемента в очередь:QueueBackgroundWorkItem is called to enqueue a work item:

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

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

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

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

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

    return RedirectToPage();
}

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