skills/csharp-background-service/SKILL.md
Background Service 開發規範:BackgroundService、IHostedService、Channel Queue 模式與生命週期管理。
npx skillsauth add CloudyWing/ai-dotfiles csharp-background-serviceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
當撰寫或審查 .NET 的背景工作、排程任務或佇列處理邏輯時,請自動套用以下規範。
| 基底類別 | 適用情境 |
| --- | --- |
| BackgroundService | 長期執行的迴圈式工作(如輪詢、佇列消費) |
| IHostedService | 需精確控制啟動 / 停止順序的初始化或清理任務 |
BackgroundService 繼承自 IHostedService,提供 ExecuteAsync 抽象方法,適合大多數場景。IHostedService 適合在 StartAsync 中做一次性初始化(如預熱快取、建立連線),不需要持續迴圈。public class OrderSyncService(
IServiceScopeFactory scopeFactory,
ILogger<OrderSyncService> logger
) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
logger.LogInformation("OrderSyncService 已啟動");
while (!stoppingToken.IsCancellationRequested) {
try {
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
IOrderRepository repository = scope.ServiceProvider
.GetRequiredService<IOrderRepository>();
await repository.SyncPendingOrdersAsync(stoppingToken).ConfigureAwait(false);
} catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) {
// 正常停止,不記錄為錯誤
} catch (Exception ex) {
logger.LogError(ex, "訂單同步發生錯誤,將在下次迴圈重試");
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken).ConfigureAwait(false);
}
logger.LogInformation("OrderSyncService 已停止");
}
}
ExecuteAsync 的 stoppingToken 必須傳遞給所有非同步操作與 Task.Delay。ExecuteAsync 中的未處理例外會導致整個 Host 停止(.NET 6+ 預設行為)。迴圈式工作必須在 while 內部 try-catch,記錄錯誤後繼續下次迴圈。stoppingToken 觸發時,Task.Delay 和非同步操作會拋出此例外。使用 when (stoppingToken.IsCancellationRequested) 過濾,避免記錄為錯誤。BackgroundService 註冊為 Singleton,不能直接注入 Scoped 服務(如 DbContext)。必須透過 IServiceScopeFactory 建立 Scope。
// ❌ 錯誤:直接注入 Scoped 服務
public class MyWorker(AppDbContext db) : BackgroundService { }
// ✅ 正確:透過 IServiceScopeFactory
public class MyWorker(IServiceScopeFactory scopeFactory) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
AppDbContext db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
}
}
// ✅ 正確:EF Core 場景優先使用 IDbContextFactory
public class MyWorker(IDbContextFactory<AppDbContext> factory) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
await using AppDbContext db = await factory.CreateDbContextAsync(stoppingToken)
.ConfigureAwait(false);
}
}
public class CacheWarmupService(
IServiceScopeFactory scopeFactory,
IMemoryCache cache,
ILogger<CacheWarmupService> logger
) : IHostedService {
public async Task StartAsync(CancellationToken cancellationToken) {
logger.LogInformation("開始預熱快取");
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
IProductRepository repository = scope.ServiceProvider
.GetRequiredService<IProductRepository>();
IReadOnlyList<Product> products = await repository
.GetAllAsync(cancellationToken)
.ConfigureAwait(false);
cache.Set("products:all", products, TimeSpan.FromHours(1));
logger.LogInformation("快取預熱完成,共 {Count} 筆產品", products.Count);
}
public Task StopAsync(CancellationToken cancellationToken) {
return Task.CompletedTask;
}
}
StartAsync 應盡快完成,不阻塞 Host 啟動。若需要長期作業,在 StartAsync 中啟動 Task 並儲存參考,在 StopAsync 中等待完成。StopAsync 有 timeout(預設 30 秒),必須在時限內完成清理。適用於「生產者-消費者」場景:HTTP 請求快速入隊,背景服務非同步處理。
public class BackgroundTaskQueue {
private readonly Channel<Func<IServiceScopeFactory, CancellationToken, ValueTask>> channel
= Channel.CreateBounded<Func<IServiceScopeFactory, CancellationToken, ValueTask>>(
new BoundedChannelOptions(100) {
FullMode = BoundedChannelFullMode.Wait
});
public async ValueTask EnqueueAsync(
Func<IServiceScopeFactory, CancellationToken, ValueTask> workItem,
CancellationToken cancellationToken
) {
ArgumentNullException.ThrowIfNull(workItem);
await channel.Writer.WriteAsync(workItem, cancellationToken).ConfigureAwait(false);
}
public async ValueTask<Func<IServiceScopeFactory, CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken
) {
return await channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
}
}
public class QueuedHostedService(
BackgroundTaskQueue taskQueue,
IServiceScopeFactory scopeFactory,
ILogger<QueuedHostedService> logger
) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
Func<IServiceScopeFactory, CancellationToken, ValueTask> workItem
= await taskQueue.DequeueAsync(stoppingToken).ConfigureAwait(false);
try {
await workItem(scopeFactory, stoppingToken).ConfigureAwait(false);
} catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) {
// 正常停止
} catch (Exception ex) {
logger.LogError(ex, "佇列工作項目執行失敗");
}
}
}
}
app.MapPost("/api/reports/generate", async (
GenerateReportRequest request,
BackgroundTaskQueue queue,
CancellationToken cancellationToken
) => {
await queue.EnqueueAsync(async (scopeFactory, token) => {
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
IReportService reportService = scope.ServiceProvider
.GetRequiredService<IReportService>();
await reportService.GenerateAsync(request.ReportId, token).ConfigureAwait(false);
}, cancellationToken).ConfigureAwait(false);
return Results.Accepted();
});
builder.Services.AddSingleton<BackgroundTaskQueue>();
builder.Services.AddHostedService<QueuedHostedService>();
public class MetricsCollector(ILogger<MetricsCollector> logger) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)) {
try {
// 收集指標
logger.LogDebug("收集系統指標");
} catch (Exception ex) {
logger.LogError(ex, "指標收集失敗");
}
}
}
}
PeriodicTimer 確保上一次 tick 的工作完成後才開始計時,避免重疊執行。Task.Delay:語意更明確,且不會因工作耗時導致間隔漂移。// 基本註冊
builder.Services.AddHostedService<OrderSyncService>();
builder.Services.AddHostedService<MetricsCollector>();
StartAsync 依序呼叫)。TaskCompletionSource)協調,不依賴註冊順序。// ❌ 禁止:在 Controller 中 Task.Run 長期工作
[HttpPost]
public IActionResult StartWork() {
Task.Run(async () => await DoLongWorkAsync()); // Fire-and-forget,無法追蹤
return Accepted();
}
// ❌ 禁止:在 ExecuteAsync 中不處理例外
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
await ProcessAsync(stoppingToken); // 未 try-catch,例外會終止 Host
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
tools
產生或補齊 .gitattributes,統一行尾處理、二進位識別與 lock files 標記,保留既有自訂偏好。
development
產生或補齊前端 Lint 設定(Prettier + ESLint Flat Config),統一格式化與程式碼品質規則,保留既有自訂偏好。
testing
依據事實校閱報告修改技術文件:以事實層為不可違反的約束,由改檔者負責表達層的措辭與行文連貫。Use when the user asks to apply fact-check results to a document, or to edit a document based on a previously produced fact-check-report.md.
data-ai
多份資料檔整合流程。當需要將兩份以上的資料檔(如 JSON、CSV)合併、補齊闕漏欄位或去重成單一檔案時使用。以 dry-run、筆數核對與抽樣比對降低整合錯誤。