Claude-skill-registry add-background-service
Create BackgroundService implementations for scheduled or polling tasks (project)
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/add-background-service" ~/.claude/skills/majiayu000-claude-skill-registry-add-background-service && rm -rf "$T"
manifest:
skills/data/add-background-service/SKILL.mdsource content
Add Background Service Skill
Create
BackgroundService implementations for scheduled or polling tasks in NovaTune.
Project Context
- Service location:
or worker projectssrc/NovaTuneApp/NovaTuneApp.ApiService/Infrastructure/ - Base class:
Microsoft.Extensions.Hosting.BackgroundService - Pattern: Polling loop with configurable interval
Steps
1. Create Options Class
Location:
src/NovaTuneApp/NovaTuneApp.ApiService/Configuration/{ServiceName}Options.cs
namespace NovaTuneApp.ApiService.Configuration; public class PhysicalDeletionOptions { public const string SectionName = "PhysicalDeletion"; /// <summary> /// Interval between polling cycles. /// Default: 5 minutes. /// </summary> public TimeSpan PollingInterval { get; set; } = TimeSpan.FromMinutes(5); /// <summary> /// Maximum items to process per cycle. /// Default: 50. /// </summary> public int BatchSize { get; set; } = 50; /// <summary> /// Whether the service is enabled. /// Default: true. /// </summary> public bool Enabled { get; set; } = true; }
2. Create Background Service
Location:
src/NovaTuneApp/NovaTuneApp.ApiService/Infrastructure/Services/{ServiceName}Service.cs
using Microsoft.Extensions.Options; namespace NovaTuneApp.ApiService.Infrastructure.Services; /// <summary> /// Background service that processes physical deletions for soft-deleted tracks. /// </summary> public class PhysicalDeletionService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly IOptions<PhysicalDeletionOptions> _options; private readonly ILogger<PhysicalDeletionService> _logger; public PhysicalDeletionService( IServiceProvider serviceProvider, IOptions<PhysicalDeletionOptions> options, ILogger<PhysicalDeletionService> logger) { _serviceProvider = serviceProvider; _options = options; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (!_options.Value.Enabled) { _logger.LogInformation("Physical deletion service is disabled"); return; } _logger.LogInformation( "Physical deletion service starting with {Interval} interval", _options.Value.PollingInterval); while (!stoppingToken.IsCancellationRequested) { try { await ProcessBatchAsync(stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { _logger.LogError(ex, "Error processing physical deletions"); } await Task.Delay(_options.Value.PollingInterval, stoppingToken); } _logger.LogInformation("Physical deletion service stopped"); } private async Task ProcessBatchAsync(CancellationToken ct) { using var scope = _serviceProvider.CreateScope(); var session = scope.ServiceProvider.GetRequiredService<IAsyncDocumentSession>(); var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>(); var itemsToProcess = await session .Query<Track, Tracks_ByScheduledDeletion>() .Where(t => t.Status == TrackStatus.Deleted && t.ScheduledDeletionAt <= DateTimeOffset.UtcNow) .Take(_options.Value.BatchSize) .ToListAsync(ct); if (itemsToProcess.Count == 0) { _logger.LogDebug("No items to process"); return; } _logger.LogInformation("Processing {Count} items for physical deletion", itemsToProcess.Count); foreach (var item in itemsToProcess) { try { await ProcessItemAsync(item, session, storageService, ct); } catch (Exception ex) { _logger.LogError(ex, "Failed to process item {ItemId}", item.Id); // Continue with next item; will retry on next poll } } } private async Task ProcessItemAsync( Track track, IAsyncDocumentSession session, IStorageService storageService, CancellationToken ct) { // Delete storage objects await storageService.DeleteObjectAsync(track.ObjectKey, ct); if (track.WaveformObjectKey is not null) { await storageService.DeleteObjectAsync(track.WaveformObjectKey, ct); } // Delete document session.Delete(track); await session.SaveChangesAsync(ct); _logger.LogInformation( "Physically deleted track {TrackId} for user {UserId}", track.TrackId, track.UserId); } }
3. Register Service
In
Program.cs:
// Register options builder.Services.Configure<PhysicalDeletionOptions>( builder.Configuration.GetSection(PhysicalDeletionOptions.SectionName)); // Register hosted service builder.Services.AddHostedService<PhysicalDeletionService>();
4. Add Configuration
In
appsettings.json:
{ "PhysicalDeletion": { "PollingInterval": "00:05:00", "BatchSize": 50, "Enabled": true } }
Patterns
Simple Polling Service
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); await Task.Delay(_interval, stoppingToken); } }
Service with Startup Delay
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Let the app start up first await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); await Task.Delay(_interval, stoppingToken); } }
Service with Retry Logic
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { const int maxRetries = 5; while (!stoppingToken.IsCancellationRequested) { var retryCount = 0; var success = false; while (!success && retryCount < maxRetries) { try { await DoWorkAsync(stoppingToken); success = true; } catch (Exception ex) when (retryCount < maxRetries - 1) { retryCount++; _logger.LogWarning(ex, "Retry {Attempt}/{Max}", retryCount, maxRetries); await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)), stoppingToken); } } await Task.Delay(_interval, stoppingToken); } }
Best Practices
- Use scoped services - Create scope for each batch/iteration
- Handle cancellation - Always check
stoppingToken - Log appropriately - Info for start/stop, Debug for iterations
- Configure intervals - Use options pattern for configuration
- Process in batches - Avoid loading too many items at once
- Continue on item failure - Don't fail the entire batch
Testing
[Fact] public async Task Service_Should_ProcessItemsInBatches() { // Arrange var options = Options.Create(new PhysicalDeletionOptions { BatchSize = 10, PollingInterval = TimeSpan.FromMilliseconds(100) }); var service = new PhysicalDeletionService( _serviceProvider, options, _logger); // Act using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); await service.StartAsync(cts.Token); await Task.Delay(500); await service.StopAsync(CancellationToken.None); // Assert // Verify items were processed }