@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.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /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
|
+
```
|