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

作者:Luke LathamBy Luke Latham

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

  • 在计时器上运行的后台任务。Background task that runs on a timer.
  • 激活有作用域的服务的托管服务。Hosted service that activates a scoped service. 有作用域的服务可使用依赖项注入。The scoped service can use dependency injection.
  • 按顺序运行的已排队后台任务。Queued background tasks that run sequentially.

查看或下载示例代码如何下载View or download sample code (how to download)

此示例应用分为两个版本:The sample app is provided in two versions:

  • Web 主机 – Web 主机可用于托管 Web 应用。Web Host – Web Host is useful for hosting web apps. 本主题中所示的示例代码来自示例的 Web 主机版本。The example code shown in this topic is from Web Host version of the sample. 有关详细信息,请参阅 Web 主机主题。For more information, see the Web Host topic.
  • 泛型主机 – 泛型主机是 ASP.NET Core 2.1 中的新增功能。Generic Host – Generic Host is new in ASP.NET Core 2.1. 有关详细信息,请参阅通用主机主题。For more information, see the Generic Host topic.

辅助角色服务模板Worker Service template

ASP.NET Core 辅助角色服务模板可作为编写长期服务应用的起点。The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. 要使用该模板作为编写托管服务应用的基础:To use the template as a basis for a hosted services app:

  1. 创建新项目。Create a new project.
  2. 选择“ASP.NET Core Web 应用程序” 。Select ASP.NET Core Web Application. 选择“下一步” 。Select Next.
  3. 在“项目名称”字段提供项目名称,或接受默认项目名称 。Provide a project name in the Project name field or accept the default project name. 选择“创建” 。Select Create.
  4. 在“创建新的 ASP.NET Core Web 应用程序”对话框中,确认选择“.NET Core”和“ASP.NET Core 3.0” 。In the Create a new ASP.NET Core Web Application dialog, confirm that .NET Core and ASP.NET Core 3.0 are selected.
  5. 选择“辅助角色服务”模板 。Select the Worker Service template. 选择“创建” 。Select Create.

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

已使用 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 4.x QueueBackgroundWorkItem暂定为 ASP.NET Core 3.0 内置版本):A background task queue is based on the .NET 4.x QueueBackgroundWorkItem (tentatively scheduled to be built-in for ASP.NET Core 3.0):

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 {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 the 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: {ex.Message}");
                }

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

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

    return RedirectToPage();
}

其他资源Additional resources