在 ASP.NET Core 中使用托管服务实现后台任务Background tasks with hosted services in ASP.NET Core

作者:Luke LathamJeow Li HuanBy 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. 通过辅助角色服务模板创建的应用将在其项目文件中指定 Worker 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.

PackagePackage

基于辅助角色服务模板的应用使用 Microsoft.NET.Sdk.Worker SDK,并且具有对 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).

对于使用 Microsoft.NET.Sdk.Web 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.

IHostedService 接口IHostedService 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. 在以下操作之前调用 StartAsyncStartAsync is called before:

    可以更改默认行为,以便在配置应用的管道并调用 ApplicationStarted 之后,运行托管服务的 StartAsyncThe 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. 若要更改默认行为,请在调用 ConfigureWebHostDefaults 后添加托管服务(以下示例中的 VideosWatcher):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.

    默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。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.

    如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsyncIf 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.

    若要延长默认值为 5 秒的关闭超时值,请设置: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. 如果在执行后台任务期间引发错误,即使未调用 StopAsync,也应调用 DisposeIf an error is thrown during background task execution, Dispose should be called even if StopAsync isn't called.

BackgroundService 基类BackgroundService 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) 中的主机块等待完成 ExecuteAsyncThe 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 interface 部分。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();
    }
}

Timer 不等待先前的 DoWork 执行完成,因此所介绍的方法可能并不适用于所有场景。The Timer doesn't wait for previous executions of DoWork to finish, so the approach shown might not be suitable for every scenario. 使用 Interlocked.Increment 以原子操作的形式将执行计数器递增,这可确保多个线程不会并行更新 executionCountInterlocked.Increment is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update executionCount concurrently.

已使用 AddHostedService 扩展方法在 IHostBuilder.ConfigureServices (Program.cs) 中注册该服务: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 方法返回 TaskThe DoWork method returns a Task. 出于演示目的,在 DoWork 方法中等待 10 秒的延迟。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 返回 ExecuteAsync 等待的 TaskDoWork 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

后台任务队列基于 .NET 4.x QueueBackgroundWorkItem暂定为 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 方法返回 ExecuteAsync 中等待的 TaskThe 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);
    }
}

每当在输入设备上选择 w 键时,MonitorLoop 服务将处理托管服务的排队任务:A MonitorLoop service handles enqueuing tasks for the hosted service whenever the w key is selected on an input device:

  • IBackgroundTaskQueue 注入到 MonitorLoop 服务中。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:
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>();

已在 Program.Main 中启动 MontiorLoopMontiorLoop is started in Program.Main:

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

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

PackagePackage

引用 Microsoft.AspNetCore.App 元包或将包引用添加到 Microsoft.Extensions.Hosting 包。Reference the Microsoft.AspNetCore.App metapackage or add a package reference to the Microsoft.Extensions.Hosting package.

IHostedService 接口IHostedService 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 主机时,会在启动服务器并触发 IApplicationLifetime.ApplicationStarted 后调用 StartAsyncWhen using the Web Host, StartAsync is called after the server has started and IApplicationLifetime.ApplicationStarted is triggered. 当使用通用主机时,会在触发 ApplicationStarted 之前调用 StartAsyncWhen 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.

    默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。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.

    如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsyncIf 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.

    若要延长默认值为 5 秒的关闭超时值,请设置: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. 如果在执行后台任务期间引发错误,即使未调用 StopAsync,也应调用 DisposeIf 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();
    }
}

Timer 不等待先前的 DoWork 执行完成,因此所介绍的方法可能并不适用于所有场景。The Timer doesn't wait for previous executions of DoWork to finish, so the approach shown might not be suitable for every scenario.

已使用 AddHostedService 扩展方法在 Startup.ConfigureServices 中注册该服务: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. 已使用 AddHostedService 扩展方法注册 IHostedService 实现:The IHostedService implementation is registered with the AddHostedService extension method:

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

排队的后台任务Queued background tasks

后台任务队列基于 .NET Framework 4.x QueueBackgroundWorkItem暂定为 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. 已使用 AddHostedService 扩展方法注册 IHostedService 实现:The IHostedService implementation is registered with the AddHostedService extension method:

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

在索引页模型类中:In the Index page model class:

  • IBackgroundTaskQueue 注入构造函数并分配给 QueueThe IBackgroundTaskQueue is injected into the constructor and assigned to Queue.
  • 注入 IServiceScopeFactory 并将其分配给 _serviceScopeFactoryAn 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