在 ASP.NET Core 中使用託管服務的背景工作Background tasks with hosted services in ASP.NET Core

Jeow Li HuanBy 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:

  • 在計時器上執行的背景工作。Background task that runs on a timer.
  • 啟動已設定範圍之服務的託管服務。Hosted service that activates a scoped service. 已設定範圍的服務可以使用 (DI) 的相依性插入。The scoped service can use dependency injection (DI).
  • 以循序方式執行的排入佇列背景工作。Queued background tasks that run sequentially.

查看或下載範例程式碼 (如何下載) 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.

PackagePackage

以背景工作角色服務範本為基礎的應用程式會使用 Microsoft.NET.Sdk.Worker SDK,並具有對Microsoft Extensions. 裝載封裝的明確套件參考。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) 。For example, see the sample app's project file (BackgroundTasksSample.csproj).

若是使用 SDK 的 web 應用程式 Microsoft.NET.Sdk.Web ,則會隱含地從共用架構參考Microsoft Extensions. 裝載套件。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. 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 在呼叫之後,在下列範例中新增託管服務 () ConfigureWebHostDefaultsTo 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.

    若要延長預設的五秒鐘關機逾時,請設定: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是用來進行長時間執行的基類 IHostedServiceBackgroundService 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 變成非同步(例如呼叫)之前,不會再啟動任何進一步的服務 awaitNo further services are started until ExecuteAsync becomes asynchronous, such as by calling await. 避免執行長時間的封鎖初始化工作 ExecuteAsyncAvoid 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();
    }
}

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. 連鎖:遞增是用來將執行計數器遞增為不可部分完成的作業,這可確保多個執行緒不會 executionCount 同時更新。Interlocked.Increment is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update executionCount concurrently.

服務會在 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 方法會傳回 TaskThe DoWork method returns a Task. 基於示範目的,會在方法中等待10秒的延遲 DoWorkFor 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 ,其等候于 ExecuteAsyncDoWork 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 為基礎 QueueBackgroundWorkItemA background task queue is based on the .NET 4.x QueueBackgroundWorkItem:

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 會傳回,它會在中等待 ExecuteAsyncThe BackgroundProcessing method returns a Task, which is awaited in ExecuteAsync.
  • 佇列中的背景工作會在中進行清除並執行 BackgroundProcessingBackground tasks in the queue are dequeued and executed in BackgroundProcessing.
  • 在服務停止之前,會等待工作專案 StopAsyncWork 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:

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

MonitorLoop開始于 Program.MainMonitorLoop 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:

  • 在計時器上執行的背景工作。Background task that runs on a timer.
  • 啟動已設定範圍之服務的託管服務。Hosted service that activates a scoped service. 已設定範圍的服務可以使用相依性插入 (DI) The scoped service can use dependency injection (DI)
  • 以循序方式執行的排入佇列背景工作。Queued background tasks that run sequentially.

查看或下載範例程式碼 (如何下載) 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 主機時, StartAsync 會在伺服器啟動且 IApplicationLifetime 之後呼叫。 ApplicationStarted會觸發。When using the Web Host, StartAsync is called after the server has started and IApplicationLifetime.ApplicationStarted is triggered. 使用泛型主機時, StartAsync 會在觸發之前呼叫 ApplicationStartedWhen 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();
    }
}

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.

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