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

    若要延長預設的五秒鐘關機逾時,請設定: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();
    }
}

服務是在 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

背景工作佇列是以 .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. 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 插入至建構函式並指派給 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