@polymorphism-tech/morph-spec 6.0.0 → 7.1.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 +367 -366
- package/docs/CHEATSHEET.md +3 -3
- package/docs/QUICKSTART.md +4 -4
- package/framework/CLAUDE.md +13 -0
- package/framework/MORPH.md +57 -36
- package/framework/agents.json +132 -127
- package/framework/commands/morph-apply.md +4 -21
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-proposal.md +2 -0
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/core/state-sync.js +212 -0
- package/framework/hooks/shared/phase-utils.js +5 -4
- package/framework/hooks/shared/state-reader.js +1 -1
- package/framework/phases.json +1 -1
- package/framework/rubrics/backend.json +41 -36
- package/framework/rules/ai-agents-standards.md +45 -15
- package/framework/rules/morph-workflow.md +15 -34
- package/framework/rules/neon-standards.md +25 -26
- package/framework/schemas/tasks.schema.json +35 -0
- package/framework/skills/core/morph-apply/SKILL.md +154 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +53 -30
- package/framework/skills/core/morph-implement/SKILL.md +25 -14
- package/framework/skills/core/morph-plan/SKILL.md +91 -23
- package/framework/skills/core/morph-proposal/SKILL.md +137 -0
- package/framework/skills/core/morph-review/SKILL.md +43 -7
- package/framework/skills/core/morph-stack-scan/SKILL.md +40 -1
- package/framework/standards/STANDARDS.json +1729 -1134
- package/framework/standards/ai-agents/agent-session.md +160 -0
- package/framework/standards/ai-agents/batching.md +193 -0
- package/framework/standards/ai-agents/context-providers.md +188 -0
- package/framework/standards/ai-agents/mcp-server.md +250 -0
- package/framework/standards/ai-agents/mcp-tools.md +197 -0
- package/framework/standards/ai-agents/middleware-patterns.md +207 -0
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -0
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -0
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -0
- package/framework/standards/ai-agents/modalities-vision.md +201 -0
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -0
- package/framework/standards/ai-agents/production.md +114 -258
- package/framework/standards/ai-agents/prompts-in-db.md +214 -0
- package/framework/standards/ai-agents/providers/model-registry.md +158 -0
- package/framework/standards/ai-agents/rag-custom-pgvector.md +243 -0
- package/framework/standards/ai-agents/service-tier-flex.md +135 -0
- package/framework/standards/ai-agents/setup.md +142 -316
- package/framework/standards/ai-agents/structured-output.md +249 -0
- package/framework/standards/ai-agents/sweet-spot.md +109 -0
- package/framework/standards/ai-agents/workflows.md +114 -284
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +433 -429
- package/framework/standards/backend/api/validation.md +92 -88
- package/framework/standards/backend/database/postgresql/database.md +336 -332
- package/framework/standards/backend/database/vector-search-rag.md +599 -541
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +4 -0
- package/framework/standards/backend/dotnet/result-pattern.md +4 -0
- package/framework/standards/backend/dotnet/scalar-ui.md +176 -172
- package/framework/standards/backend/dotnet/scrutor.md +4 -0
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +278 -274
- package/framework/standards/backend/integrations/asaas/asaas-api.md +260 -216
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +354 -350
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +30 -19
- package/framework/standards/backend/integrations/resend/resend-email.md +400 -385
- package/framework/standards/data/nosql/cache/redis.md +106 -97
- package/framework/standards/data/nosql/cosmos-db.md +127 -118
- package/framework/standards/data/vector-search/rag-chunking.md +113 -104
- package/framework/standards/frontend/design-system/ai-image-generation.md +4 -0
- package/framework/standards/frontend/design-system/animations.md +164 -160
- package/framework/standards/frontend/design-system/naming.md +68 -64
- package/framework/standards/frontend/gsap/gsap-core.md +14 -8
- package/framework/standards/frontend/gsap/gsap-plugins.md +26 -21
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +9 -7
- package/framework/standards/frontend/nextjs/app-router.md +127 -123
- package/framework/standards/frontend/nextjs/components.md +136 -132
- package/framework/standards/frontend/nextjs/data-fetching.md +130 -126
- package/framework/standards/frontend/nextjs/forms.md +134 -128
- package/framework/standards/frontend/nextjs/motion-patterns.md +328 -322
- package/framework/standards/frontend/nextjs/naming-conventions.md +71 -67
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +229 -225
- package/framework/standards/frontend/nextjs/project-structure.md +106 -102
- package/framework/standards/frontend/nextjs/state-management.md +76 -72
- package/framework/standards/frontend/nextjs/testing.md +115 -111
- package/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -192
- package/framework/standards/infrastructure/neon/neon-pgrag.md +6 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +4 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +5 -1
- package/framework/standards/integration/mcp/mcp-tools.md +471 -462
- package/framework/templates/REGISTRY.json +1745 -1239
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -0
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -0
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -0
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -0
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -0
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -0
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -0
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -0
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -0
- package/framework/templates/docs/onboarding.md +4 -4
- package/framework/templates/tasks.template.json +17 -0
- package/package.json +89 -83
- package/src/commands/project/init.js +16 -0
- package/src/commands/validation/validate-feature.js +1 -1
- package/src/core/paths/output-schema.js +6 -6
- package/src/core/state/state-manager.js +1 -1
- package/src/core/templates/template-registry.js +289 -256
- package/src/lib/detectors/design-system-detector.js +1 -1
- package/src/lib/generators/context-generator.js +516 -518
- package/src/lib/generators/scaffold-generator.js +23 -26
- package/src/lib/installers/memory-installer.js +138 -0
- package/src/lib/phase-chain/phase-validator.js +4 -4
- package/src/lib/risk-detector.js +80 -0
- package/src/lib/tasks/tasks-json-parser.js +185 -0
- package/src/scripts/setup-infra.js +25 -0
- package/src/utils/hooks-installer.js +59 -10
- package/framework/standards/infrastructure/neon/neon-semantic-kernel.md +0 -310
- package/framework/templates/integrations/semantic-kernel-neon.cs +0 -125
|
@@ -1,350 +1,354 @@
|
|
|
1
|
-
# Hangfire Background Jobs Standard
|
|
2
|
-
|
|
3
|
-
> **Scope:** universal
|
|
4
|
-
> **Layer:** 2 (on keyword)
|
|
5
|
-
> **Keywords:** hangfire, background job, recurring, fire-and-forget, scheduled
|
|
6
|
-
> **Load When:** hangfire or background job keywords detected
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
x => x.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
public
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Cron
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Cron.
|
|
314
|
-
Cron.
|
|
315
|
-
Cron.
|
|
316
|
-
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
###
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
1
|
+
# Hangfire Background Jobs Standard
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 2 (on keyword)
|
|
5
|
+
> **Keywords:** hangfire, background job, recurring, fire-and-forget, scheduled
|
|
6
|
+
> **Load When:** hangfire or background job keywords detected
|
|
7
|
+
|
|
8
|
+
**Verified against:** .NET 10 + Hangfire 1.8 (Core/SqlServer/AspNetCore). Last-verified: 2026-05-20.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Background job processing for .NET applications with Hangfire.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
Hangfire provides:
|
|
19
|
+
- Fire-and-forget jobs
|
|
20
|
+
- Delayed jobs
|
|
21
|
+
- Recurring jobs (cron schedules)
|
|
22
|
+
- Job continuations
|
|
23
|
+
- Built-in dashboard
|
|
24
|
+
- Retry logic with exponential backoff
|
|
25
|
+
|
|
26
|
+
**Stack:** .NET 10
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Core Principles
|
|
31
|
+
|
|
32
|
+
1. **Stateless Jobs**: Job methods should be stateless and idempotent
|
|
33
|
+
2. **Minimal Dependencies**: Jobs should not depend on HTTP context or scoped services
|
|
34
|
+
3. **Explicit Retries**: Configure retry behavior per job type
|
|
35
|
+
4. **Monitor Dashboard**: Use Hangfire Dashboard for job monitoring
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation & Setup
|
|
40
|
+
|
|
41
|
+
### Install Packages
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
dotnet add package Hangfire.Core
|
|
45
|
+
dotnet add package Hangfire.SqlServer
|
|
46
|
+
dotnet add package Hangfire.AspNetCore
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Program.cs Configuration
|
|
50
|
+
|
|
51
|
+
```csharp
|
|
52
|
+
// Program.cs
|
|
53
|
+
using Hangfire;
|
|
54
|
+
using Hangfire.SqlServer;
|
|
55
|
+
|
|
56
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
57
|
+
|
|
58
|
+
// Add Hangfire services
|
|
59
|
+
builder.Services.AddHangfire(configuration => configuration
|
|
60
|
+
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
|
61
|
+
.UseSimpleAssemblyNameTypeSerializer()
|
|
62
|
+
.UseRecommendedSerializerSettings()
|
|
63
|
+
.UseSqlServerStorage(
|
|
64
|
+
builder.Configuration.GetConnectionString("HangfireConnection"),
|
|
65
|
+
new SqlServerStorageOptions
|
|
66
|
+
{
|
|
67
|
+
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
|
68
|
+
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
|
|
69
|
+
QueuePollInterval = TimeSpan.Zero,
|
|
70
|
+
UseRecommendedIsolationLevel = true,
|
|
71
|
+
DisableGlobalLocks = true
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
builder.Services.AddHangfireServer();
|
|
75
|
+
|
|
76
|
+
var app = builder.Build();
|
|
77
|
+
|
|
78
|
+
// Hangfire Dashboard (protect in production!)
|
|
79
|
+
app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|
80
|
+
{
|
|
81
|
+
Authorization = new[] { new HangfireAuthorizationFilter() }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.Run();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Job Types
|
|
90
|
+
|
|
91
|
+
### Fire-and-Forget Jobs
|
|
92
|
+
|
|
93
|
+
```csharp
|
|
94
|
+
// Enqueue immediately
|
|
95
|
+
BackgroundJob.Enqueue<IEmailService>(x => x.SendWelcomeEmail(userId));
|
|
96
|
+
|
|
97
|
+
// Alternative: Static method
|
|
98
|
+
BackgroundJob.Enqueue(() => Console.WriteLine("Hello, world!"));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Delayed Jobs
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
// Run after 1 hour
|
|
105
|
+
BackgroundJob.Schedule<IReportService>(
|
|
106
|
+
x => x.GenerateMonthlyReport(),
|
|
107
|
+
TimeSpan.FromHours(1));
|
|
108
|
+
|
|
109
|
+
// Run at specific time
|
|
110
|
+
BackgroundJob.Schedule<INotificationService>(
|
|
111
|
+
x => x.SendReminder(userId),
|
|
112
|
+
DateTimeOffset.UtcNow.AddDays(7));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Recurring Jobs
|
|
116
|
+
|
|
117
|
+
```csharp
|
|
118
|
+
// Run daily at 2 AM UTC
|
|
119
|
+
RecurringJob.AddOrUpdate<ICleanupService>(
|
|
120
|
+
"cleanup-old-data",
|
|
121
|
+
x => x.CleanupOldRecords(),
|
|
122
|
+
Cron.Daily(2));
|
|
123
|
+
|
|
124
|
+
// Run every 15 minutes
|
|
125
|
+
RecurringJob.AddOrUpdate<ISyncService>(
|
|
126
|
+
"sync-external-data",
|
|
127
|
+
x => x.SyncData(),
|
|
128
|
+
"*/15 * * * *");
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Job Continuations
|
|
132
|
+
|
|
133
|
+
```csharp
|
|
134
|
+
var parentJobId = BackgroundJob.Enqueue<IOrderService>(
|
|
135
|
+
x => x.ProcessOrder(orderId));
|
|
136
|
+
|
|
137
|
+
BackgroundJob.ContinueJobWith<IEmailService>(
|
|
138
|
+
parentJobId,
|
|
139
|
+
x => x.SendOrderConfirmation(orderId));
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Job Service Pattern
|
|
145
|
+
|
|
146
|
+
```csharp
|
|
147
|
+
// Services/Jobs/IEmailJobService.cs
|
|
148
|
+
public interface IEmailJobService
|
|
149
|
+
{
|
|
150
|
+
Task SendWelcomeEmailAsync(string userId);
|
|
151
|
+
Task SendPasswordResetAsync(string email);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Services/Jobs/EmailJobService.cs
|
|
155
|
+
public class EmailJobService : IEmailJobService
|
|
156
|
+
{
|
|
157
|
+
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
|
158
|
+
private readonly IEmailSender _emailSender;
|
|
159
|
+
|
|
160
|
+
public EmailJobService(
|
|
161
|
+
IDbContextFactory<AppDbContext> dbFactory,
|
|
162
|
+
IEmailSender emailSender)
|
|
163
|
+
{
|
|
164
|
+
_dbFactory = dbFactory;
|
|
165
|
+
_emailSender = emailSender;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public async Task SendWelcomeEmailAsync(string userId)
|
|
169
|
+
{
|
|
170
|
+
// Use IDbContextFactory for background jobs
|
|
171
|
+
await using var db = await _dbFactory.CreateDbContextAsync();
|
|
172
|
+
|
|
173
|
+
var user = await db.Users.FindAsync(userId);
|
|
174
|
+
if (user == null) return;
|
|
175
|
+
|
|
176
|
+
await _emailSender.SendAsync(
|
|
177
|
+
user.Email,
|
|
178
|
+
"Welcome!",
|
|
179
|
+
$"Hello {user.Name}!");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Register in DI
|
|
184
|
+
builder.Services.AddScoped<IEmailJobService, EmailJobService>();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Retry & Error Handling
|
|
190
|
+
|
|
191
|
+
### Custom Retry Attribute
|
|
192
|
+
|
|
193
|
+
```csharp
|
|
194
|
+
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
|
|
195
|
+
public async Task ProcessPaymentAsync(string paymentId)
|
|
196
|
+
{
|
|
197
|
+
// Job logic with automatic retry on failure
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Manual Retry Logic
|
|
202
|
+
|
|
203
|
+
```csharp
|
|
204
|
+
public async Task SendEmailWithRetryAsync(string email)
|
|
205
|
+
{
|
|
206
|
+
int maxRetries = 3;
|
|
207
|
+
int attempt = 0;
|
|
208
|
+
|
|
209
|
+
while (attempt < maxRetries)
|
|
210
|
+
{
|
|
211
|
+
try
|
|
212
|
+
{
|
|
213
|
+
await _emailSender.SendAsync(email, "Subject", "Body");
|
|
214
|
+
return; // Success
|
|
215
|
+
}
|
|
216
|
+
catch (Exception ex)
|
|
217
|
+
{
|
|
218
|
+
attempt++;
|
|
219
|
+
if (attempt >= maxRetries)
|
|
220
|
+
{
|
|
221
|
+
// Log and give up
|
|
222
|
+
_logger.LogError(ex, "Failed to send email after {Attempts} attempts", maxRetries);
|
|
223
|
+
throw;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Dashboard Authorization
|
|
235
|
+
|
|
236
|
+
```csharp
|
|
237
|
+
// Filters/HangfireAuthorizationFilter.cs
|
|
238
|
+
using Hangfire.Dashboard;
|
|
239
|
+
|
|
240
|
+
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
|
|
241
|
+
{
|
|
242
|
+
public bool Authorize(DashboardContext context)
|
|
243
|
+
{
|
|
244
|
+
var httpContext = context.GetHttpContext();
|
|
245
|
+
|
|
246
|
+
// Allow in development
|
|
247
|
+
if (httpContext.Request.Host.Host.Contains("localhost"))
|
|
248
|
+
return true;
|
|
249
|
+
|
|
250
|
+
// Require authenticated user with Admin role
|
|
251
|
+
return httpContext.User.Identity?.IsAuthenticated == true
|
|
252
|
+
&& httpContext.User.IsInRole("Admin");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Best Practices
|
|
260
|
+
|
|
261
|
+
### Avoid Scoped Services
|
|
262
|
+
|
|
263
|
+
```csharp
|
|
264
|
+
// ❌ BAD: Don't inject DbContext directly
|
|
265
|
+
public class BadJobService
|
|
266
|
+
{
|
|
267
|
+
private readonly AppDbContext _db; // Scoped service!
|
|
268
|
+
|
|
269
|
+
public BadJobService(AppDbContext db) => _db = db;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ✅ GOOD: Use IDbContextFactory
|
|
273
|
+
public class GoodJobService
|
|
274
|
+
{
|
|
275
|
+
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
|
276
|
+
|
|
277
|
+
public GoodJobService(IDbContextFactory<AppDbContext> dbFactory)
|
|
278
|
+
=> _dbFactory = dbFactory;
|
|
279
|
+
|
|
280
|
+
public async Task ProcessAsync()
|
|
281
|
+
{
|
|
282
|
+
await using var db = await _dbFactory.CreateDbContextAsync();
|
|
283
|
+
// Use db...
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Idempotent Jobs
|
|
289
|
+
|
|
290
|
+
```csharp
|
|
291
|
+
public async Task ProcessOrderAsync(string orderId)
|
|
292
|
+
{
|
|
293
|
+
await using var db = await _dbFactory.CreateDbContextAsync();
|
|
294
|
+
|
|
295
|
+
var order = await db.Orders.FindAsync(orderId);
|
|
296
|
+
if (order == null || order.Status == OrderStatus.Processed)
|
|
297
|
+
{
|
|
298
|
+
// Already processed or doesn't exist - skip
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Process order...
|
|
303
|
+
order.Status = OrderStatus.Processed;
|
|
304
|
+
await db.SaveChangesAsync();
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Cron Schedule Examples
|
|
311
|
+
|
|
312
|
+
```csharp
|
|
313
|
+
Cron.Minutely() // Every minute
|
|
314
|
+
Cron.Hourly() // Every hour at minute 0
|
|
315
|
+
Cron.Daily() // Every day at 00:00 UTC
|
|
316
|
+
Cron.Daily(14) // Every day at 14:00 UTC
|
|
317
|
+
Cron.Weekly() // Every Sunday at 00:00 UTC
|
|
318
|
+
Cron.Monthly() // First day of month at 00:00 UTC
|
|
319
|
+
Cron.Yearly() // January 1st at 00:00 UTC
|
|
320
|
+
|
|
321
|
+
// Custom cron
|
|
322
|
+
"*/5 * * * *" // Every 5 minutes
|
|
323
|
+
"0 */2 * * *" // Every 2 hours
|
|
324
|
+
"0 9-17 * * 1-5" // 9 AM to 5 PM, Mon-Fri
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Monitoring & Troubleshooting
|
|
330
|
+
|
|
331
|
+
### Dashboard Access
|
|
332
|
+
|
|
333
|
+
Access the dashboard at `/hangfire` (configured in Program.cs).
|
|
334
|
+
|
|
335
|
+
### Common Issues
|
|
336
|
+
|
|
337
|
+
| Issue | Cause | Solution |
|
|
338
|
+
|-------|-------|----------|
|
|
339
|
+
| Jobs not processing | Hangfire server not started | Ensure `AddHangfireServer()` is called |
|
|
340
|
+
| DbContext errors | Using scoped DbContext | Use `IDbContextFactory` |
|
|
341
|
+
| Jobs timing out | Long-running job | Split into smaller jobs with continuations |
|
|
342
|
+
| Memory leaks | Not disposing DbContext | Use `await using` for DbContext |
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## References
|
|
347
|
+
|
|
348
|
+
- [Hangfire Documentation](https://docs.hangfire.io/)
|
|
349
|
+
- [Hangfire Best Practices](https://docs.hangfire.io/en/latest/best-practices.html)
|
|
350
|
+
- [Cron Expression Generator](https://crontab.guru/)
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
*MORPH-SPEC by Polymorphism Tech*
|