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

在 ASP.NET Core 中,后台任务作为托管服务实现 。托管服务是一个类,具有实现 接口的后台任务逻辑。本主题提供了三个托管服务示例:

  • 在计时器上运行的后台任务。
  • 激活有作用域的服务的托管服务。有作用域的服务可使用。
  • 按顺序运行的已排队后台任务。

查看或下载示例代码()

ASP.NET Core 辅助角色服务模板可作为编写长期服务应用的起点。通过辅助角色服务模板创建的应用将在其项目文件中指定 Worker SDK:

要使用该模板作为编写托管服务应用的基础:

  • 创建新项目。
  • 选择“辅助角色服务”。选择“下一步”。
  • 在“项目名称”字段提供项目名称,或接受默认项目名称。选择“创建”。
  • 在“创建辅助角色服务”对话框中,选择“创建”。
  • 创建新项目。
  • 在侧栏中的“.NET Core”下,选择“应用”。
  • 在“ASP.NET Core”下,选择“辅助角色”。选择“下一步”。
  • 对于“目标框架”,选择“.NET Core 3.0”或更高版本。选择“下一步”。
  • 在“项目名称”字段中提供名称。选择“创建”。

将辅助角色服务 () 模板用于命令行界面中的 命令。下面的示例中创建了名为 ContosoWorker 的辅助角色服务应用。执行命令时会自动为 ContosoWorker 应用创建文件夹。

  1. dotnet new worker -o ContosoWorker

PackagePackage

基于辅助角色服务模板的应用使用 Microsoft.NET.Sdk.Worker SDK,并且具有对 包的显式包引用。有关示例,请参阅示例应用的项目文件 (BackgroundTasksSample.csproj)。

对于使用 Microsoft.NET.Sdk.Web SDK 的 Web 应用,通过共享框架隐式引用 Microsoft.Extensions.Hosting 包。在应用的项目文件中不需要显式包引用。

IHostedService 接口IHostedService interface

IHostedService 接口为主机托管的对象定义了两种方法:

  • StartAsync 包含启动后台任务的逻辑。在以下操作之前调用 StartAsync

    • 已配置应用的请求处理管道 (Startup.Configure)。
    • 已启动服务器且已触发 IApplicationLifetime.ApplicationStarted
      可以更改默认行为,以便在配置应用的管道并调用 ApplicationStarted 之后,运行托管服务的 StartAsync。若要更改默认行为,请在调用 ConfigureWebHostDefaults 后添加托管服务(以下示例中的 VideosWatcher):
  1. using Microsoft.AspNetCore.Hosting;
  2. using Microsoft.Extensions.DependencyInjection;
  3. using Microsoft.Extensions.Hosting;
  4. public class Program
  5. {
  6. public static void Main(string[] args)
  7. {
  8. CreateHostBuilder(args).Build().Run();
  9. }
  10. public static IHostBuilder CreateHostBuilder(string[] args) =>
  11. Host.CreateDefaultBuilder(args)
  12. .ConfigureWebHostDefaults(webBuilder =>
  13. {
  14. webBuilder.UseStartup<Startup>();
  15. })
  16. .ConfigureServices(services =>
  17. {
  18. services.AddHostedService<VideosWatcher>();
  19. });
  20. }
  • – 主机正常关闭时触发。StopAsync 包含结束后台任务的逻辑。实现 IDisposable 和以处置任何非托管资源。

默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。在令牌上请求取消时:

  • 应中止应用正在执行的任何剩余后台操作。
  • StopAsync 中调用的任何方法都应及时返回。
    但是,在请求取消后,将不会放弃任务 — 调用方等待所有任务完成。

如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsync。因此,在 StopAsync 中执行的任何方法或操作都可能不会发生。

若要延长默认值为 5 秒的关闭超时值,请设置:

  • ShutdownTimeout(当使用通用主机时)。有关详细信息,请参阅 。
  • 使用 Web 主机时为关闭超时值主机配置设置。有关详细信息,请参阅 ASP.NET Core Web 主机

托管服务在应用启动时激活一次,在应用关闭时正常关闭。如果在执行后台任务期间引发错误,即使未调用 StopAsync,也应调用 Dispose

BackgroundService 基类BackgroundService base class

BackgroundService 是用于实现长时间运行的 的基类。

调用 ExecuteAsync(CancellationToken) 来运行后台服务。实现返回一个 ,其表示后台服务的整个生存期。ExecuteAsync 变为异步(例如通过调用 await)之前,不会启动任何其他服务。避免在 ExecuteAsync 中执行长时间的阻塞初始化工作。 中的主机块等待完成 ExecuteAsync

调用 IHostedService.StopAsync 时,将触发取消令牌。当激发取消令牌以便正常关闭服务时,ExecuteAsync 的实现应立即完成。否则,服务将在关闭超时后不正常关闭。有关更多信息,请参阅 部分。

  1. public class TimedHostedService : IHostedService, IDisposable
  2. {
  3. private int executionCount = 0;
  4. private readonly ILogger<TimedHostedService> _logger;
  5. private Timer _timer;
  6. public TimedHostedService(ILogger<TimedHostedService> logger)
  7. {
  8. _logger = logger;
  9. }
  10. public Task StartAsync(CancellationToken stoppingToken)
  11. {
  12. _logger.LogInformation("Timed Hosted Service running.");
  13. _timer = new Timer(DoWork, null, TimeSpan.Zero,
  14. TimeSpan.FromSeconds(5));
  15. return Task.CompletedTask;
  16. }
  17. private void DoWork(object state)
  18. {
  19. var count = Interlocked.Increment(ref executionCount);
  20. _logger.LogInformation(
  21. "Timed Hosted Service is working. Count: {Count}", count);
  22. }
  23. public Task StopAsync(CancellationToken stoppingToken)
  24. {
  25. _logger.LogInformation("Timed Hosted Service is stopping.");
  26. _timer?.Change(Timeout.Infinite, 0);
  27. return Task.CompletedTask;
  28. }
  29. public void Dispose()
  30. {
  31. _timer?.Dispose();
  32. }
  33. }

Timer 不等待先前的 DoWork 执行完成,因此所介绍的方法可能并不适用于所有场景。使用 以原子操作的形式将执行计数器递增,这可确保多个线程不会并行更新 executionCount

已使用 AddHostedService 扩展方法在 IHostBuilder.ConfigureServices (Program.cs) 中注册该服务:

  1. services.AddHostedService<TimedHostedService>();

在后台任务中使用有作用域的服务Consuming a scoped service in a background task

要在 中使用有作用域的服务,请创建作用域。默认情况下,不会为托管服务创建作用域。

作用域后台任务服务包含后台任务的逻辑。如下示例中:

  • 服务是异步的。DoWork 方法返回 Task。出于演示目的,在 DoWork 方法中等待 10 秒的延迟。
  • 注入到服务中。
  1. internal interface IScopedProcessingService
  2. {
  3. Task DoWork(CancellationToken stoppingToken);
  4. }
  5. internal class ScopedProcessingService : IScopedProcessingService
  6. {
  7. private int executionCount = 0;
  8. private readonly ILogger _logger;
  9. public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
  10. {
  11. _logger = logger;
  12. }
  13. public async Task DoWork(CancellationToken stoppingToken)
  14. {
  15. while (!stoppingToken.IsCancellationRequested)
  16. {
  17. executionCount++;
  18. _logger.LogInformation(
  19. "Scoped Processing Service is working. Count: {Count}", executionCount);
  20. await Task.Delay(10000, stoppingToken);
  21. }
  22. }
  23. }

托管服务创建一个作用域来解决作用域后台任务服务以调用其 DoWork 方法。DoWork 返回 ExecuteAsync 等待的 Task

  1. public class ConsumeScopedServiceHostedService : BackgroundService
  2. {
  3. private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
  4. public ConsumeScopedServiceHostedService(IServiceProvider services,
  5. ILogger<ConsumeScopedServiceHostedService> logger)
  6. {
  7. Services = services;
  8. _logger = logger;
  9. }
  10. public IServiceProvider Services { get; }
  11. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  12. {
  13. _logger.LogInformation(
  14. "Consume Scoped Service Hosted Service running.");
  15. await DoWork(stoppingToken);
  16. }
  17. private async Task DoWork(CancellationToken stoppingToken)
  18. {
  19. _logger.LogInformation(
  20. "Consume Scoped Service Hosted Service is working.");
  21. using (var scope = Services.CreateScope())
  22. {
  23. var scopedProcessingService =
  24. scope.ServiceProvider
  25. .GetRequiredService<IScopedProcessingService>();
  26. await scopedProcessingService.DoWork(stoppingToken);
  27. }
  28. }
  29. public override async Task StopAsync(CancellationToken stoppingToken)
  30. {
  31. _logger.LogInformation(
  32. "Consume Scoped Service Hosted Service is stopping.");
  33. await Task.CompletedTask;
  34. }
  35. }

已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。已使用 AddHostedService 扩展方法注册托管服务:

排队的后台任务Queued background tasks

后台任务队列基于 .NET 4.x (暂定为 ASP.NET Core 内置版本):

  1. public interface IBackgroundTaskQueue
  2. {
  3. void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
  4. Task<Func<CancellationToken, Task>> DequeueAsync(
  5. CancellationToken cancellationToken);
  6. }
  7. public class BackgroundTaskQueue : IBackgroundTaskQueue
  8. {
  9. private ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
  10. new ConcurrentQueue<Func<CancellationToken, Task>>();
  11. private SemaphoreSlim _signal = new SemaphoreSlim(0);
  12. public void QueueBackgroundWorkItem(
  13. Func<CancellationToken, Task> workItem)
  14. if (workItem == null)
  15. {
  16. throw new ArgumentNullException(nameof(workItem));
  17. }
  18. _workItems.Enqueue(workItem);
  19. _signal.Release();
  20. }
  21. CancellationToken cancellationToken)
  22. {
  23. await _signal.WaitAsync(cancellationToken);
  24. _workItems.TryDequeue(out var workItem);
  25. return workItem;
  26. }
  27. }

在以下 QueueHostedService 示例中:

  • BackgroundProcessing 方法返回 ExecuteAsync 中等待的 Task
  • BackgroundProcessing 中,取消排队并执行队列中的后台任务。
  • 服务在 StopAsync 中停止之前,将等待工作项。
  1. public class QueuedHostedService : BackgroundService
  2. {
  3. private readonly ILogger<QueuedHostedService> _logger;
  4. public QueuedHostedService(IBackgroundTaskQueue taskQueue,
  5. ILogger<QueuedHostedService> logger)
  6. {
  7. TaskQueue = taskQueue;
  8. _logger = logger;
  9. }
  10. public IBackgroundTaskQueue TaskQueue { get; }
  11. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  12. {
  13. _logger.LogInformation(
  14. $"Queued Hosted Service is running.{Environment.NewLine}" +
  15. $"{Environment.NewLine}Tap W to add a work item to the " +
  16. $"background queue.{Environment.NewLine}");
  17. await BackgroundProcessing(stoppingToken);
  18. }
  19. private async Task BackgroundProcessing(CancellationToken stoppingToken)
  20. {
  21. while (!stoppingToken.IsCancellationRequested)
  22. {
  23. var workItem =
  24. await TaskQueue.DequeueAsync(stoppingToken);
  25. try
  26. {
  27. await workItem(stoppingToken);
  28. }
  29. catch (Exception ex)
  30. {
  31. _logger.LogError(ex,
  32. "Error occurred executing {WorkItem}.", nameof(workItem));
  33. }
  34. }
  35. }
  36. public override async Task StopAsync(CancellationToken stoppingToken)
  37. {
  38. _logger.LogInformation("Queued Hosted Service is stopping.");
  39. await base.StopAsync(stoppingToken);
  40. }
  41. }

每当在输入设备上选择 w 键时,MonitorLoop 服务将处理托管服务的排队任务:

  • IBackgroundTaskQueue 注入到 MonitorLoop 服务中。
  • 调用 IBackgroundTaskQueue.QueueBackgroundWorkItem 来将工作项排入队列。
  • 工作项模拟长时间运行的后台任务:
    • 将执行三次 5 秒的延迟 (Task.Delay)。
    • 如果任务已取消,try-catch 语句将捕获 。
  1. public class MonitorLoop
  2. {
  3. private readonly IBackgroundTaskQueue _taskQueue;
  4. private readonly ILogger _logger;
  5. private readonly CancellationToken _cancellationToken;
  6. public MonitorLoop(IBackgroundTaskQueue taskQueue,
  7. ILogger<MonitorLoop> logger,
  8. IHostApplicationLifetime applicationLifetime)
  9. {
  10. _taskQueue = taskQueue;
  11. _logger = logger;
  12. _cancellationToken = applicationLifetime.ApplicationStopping;
  13. }
  14. public void StartMonitorLoop()
  15. {
  16. _logger.LogInformation("Monitor Loop is starting.");
  17. // Run a console user input loop in a background thread
  18. Task.Run(() => Monitor());
  19. }
  20. public void Monitor()
  21. {
  22. while (!_cancellationToken.IsCancellationRequested)
  23. {
  24. var keyStroke = Console.ReadKey();
  25. if (keyStroke.Key == ConsoleKey.W)
  26. {
  27. // Enqueue a background work item
  28. _taskQueue.QueueBackgroundWorkItem(async token =>
  29. {
  30. // Simulate three 5-second tasks to complete
  31. // for each enqueued work item
  32. int delayLoop = 0;
  33. var guid = Guid.NewGuid().ToString();
  34. _logger.LogInformation(
  35. "Queued Background Task {Guid} is starting.", guid);
  36. while (!token.IsCancellationRequested && delayLoop < 3)
  37. {
  38. try
  39. {
  40. await Task.Delay(TimeSpan.FromSeconds(5), token);
  41. }
  42. catch (OperationCanceledException)
  43. {
  44. // Prevent throwing if the Delay is cancelled
  45. }
  46. delayLoop++;
  47. _logger.LogInformation(
  48. "Queued Background Task {Guid} is running. " +
  49. "{DelayLoop}/3", guid, delayLoop);
  50. }
  51. if (delayLoop == 3)
  52. {
  53. _logger.LogInformation(
  54. "Queued Background Task {Guid} is complete.", guid);
  55. }
  56. else
  57. {
  58. _logger.LogInformation(
  59. "Queued Background Task {Guid} was cancelled.", guid);
  60. }
  61. });
  62. }
  63. }
  64. }
  65. }

已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。已使用 AddHostedService 扩展方法注册托管服务:

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

已在 Program.Main 中启动 MontiorLoop

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

在 ASP.NET Core 中,后台任务作为托管服务实现 。托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。本主题提供了三个托管服务示例:

  • 在计时器上运行的后台任务。
  • 激活有的托管服务。有作用域的服务可使用依赖项注入 (DI)
  • 按顺序运行的已排队后台任务。

如何下载

PackagePackage

引用 Microsoft.AspNetCore.App 元包或将包引用添加到 包。

托管服务实现 IHostedService 接口。该接口为主机托管的对象定义了两种方法:

  • StartAsync 包含启动后台任务的逻辑。当使用 Web 主机时,会在启动服务器并触发 后调用 StartAsync。当使用通用主机时,会在触发 ApplicationStarted 之前调用 StartAsync

默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。在令牌上请求取消时:

  • 应中止应用正在执行的任何剩余后台操作。
  • StopAsync 中调用的任何方法都应及时返回。
    但是,在请求取消后,将不会放弃任务 — 调用方等待所有任务完成。

如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsync。因此,在 StopAsync 中执行的任何方法或操作都可能不会发生。

若要延长默认值为 5 秒的关闭超时值,请设置:

  • (当使用通用主机时)。有关详细信息,请参阅 .NET 通用主机
  • 使用 Web 主机时为关闭超时值主机配置设置。有关详细信息,请参阅 。

托管服务在应用启动时激活一次,在应用关闭时正常关闭。如果在执行后台任务期间引发错误,即使未调用 StopAsync,也应调用 Dispose

计时的后台任务Timed background tasks

定时后台任务使用 类。计时器触发任务的 DoWork 方法。StopAsync 上禁用计时器,并在 Dispose 上处置服务容器时处置计时器:

  1. internal class TimedHostedService : IHostedService, IDisposable
  2. {
  3. private readonly ILogger _logger;
  4. private Timer _timer;
  5. public TimedHostedService(ILogger<TimedHostedService> logger)
  6. {
  7. _logger = logger;
  8. }
  9. public Task StartAsync(CancellationToken cancellationToken)
  10. {
  11. _logger.LogInformation("Timed Background Service is starting.");
  12. _timer = new Timer(DoWork, null, TimeSpan.Zero,
  13. TimeSpan.FromSeconds(5));
  14. return Task.CompletedTask;
  15. }
  16. private void DoWork(object state)
  17. {
  18. _logger.LogInformation("Timed Background Service is working.");
  19. }
  20. public Task StopAsync(CancellationToken cancellationToken)
  21. {
  22. _logger.LogInformation("Timed Background Service is stopping.");
  23. return Task.CompletedTask;
  24. }
  25. {
  26. _timer?.Dispose();
  27. }
  28. }

Timer 不等待先前的 DoWork 执行完成,因此所介绍的方法可能并不适用于所有场景。

已使用 AddHostedService 扩展方法在 Startup.ConfigureServices 中注册该服务:

在后台任务中使用有作用域的服务Consuming a scoped service in a background task

要在 IHostedService 中使用有作用域的服务,请创建一个作用域。默认情况下,不会为托管服务创建作用域。

作用域后台任务服务包含后台任务的逻辑。在以下示例中,将 注入到服务中:

  1. internal interface IScopedProcessingService
  2. {
  3. void DoWork();
  4. }
  5. internal class ScopedProcessingService : IScopedProcessingService
  6. {
  7. private readonly ILogger _logger;
  8. public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
  9. {
  10. _logger = logger;
  11. }
  12. public void DoWork()
  13. {
  14. _logger.LogInformation("Scoped Processing Service is working.");
  15. }
  16. }

托管服务创建一个作用域来解决作用域后台任务服务以调用其 DoWork 方法:

  1. internal class ConsumeScopedServiceHostedService : IHostedService
  2. {
  3. private readonly ILogger _logger;
  4. public ConsumeScopedServiceHostedService(IServiceProvider services,
  5. ILogger<ConsumeScopedServiceHostedService> logger)
  6. {
  7. Services = services;
  8. _logger = logger;
  9. }
  10. public IServiceProvider Services { get; }
  11. public Task StartAsync(CancellationToken cancellationToken)
  12. {
  13. _logger.LogInformation(
  14. "Consume Scoped Service Hosted Service is starting.");
  15. DoWork();
  16. return Task.CompletedTask;
  17. }
  18. private void DoWork()
  19. {
  20. _logger.LogInformation(
  21. "Consume Scoped Service Hosted Service is working.");
  22. using (var scope = Services.CreateScope())
  23. {
  24. var scopedProcessingService =
  25. scope.ServiceProvider
  26. .GetRequiredService<IScopedProcessingService>();
  27. scopedProcessingService.DoWork();
  28. }
  29. }
  30. public Task StopAsync(CancellationToken cancellationToken)
  31. {
  32. _logger.LogInformation(
  33. "Consume Scoped Service Hosted Service is stopping.");
  34. return Task.CompletedTask;
  35. }
  36. }

已在 Startup.ConfigureServices 中注册这些服务。已使用 AddHostedService 扩展方法注册 IHostedService 实现:

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

排队的后台任务Queued background tasks

后台任务队列基于 .NET Framework 4.x (暂定为 ASP.NET Core 内置版本):

  1. public interface IBackgroundTaskQueue
  2. {
  3. void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
  4. Task<Func<CancellationToken, Task>> DequeueAsync(
  5. CancellationToken cancellationToken);
  6. }
  7. public class BackgroundTaskQueue : IBackgroundTaskQueue
  8. {
  9. private ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
  10. new ConcurrentQueue<Func<CancellationToken, Task>>();
  11. private SemaphoreSlim _signal = new SemaphoreSlim(0);
  12. public void QueueBackgroundWorkItem(
  13. Func<CancellationToken, Task> workItem)
  14. {
  15. if (workItem == null)
  16. {
  17. throw new ArgumentNullException(nameof(workItem));
  18. }
  19. _workItems.Enqueue(workItem);
  20. _signal.Release();
  21. }
  22. public async Task<Func<CancellationToken, Task>> DequeueAsync(
  23. CancellationToken cancellationToken)
  24. {
  25. await _signal.WaitAsync(cancellationToken);
  26. _workItems.TryDequeue(out var workItem);
  27. return workItem;
  28. }
  29. }

QueueHostedService 中,队列中的后台任务会取消排队,并作为 执行,此类是用于实现长时间运行 IHostedService 的基类:

  1. public class QueuedHostedService : BackgroundService
  2. {
  3. private readonly ILogger _logger;
  4. public QueuedHostedService(IBackgroundTaskQueue taskQueue,
  5. ILoggerFactory loggerFactory)
  6. {
  7. TaskQueue = taskQueue;
  8. _logger = loggerFactory.CreateLogger<QueuedHostedService>();
  9. }
  10. public IBackgroundTaskQueue TaskQueue { get; }
  11. protected async override Task ExecuteAsync(
  12. CancellationToken cancellationToken)
  13. {
  14. _logger.LogInformation("Queued Hosted Service is starting.");
  15. while (!cancellationToken.IsCancellationRequested)
  16. {
  17. var workItem = await TaskQueue.DequeueAsync(cancellationToken);
  18. try
  19. {
  20. await workItem(cancellationToken);
  21. }
  22. catch (Exception ex)
  23. {
  24. _logger.LogError(ex,
  25. "Error occurred executing {WorkItem}.", nameof(workItem));
  26. }
  27. }
  28. _logger.LogInformation("Queued Hosted Service is stopping.");
  29. }
  30. }

已在 Startup.ConfigureServices 中注册这些服务。已使用 AddHostedService 扩展方法注册 IHostedService 实现:

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

在索引页模型类中:

  • IBackgroundTaskQueue 注入构造函数并分配给 Queue
  • 注入 IServiceScopeFactory 并将其分配给 _serviceScopeFactory。工厂用于创建 的实例,用于在范围内创建服务。创建范围是为了使用应用的AppDbContext设置了范围的服务),以在 IBackgroundTaskQueue(单一实例服务)中写入数据库记录。

在索引页上选择“添加任务”按钮时,会执行 OnPostAddTask 方法 。调用 QueueBackgroundWorkItem 来将工作项排入队列:

  1. public IActionResult OnPostAddTaskAsync()
  2. {
  3. Queue.QueueBackgroundWorkItem(async token =>
  4. {
  5. var guid = Guid.NewGuid().ToString();
  6. using (var scope = _serviceScopeFactory.CreateScope())
  7. {
  8. var scopedServices = scope.ServiceProvider;
  9. var db = scopedServices.GetRequiredService<AppDbContext>();
  10. for (int delayLoop = 1; delayLoop < 4; delayLoop++)
  11. {
  12. try
  13. {
  14. db.Messages.Add(
  15. new Message()
  16. {
  17. Text = $"Queued Background Task {guid} has " +
  18. $"written a step. {delayLoop}/3"
  19. });
  20. await db.SaveChangesAsync();
  21. }
  22. catch (Exception ex)
  23. {
  24. _logger.LogError(ex,
  25. "An error occurred writing to the " +
  26. "database. Error: {Message}", ex.Message);
  27. }
  28. await Task.Delay(TimeSpan.FromSeconds(5), token);
  29. }
  30. }
  31. _logger.LogInformation(
  32. "Queued Background Task {Guid} is complete. 3/3", guid);
  33. });
  34. }