@malamute/ai-rules 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,552 @@
1
+ ---
2
+ paths:
3
+ - "**/*Worker.cs"
4
+ - "**/*Service.cs"
5
+ - "**/BackgroundServices/**/*.cs"
6
+ - "**/Workers/**/*.cs"
7
+ - "**/Jobs/**/*.cs"
8
+ ---
9
+
10
+ # .NET Background Services
11
+
12
+ ## Basic Hosted Service
13
+
14
+ ```csharp
15
+ // BackgroundServices/HealthCheckWorker.cs
16
+ public class HealthCheckWorker : BackgroundService
17
+ {
18
+ private readonly ILogger<HealthCheckWorker> _logger;
19
+ private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1);
20
+
21
+ public HealthCheckWorker(ILogger<HealthCheckWorker> logger)
22
+ {
23
+ _logger = logger;
24
+ }
25
+
26
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
27
+ {
28
+ _logger.LogInformation("Health Check Worker starting");
29
+
30
+ while (!stoppingToken.IsCancellationRequested)
31
+ {
32
+ try
33
+ {
34
+ await PerformHealthCheckAsync(stoppingToken);
35
+ }
36
+ catch (Exception ex)
37
+ {
38
+ _logger.LogError(ex, "Error during health check");
39
+ }
40
+
41
+ await Task.Delay(_checkInterval, stoppingToken);
42
+ }
43
+
44
+ _logger.LogInformation("Health Check Worker stopping");
45
+ }
46
+
47
+ private async Task PerformHealthCheckAsync(CancellationToken ct)
48
+ {
49
+ _logger.LogDebug("Performing health check");
50
+ // Health check logic
51
+ }
52
+ }
53
+
54
+ // Registration
55
+ builder.Services.AddHostedService<HealthCheckWorker>();
56
+ ```
57
+
58
+ ## Service with Scoped Dependencies
59
+
60
+ ```csharp
61
+ // BackgroundServices/OrderProcessingWorker.cs
62
+ public class OrderProcessingWorker : BackgroundService
63
+ {
64
+ private readonly IServiceProvider _serviceProvider;
65
+ private readonly ILogger<OrderProcessingWorker> _logger;
66
+
67
+ public OrderProcessingWorker(
68
+ IServiceProvider serviceProvider,
69
+ ILogger<OrderProcessingWorker> logger)
70
+ {
71
+ _serviceProvider = serviceProvider;
72
+ _logger = logger;
73
+ }
74
+
75
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
76
+ {
77
+ while (!stoppingToken.IsCancellationRequested)
78
+ {
79
+ try
80
+ {
81
+ await using var scope = _serviceProvider.CreateAsyncScope();
82
+ var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
83
+ var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
84
+
85
+ await ProcessPendingOrdersAsync(orderService, dbContext, stoppingToken);
86
+ }
87
+ catch (Exception ex)
88
+ {
89
+ _logger.LogError(ex, "Error processing orders");
90
+ }
91
+
92
+ await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
93
+ }
94
+ }
95
+
96
+ private async Task ProcessPendingOrdersAsync(
97
+ IOrderService orderService,
98
+ AppDbContext dbContext,
99
+ CancellationToken ct)
100
+ {
101
+ var pendingOrders = await dbContext.Orders
102
+ .Where(o => o.Status == OrderStatus.Pending)
103
+ .Take(10)
104
+ .ToListAsync(ct);
105
+
106
+ foreach (var order in pendingOrders)
107
+ {
108
+ await orderService.ProcessAsync(order, ct);
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ ## Queue Processing Worker
115
+
116
+ ```csharp
117
+ // BackgroundServices/QueueWorker.cs
118
+ public class QueueWorker : BackgroundService
119
+ {
120
+ private readonly IServiceProvider _serviceProvider;
121
+ private readonly Channel<WorkItem> _queue;
122
+ private readonly ILogger<QueueWorker> _logger;
123
+ private readonly int _maxConcurrency = 5;
124
+
125
+ public QueueWorker(
126
+ IServiceProvider serviceProvider,
127
+ Channel<WorkItem> queue,
128
+ ILogger<QueueWorker> logger)
129
+ {
130
+ _serviceProvider = serviceProvider;
131
+ _queue = queue;
132
+ _logger = logger;
133
+ }
134
+
135
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
136
+ {
137
+ var tasks = new List<Task>();
138
+
139
+ for (int i = 0; i < _maxConcurrency; i++)
140
+ {
141
+ tasks.Add(ProcessQueueAsync(i, stoppingToken));
142
+ }
143
+
144
+ await Task.WhenAll(tasks);
145
+ }
146
+
147
+ private async Task ProcessQueueAsync(int workerId, CancellationToken ct)
148
+ {
149
+ _logger.LogInformation("Worker {WorkerId} starting", workerId);
150
+
151
+ await foreach (var item in _queue.Reader.ReadAllAsync(ct))
152
+ {
153
+ try
154
+ {
155
+ await using var scope = _serviceProvider.CreateAsyncScope();
156
+ var handler = scope.ServiceProvider.GetRequiredService<IWorkItemHandler>();
157
+
158
+ await handler.HandleAsync(item, ct);
159
+
160
+ _logger.LogDebug("Worker {WorkerId} processed item {ItemId}", workerId, item.Id);
161
+ }
162
+ catch (Exception ex)
163
+ {
164
+ _logger.LogError(ex, "Worker {WorkerId} failed to process item {ItemId}", workerId, item.Id);
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // Channel registration
171
+ builder.Services.AddSingleton(Channel.CreateUnbounded<WorkItem>(new UnboundedChannelOptions
172
+ {
173
+ SingleReader = false,
174
+ SingleWriter = false
175
+ }));
176
+ ```
177
+
178
+ ## Timed Background Service
179
+
180
+ ```csharp
181
+ // BackgroundServices/ReportGenerationWorker.cs
182
+ public class ReportGenerationWorker : BackgroundService
183
+ {
184
+ private readonly IServiceProvider _serviceProvider;
185
+ private readonly ILogger<ReportGenerationWorker> _logger;
186
+ private Timer? _timer;
187
+
188
+ public ReportGenerationWorker(
189
+ IServiceProvider serviceProvider,
190
+ ILogger<ReportGenerationWorker> logger)
191
+ {
192
+ _serviceProvider = serviceProvider;
193
+ _logger = logger;
194
+ }
195
+
196
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
197
+ {
198
+ _timer = new Timer(
199
+ DoWork,
200
+ null,
201
+ GetDelayUntilMidnight(),
202
+ TimeSpan.FromDays(1));
203
+
204
+ stoppingToken.Register(() => _timer?.Change(Timeout.Infinite, 0));
205
+
206
+ return Task.CompletedTask;
207
+ }
208
+
209
+ private async void DoWork(object? state)
210
+ {
211
+ try
212
+ {
213
+ await using var scope = _serviceProvider.CreateAsyncScope();
214
+ var reportService = scope.ServiceProvider.GetRequiredService<IReportService>();
215
+
216
+ await reportService.GenerateDailyReportAsync();
217
+
218
+ _logger.LogInformation("Daily report generated successfully");
219
+ }
220
+ catch (Exception ex)
221
+ {
222
+ _logger.LogError(ex, "Failed to generate daily report");
223
+ }
224
+ }
225
+
226
+ private static TimeSpan GetDelayUntilMidnight()
227
+ {
228
+ var now = DateTime.UtcNow;
229
+ var midnight = now.Date.AddDays(1);
230
+ return midnight - now;
231
+ }
232
+
233
+ public override void Dispose()
234
+ {
235
+ _timer?.Dispose();
236
+ base.Dispose();
237
+ }
238
+ }
239
+ ```
240
+
241
+ ## Cron-Based Worker (with NCronTab)
242
+
243
+ ```csharp
244
+ // BackgroundServices/ScheduledWorker.cs
245
+ public class ScheduledWorker : BackgroundService
246
+ {
247
+ private readonly IServiceProvider _serviceProvider;
248
+ private readonly ILogger<ScheduledWorker> _logger;
249
+ private readonly CrontabSchedule _schedule;
250
+
251
+ public ScheduledWorker(
252
+ IServiceProvider serviceProvider,
253
+ IOptions<SchedulerOptions> options,
254
+ ILogger<ScheduledWorker> logger)
255
+ {
256
+ _serviceProvider = serviceProvider;
257
+ _logger = logger;
258
+ _schedule = CrontabSchedule.Parse(options.Value.CronExpression);
259
+ }
260
+
261
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
262
+ {
263
+ while (!stoppingToken.IsCancellationRequested)
264
+ {
265
+ var now = DateTime.UtcNow;
266
+ var nextRun = _schedule.GetNextOccurrence(now);
267
+ var delay = nextRun - now;
268
+
269
+ if (delay > TimeSpan.Zero)
270
+ {
271
+ await Task.Delay(delay, stoppingToken);
272
+ }
273
+
274
+ if (!stoppingToken.IsCancellationRequested)
275
+ {
276
+ await ExecuteJobAsync(stoppingToken);
277
+ }
278
+ }
279
+ }
280
+
281
+ private async Task ExecuteJobAsync(CancellationToken ct)
282
+ {
283
+ try
284
+ {
285
+ await using var scope = _serviceProvider.CreateAsyncScope();
286
+ var job = scope.ServiceProvider.GetRequiredService<IScheduledJob>();
287
+
288
+ await job.ExecuteAsync(ct);
289
+ }
290
+ catch (Exception ex)
291
+ {
292
+ _logger.LogError(ex, "Scheduled job failed");
293
+ }
294
+ }
295
+ }
296
+ ```
297
+
298
+ ## Message Queue Consumer (RabbitMQ)
299
+
300
+ ```csharp
301
+ // BackgroundServices/RabbitMqConsumer.cs
302
+ public class RabbitMqConsumer : BackgroundService
303
+ {
304
+ private readonly IServiceProvider _serviceProvider;
305
+ private readonly ILogger<RabbitMqConsumer> _logger;
306
+ private readonly IConnection _connection;
307
+ private readonly IModel _channel;
308
+
309
+ public RabbitMqConsumer(
310
+ IServiceProvider serviceProvider,
311
+ IOptions<RabbitMqOptions> options,
312
+ ILogger<RabbitMqConsumer> logger)
313
+ {
314
+ _serviceProvider = serviceProvider;
315
+ _logger = logger;
316
+
317
+ var factory = new ConnectionFactory
318
+ {
319
+ HostName = options.Value.Host,
320
+ UserName = options.Value.Username,
321
+ Password = options.Value.Password,
322
+ DispatchConsumersAsync = true
323
+ };
324
+
325
+ _connection = factory.CreateConnection();
326
+ _channel = _connection.CreateModel();
327
+
328
+ _channel.QueueDeclare(
329
+ queue: options.Value.QueueName,
330
+ durable: true,
331
+ exclusive: false,
332
+ autoDelete: false);
333
+ }
334
+
335
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
336
+ {
337
+ var consumer = new AsyncEventingBasicConsumer(_channel);
338
+
339
+ consumer.Received += async (model, ea) =>
340
+ {
341
+ try
342
+ {
343
+ var body = ea.Body.ToArray();
344
+ var message = JsonSerializer.Deserialize<Message>(body);
345
+
346
+ await ProcessMessageAsync(message!, stoppingToken);
347
+
348
+ _channel.BasicAck(ea.DeliveryTag, false);
349
+ }
350
+ catch (Exception ex)
351
+ {
352
+ _logger.LogError(ex, "Error processing message");
353
+ _channel.BasicNack(ea.DeliveryTag, false, true);
354
+ }
355
+ };
356
+
357
+ _channel.BasicConsume(queue: "orders", autoAck: false, consumer: consumer);
358
+
359
+ return Task.CompletedTask;
360
+ }
361
+
362
+ private async Task ProcessMessageAsync(Message message, CancellationToken ct)
363
+ {
364
+ await using var scope = _serviceProvider.CreateAsyncScope();
365
+ var handler = scope.ServiceProvider.GetRequiredService<IMessageHandler>();
366
+
367
+ await handler.HandleAsync(message, ct);
368
+ }
369
+
370
+ public override void Dispose()
371
+ {
372
+ _channel?.Close();
373
+ _connection?.Close();
374
+ base.Dispose();
375
+ }
376
+ }
377
+ ```
378
+
379
+ ## Startup/Shutdown Tasks
380
+
381
+ ```csharp
382
+ // BackgroundServices/StartupTask.cs
383
+ public class DatabaseMigrationTask : IHostedService
384
+ {
385
+ private readonly IServiceProvider _serviceProvider;
386
+ private readonly ILogger<DatabaseMigrationTask> _logger;
387
+
388
+ public DatabaseMigrationTask(
389
+ IServiceProvider serviceProvider,
390
+ ILogger<DatabaseMigrationTask> logger)
391
+ {
392
+ _serviceProvider = serviceProvider;
393
+ _logger = logger;
394
+ }
395
+
396
+ public async Task StartAsync(CancellationToken cancellationToken)
397
+ {
398
+ _logger.LogInformation("Running database migrations");
399
+
400
+ await using var scope = _serviceProvider.CreateAsyncScope();
401
+ var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
402
+
403
+ await dbContext.Database.MigrateAsync(cancellationToken);
404
+
405
+ _logger.LogInformation("Database migrations completed");
406
+ }
407
+
408
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
409
+ }
410
+
411
+ // Graceful shutdown
412
+ public class GracefulShutdownService : IHostedService
413
+ {
414
+ private readonly ILogger<GracefulShutdownService> _logger;
415
+ private readonly IHostApplicationLifetime _lifetime;
416
+
417
+ public GracefulShutdownService(
418
+ ILogger<GracefulShutdownService> logger,
419
+ IHostApplicationLifetime lifetime)
420
+ {
421
+ _logger = logger;
422
+ _lifetime = lifetime;
423
+ }
424
+
425
+ public Task StartAsync(CancellationToken cancellationToken)
426
+ {
427
+ _lifetime.ApplicationStopping.Register(OnStopping);
428
+ _lifetime.ApplicationStopped.Register(OnStopped);
429
+ return Task.CompletedTask;
430
+ }
431
+
432
+ private void OnStopping()
433
+ {
434
+ _logger.LogInformation("Application is shutting down...");
435
+ // Complete in-flight requests, close connections
436
+ }
437
+
438
+ private void OnStopped()
439
+ {
440
+ _logger.LogInformation("Application has stopped");
441
+ }
442
+
443
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
444
+ }
445
+ ```
446
+
447
+ ## Health Checks for Workers
448
+
449
+ ```csharp
450
+ // Health/WorkerHealthCheck.cs
451
+ public class WorkerHealthCheck : IHealthCheck
452
+ {
453
+ private readonly IWorkerStatusService _workerStatus;
454
+
455
+ public WorkerHealthCheck(IWorkerStatusService workerStatus)
456
+ {
457
+ _workerStatus = workerStatus;
458
+ }
459
+
460
+ public Task<HealthCheckResult> CheckHealthAsync(
461
+ HealthCheckContext context,
462
+ CancellationToken cancellationToken = default)
463
+ {
464
+ var status = _workerStatus.GetStatus();
465
+
466
+ if (status.IsHealthy)
467
+ {
468
+ return Task.FromResult(HealthCheckResult.Healthy(
469
+ $"Last run: {status.LastRunTime}, Items processed: {status.ItemsProcessed}"));
470
+ }
471
+
472
+ return Task.FromResult(HealthCheckResult.Unhealthy(
473
+ $"Worker unhealthy: {status.ErrorMessage}"));
474
+ }
475
+ }
476
+
477
+ // Registration
478
+ builder.Services.AddHealthChecks()
479
+ .AddCheck<WorkerHealthCheck>("worker");
480
+ ```
481
+
482
+ ## Anti-patterns
483
+
484
+ ```csharp
485
+ // BAD: Using scoped services without scope
486
+ public class BadWorker : BackgroundService
487
+ {
488
+ private readonly IOrderService _orderService; // Scoped service!
489
+
490
+ public BadWorker(IOrderService orderService)
491
+ {
492
+ _orderService = orderService; // Same instance forever!
493
+ }
494
+ }
495
+
496
+ // GOOD: Create scope for scoped services
497
+ await using var scope = _serviceProvider.CreateAsyncScope();
498
+ var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
499
+
500
+ // BAD: No error handling
501
+ protected override async Task ExecuteAsync(CancellationToken ct)
502
+ {
503
+ while (!ct.IsCancellationRequested)
504
+ {
505
+ await ProcessAsync(); // Exception kills the worker!
506
+ }
507
+ }
508
+
509
+ // GOOD: Proper error handling
510
+ try
511
+ {
512
+ await ProcessAsync();
513
+ }
514
+ catch (Exception ex)
515
+ {
516
+ _logger.LogError(ex, "Processing failed");
517
+ await Task.Delay(TimeSpan.FromSeconds(5), ct);
518
+ }
519
+
520
+ // BAD: Not respecting cancellation
521
+ protected override async Task ExecuteAsync(CancellationToken ct)
522
+ {
523
+ while (true) // Never stops!
524
+ {
525
+ await Task.Delay(1000);
526
+ }
527
+ }
528
+
529
+ // GOOD: Check cancellation token
530
+ while (!ct.IsCancellationRequested)
531
+ {
532
+ await Task.Delay(1000, ct);
533
+ }
534
+
535
+ // BAD: Tight polling loop
536
+ while (!ct.IsCancellationRequested)
537
+ {
538
+ var item = await GetNextItemAsync();
539
+ if (item == null) continue; // CPU spinning!
540
+ }
541
+
542
+ // GOOD: Use delay or blocking read
543
+ while (!ct.IsCancellationRequested)
544
+ {
545
+ var item = await GetNextItemAsync();
546
+ if (item == null)
547
+ {
548
+ await Task.Delay(TimeSpan.FromSeconds(1), ct);
549
+ continue;
550
+ }
551
+ }
552
+ ```