$npx -y skills add wshaddix/dotnet-skills --skill background-servicesHosted services, background jobs, outbox patterns, and graceful shutdown handling for ASP.NET Core applications. Includes patterns for reliable job processing, distributed systems, and lifecycle management. Use when implementing background processing in ASP.NET Core applications,
| 1 | # Background Services in ASP.NET Core |
| 2 | |
| 3 | ## Rationale |
| 4 | |
| 5 | Background services are essential for offloading work from the request pipeline, processing queues, and handling scheduled tasks. Poorly implemented background services can lead to data loss, orphaned jobs, and resource leaks. These patterns ensure reliable, observable, and gracefully degrading background processing in production applications. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## BackgroundService vs IHostedService |
| 10 | |
| 11 | | Feature | `BackgroundService` | `IHostedService` | |
| 12 | |---------|-------------------|-----------------| |
| 13 | | Purpose | Long-running loop or continuous work | Startup/shutdown hooks | |
| 14 | | Methods | Override `ExecuteAsync` | Implement `StartAsync` + `StopAsync` | |
| 15 | | Lifetime | Runs until cancellation or host shutdown | `StartAsync` runs at startup, `StopAsync` at shutdown | |
| 16 | | Use when | Polling queues, processing streams, periodic jobs | Database migrations, cache warming, resource cleanup | |
| 17 | |
| 18 | --- |
| 19 | |
| 20 | ## Pattern 1: Basic BackgroundService Structure |
| 21 | |
| 22 | Use `BackgroundService` base class for consistent lifecycle management and cancellation support. |
| 23 | |
| 24 | ```csharp |
| 25 | public sealed class OrderProcessorWorker( |
| 26 | IServiceScopeFactory scopeFactory, |
| 27 | ILogger<OrderProcessorWorker> logger) : BackgroundService |
| 28 | { |
| 29 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
| 30 | { |
| 31 | logger.LogInformation("Order processor started"); |
| 32 | |
| 33 | while (!stoppingToken.IsCancellationRequested) |
| 34 | { |
| 35 | try |
| 36 | { |
| 37 | using var scope = scopeFactory.CreateScope(); |
| 38 | var processor = scope.ServiceProvider |
| 39 | .GetRequiredService<IOrderProcessor>(); |
| 40 | |
| 41 | var processed = await processor.ProcessPendingAsync(stoppingToken); |
| 42 | |
| 43 | if (processed == 0) |
| 44 | { |
| 45 | await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); |
| 46 | } |
| 47 | } |
| 48 | catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) |
| 49 | { |
| 50 | break; |
| 51 | } |
| 52 | catch (Exception ex) |
| 53 | { |
| 54 | logger.LogError(ex, "Error processing orders"); |
| 55 | await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | logger.LogInformation("Order processor stopped"); |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | // Registration |
| 64 | builder.Services.AddHostedService<OrderProcessorWorker>(); |
| 65 | ``` |
| 66 | |
| 67 | ### Critical Rules for BackgroundService |
| 68 | |
| 69 | 1. **Always create scopes** -- `BackgroundService` is registered as a singleton. Inject `IServiceScopeFactory`, not scoped services directly. |
| 70 | 2. **Always handle exceptions** -- by default, unhandled exceptions in `ExecuteAsync` stop the host. Wrap the loop body in try/catch. |
| 71 | 3. **Always respect the stopping token** -- check `stoppingToken.IsCancellationRequested` and pass the token to all async calls. |
| 72 | 4. **Back off on empty/error** -- avoid tight polling loops that waste CPU. Use `Task.Delay` with the stopping token. |
| 73 | |
| 74 | --- |
| 75 | |
| 76 | ## Pattern 2: IHostedService for Startup/Shutdown Hooks |
| 77 | |
| 78 | ### Startup Hook (Cache Warming, Migrations) |
| 79 | |
| 80 | ```csharp |
| 81 | public sealed class CacheWarmupService( |
| 82 | IServiceScopeFactory scopeFactory, |
| 83 | ILogger<CacheWarmupService> logger) : IHostedService |
| 84 | { |
| 85 | public async Task StartAsync(CancellationToken cancellationToken) |
| 86 | { |
| 87 | logger.LogInformation("Warming caches"); |
| 88 | |
| 89 | using var scope = scopeFactory.CreateScope(); |
| 90 | var cache = scope.ServiceProvider.GetRequiredService<IProductCache>(); |
| 91 | await cache.WarmAsync(cancellationToken); |
| 92 | |
| 93 | logger.LogInformation("Cache warmup complete"); |
| 94 | } |
| 95 | |
| 96 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; |
| 97 | } |
| 98 | ``` |
| 99 | |
| 100 | ### Startup + Shutdown (Resource Lifecycle) |
| 101 | |
| 102 | ```csharp |
| 103 | public sealed class MessageBusService( |
| 104 | ILogger<MessageBusService> logger) : IHostedService |
| 105 | { |
| 106 | private IConnection? _connection; |
| 107 | |
| 108 | public async Task StartAsync(CancellationToken cancellationToken) |
| 109 | { |
| 110 | logger.LogInformation("Connecting to message bus"); |
| 111 | _connection = await CreateConnectionAsync(cancellationToken); |
| 112 | } |
| 113 | |
| 114 | public async Task StopAsync(CancellationToken cancellationToken) |
| 115 | { |
| 116 | logger.LogInformation("Disconnecting from message bus"); |
| 117 | if (_connection is not null) |
| 118 | { |
| 119 | await _connection.CloseAsync(cancellationToken); |
| 120 | _connection = null; |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | private static Task<IConnection> CreateConnectionAsync(CancellationToken ct) => |
| 125 | throw new NotImplementedException(); |
| 126 | } |
| 127 | ``` |
| 128 | |
| 129 | --- |
| 130 | |
| 131 | ## Pattern 3: Hosted Service Lifecycle |