@polymorphism-tech/morph-spec 4.10.7 → 4.10.9
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/CLAUDE.md +3 -3
- package/LICENSE +71 -71
- package/README.md +379 -379
- package/bin/morph-spec.js +1 -1
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +206 -206
- package/framework/agents/README.md +14 -14
- package/framework/agents/architecture/standards-architect.md +159 -159
- package/framework/agents/frontend/blazor-builder.md +13 -0
- package/framework/agents/frontend/css-specialist.md +87 -0
- package/framework/agents/frontend/nextjs-expert.md +13 -0
- package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
- package/framework/agents/frontend/ui-ux-designer.md +80 -10
- package/framework/agents/infrastructure/azure-architect.md +149 -149
- package/framework/agents/infrastructure/infra-architect.md +48 -48
- package/framework/agents.json +2268 -2204
- package/framework/hooks/README.md +204 -204
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
- package/framework/hooks/shared/phase-utils.js +139 -139
- package/framework/hooks/shared/worktree-helpers.js +30 -8
- package/framework/rules/csharp-standards.md +13 -13
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +16 -16
- package/framework/rules/testing-standards.md +14 -14
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
- package/framework/skills/level-0-meta/morph-init/SKILL.md +136 -3
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -500
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -262
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +457 -323
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2040 -2040
- package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
- package/framework/templates/README.md +814 -814
- package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
- package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
- package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
- package/framework/templates/code/dotnet/contracts/README.md +74 -74
- package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
- package/framework/templates/code/dotnet/frontend/component.razor +239 -239
- package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
- package/framework/templates/code/dotnet/jobs/job.cs +171 -171
- package/framework/templates/code/dotnet/test.cs +239 -239
- package/framework/templates/code/sql/rls-policy.sql +57 -57
- package/framework/templates/code/sql/supabase-migration.sql +100 -100
- package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
- package/framework/templates/code/typescript/contracts.ts +168 -168
- package/framework/templates/context/CONTEXT.md +181 -181
- package/framework/templates/context/design-system.md +211 -0
- package/framework/templates/docs/proposal.md +182 -182
- package/framework/templates/docs/user-stories.md +34 -34
- package/framework/templates/examples/design-system-examples.md +357 -357
- package/framework/templates/examples/spec-examples.md +90 -90
- package/framework/templates/feature/decisions.md +187 -187
- package/framework/templates/feature/recap.md +146 -146
- package/framework/templates/feature/tasks.md +199 -199
- package/framework/templates/infrastructure/azure/README.md +286 -286
- package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
- package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
- package/package.json +82 -82
- package/src/commands/agents/index.js +13 -13
- package/src/commands/project/install-plugin-cmd.js +28 -28
- package/src/commands/project/setup-infra-cmd.js +12 -12
- package/src/commands/project/worktree.js +58 -2
- package/src/commands/tasks/index.js +4 -4
- package/src/commands/templates/index.js +5 -5
- package/src/commands/utils/index.js +3 -3
- package/src/commands/validation/index.js +4 -4
- package/src/core/state/phase-state-machine.js +217 -217
- package/src/lib/detectors/design-system-detector.js +73 -31
- package/src/lib/generators/design-system-generator.js +298 -298
- package/src/lib/generators/index.js +10 -10
- package/src/lib/generators/metadata-extractor.js +387 -387
- package/src/lib/stack-filter.js +65 -65
- package/src/lib/standards/index.js +7 -7
- package/src/lib/standards/standards-context-injector.js +302 -302
- package/src/lib/validators/architecture/architecture-validator.js +387 -387
- package/src/lib/validators/architecture/index.js +7 -7
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
- package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
- package/src/lib/validators/blazor/blazor-validator.js +369 -369
- package/src/lib/validators/blazor/index.js +9 -9
- package/src/lib/validators/content/content-validator.js +287 -287
- package/src/lib/validators/content/index.js +7 -7
- package/src/lib/validators/contracts/index.js +7 -7
- package/src/lib/validators/css/css-validator.js +352 -352
- package/src/lib/validators/css/index.js +7 -7
- package/src/lib/validators/design-system/design-system-validator.js +231 -231
- package/src/lib/validators/design-system/index.js +7 -7
- package/src/lib/validators/nextjs/index.js +6 -6
- package/src/lib/validators/nextjs/next-component-validator.js +183 -183
- package/src/lib/validators/packages/index.js +7 -7
- package/src/lib/validators/packages/package-validator.js +360 -360
- package/src/lib/validators/shared/index.js +12 -12
- package/src/lib/validators/shared/issue-counter.js +18 -18
- package/src/lib/validators/shared/result-formatter.js +124 -124
- package/src/lib/validators/ui/index.js +7 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
- package/src/scripts/global-install.js +58 -58
- package/src/scripts/install-plugin.js +126 -126
- package/src/utils/claude-md-injector.js +90 -90
- package/src/utils/file-copier.js +200 -196
- package/src/utils/hooks-installer.js +556 -494
- package/src/utils/logger.js +32 -32
- package/src/utils/skills-installer.js +178 -178
- package/src/utils/version-checker.js +175 -175
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// HANGFIRE JOB TEMPLATE
|
|
3
|
-
// Generated by MORPH Framework
|
|
4
|
-
// ============================================================
|
|
5
|
-
|
|
6
|
-
using Hangfire;
|
|
7
|
-
using Microsoft.Extensions.Logging;
|
|
8
|
-
|
|
9
|
-
namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Jobs;
|
|
10
|
-
|
|
11
|
-
/// <summary>
|
|
12
|
-
/// Background job for processing {{pascalCase FEATURE_NAME}}.
|
|
13
|
-
/// Uses Hangfire for scheduling and retry handling.
|
|
14
|
-
/// </summary>
|
|
15
|
-
public class {{pascalCase FEATURE_NAME}}ProcessorJob(
|
|
16
|
-
I{{pascalCase FEATURE_NAME}}Service service,
|
|
17
|
-
I{{pascalCase FEATURE_NAME}}AnalyzerAgent analyzer,
|
|
18
|
-
ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob
|
|
19
|
-
{
|
|
20
|
-
/// <summary>
|
|
21
|
-
/// Executes the {{pascalCase FEATURE_NAME}} processing job.
|
|
22
|
-
/// </summary>
|
|
23
|
-
/// <param name="id">The {{pascalCase FEATURE_NAME}} ID to process</param>
|
|
24
|
-
/// <param name="cancellationToken">Cancellation token</param>
|
|
25
|
-
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
|
|
26
|
-
[Queue("default")]
|
|
27
|
-
[JobDisplayName("{{pascalCase FEATURE_NAME}} Processing - ID: {0}")]
|
|
28
|
-
public async Task ExecuteAsync(int id, CancellationToken cancellationToken)
|
|
29
|
-
{
|
|
30
|
-
logger.LogInformation("Starting {{pascalCase FEATURE_NAME}} processing for ID {Id}", id);
|
|
31
|
-
|
|
32
|
-
try
|
|
33
|
-
{
|
|
34
|
-
// Get the entity
|
|
35
|
-
var item = await service.GetByIdAsync(id, cancellationToken);
|
|
36
|
-
if (item is null)
|
|
37
|
-
{
|
|
38
|
-
logger.LogWarning("{{pascalCase FEATURE_NAME}} with ID {Id} not found, skipping", id);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Check if already processed
|
|
43
|
-
if (item.Status == {{pascalCase FEATURE_NAME}}Status.Completed)
|
|
44
|
-
{
|
|
45
|
-
logger.LogInformation("{{pascalCase FEATURE_NAME}} {Id} already completed, skipping", id);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Perform analysis (if applicable)
|
|
50
|
-
var analysisData = new {{pascalCase FEATURE_NAME}}Data(item.Name);
|
|
51
|
-
var analysis = await analyzer.AnalyzeAsync(analysisData, cancellationToken);
|
|
52
|
-
|
|
53
|
-
logger.LogInformation(
|
|
54
|
-
"{{pascalCase FEATURE_NAME}} {Id} analyzed. Confidence: {Confidence:P0}",
|
|
55
|
-
id, analysis.ConfidenceScore);
|
|
56
|
-
|
|
57
|
-
// Update status
|
|
58
|
-
// Note: You might need to add a method to update with analysis results
|
|
59
|
-
// await service.CompleteWithAnalysisAsync(id, analysis, cancellationToken);
|
|
60
|
-
|
|
61
|
-
logger.LogInformation("Completed {{pascalCase FEATURE_NAME}} processing for ID {Id}", id);
|
|
62
|
-
}
|
|
63
|
-
catch (Exception ex)
|
|
64
|
-
{
|
|
65
|
-
logger.LogError(ex, "Failed to process {{pascalCase FEATURE_NAME}} {Id}", id);
|
|
66
|
-
throw; // Re-throw to trigger Hangfire retry
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ============================================================
|
|
72
|
-
// RECURRING JOB CONFIGURATION
|
|
73
|
-
// ============================================================
|
|
74
|
-
//
|
|
75
|
-
// For scheduled/recurring jobs, configure in Program.cs:
|
|
76
|
-
//
|
|
77
|
-
// // Run every hour
|
|
78
|
-
// RecurringJob.AddOrUpdate<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
79
|
-
// "{{kebabCase FEATURE_NAME}}-processor",
|
|
80
|
-
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
81
|
-
// Cron.Hourly);
|
|
82
|
-
//
|
|
83
|
-
// // Run daily at midnight
|
|
84
|
-
// RecurringJob.AddOrUpdate<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
85
|
-
// "{{kebabCase FEATURE_NAME}}-daily-processor",
|
|
86
|
-
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
87
|
-
// Cron.Daily);
|
|
88
|
-
//
|
|
89
|
-
// // Custom cron expression (every 15 minutes)
|
|
90
|
-
// RecurringJob.AddOrUpdate<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
91
|
-
// "{{kebabCase FEATURE_NAME}}-frequent-processor",
|
|
92
|
-
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
93
|
-
// "*/15 * * * *");
|
|
94
|
-
//
|
|
95
|
-
// ============================================================
|
|
96
|
-
|
|
97
|
-
// ============================================================
|
|
98
|
-
// BATCH JOB TEMPLATE
|
|
99
|
-
// ============================================================
|
|
100
|
-
|
|
101
|
-
/// <summary>
|
|
102
|
-
/// Batch job for processing multiple {{pascalCase FEATURE_NAME}}s.
|
|
103
|
-
/// </summary>
|
|
104
|
-
public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
|
|
105
|
-
I{{pascalCase FEATURE_NAME}}Service service,
|
|
106
|
-
I{{pascalCase FEATURE_NAME}}ProcessorJob itemProcessor,
|
|
107
|
-
ILogger<{{pascalCase FEATURE_NAME}}BatchProcessorJob> logger)
|
|
108
|
-
{
|
|
109
|
-
/// <summary>
|
|
110
|
-
/// Processes all pending {{pascalCase FEATURE_NAME}}s.
|
|
111
|
-
/// </summary>
|
|
112
|
-
[AutomaticRetry(Attempts = 1)]
|
|
113
|
-
[Queue("batch")]
|
|
114
|
-
[JobDisplayName("{{pascalCase FEATURE_NAME}} Batch Processing")]
|
|
115
|
-
public async Task ProcessAllPendingAsync(CancellationToken cancellationToken)
|
|
116
|
-
{
|
|
117
|
-
logger.LogInformation("Starting batch processing for pending {{pascalCase FEATURE_NAME}}s");
|
|
118
|
-
|
|
119
|
-
var items = await service.GetAllAsync(cancellationToken);
|
|
120
|
-
var pending = items.Where(x => x.Status == {{pascalCase FEATURE_NAME}}Status.Pending).ToList();
|
|
121
|
-
|
|
122
|
-
logger.LogInformation("Found {Count} pending {{pascalCase FEATURE_NAME}}s to process", pending.Count);
|
|
123
|
-
|
|
124
|
-
foreach (var item in pending)
|
|
125
|
-
{
|
|
126
|
-
// Enqueue individual processing jobs
|
|
127
|
-
BackgroundJob.Enqueue<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
128
|
-
job => job.ExecuteAsync(item.Id, CancellationToken.None));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
logger.LogInformation("Enqueued {Count} processing jobs", pending.Count);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ============================================================
|
|
136
|
-
// HANGFIRE CONFIGURATION
|
|
137
|
-
// ============================================================
|
|
138
|
-
//
|
|
139
|
-
// In Program.cs:
|
|
140
|
-
//
|
|
141
|
-
// // Add Hangfire services
|
|
142
|
-
// builder.Services.AddHangfire(config => config
|
|
143
|
-
// .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
|
144
|
-
// .UseSimpleAssemblyNameTypeSerializer()
|
|
145
|
-
// .UseRecommendedSerializerSettings()
|
|
146
|
-
// .UseSqlServerStorage(connectionString, new SqlServerStorageOptions
|
|
147
|
-
// {
|
|
148
|
-
// CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
|
149
|
-
// SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
|
|
150
|
-
// QueuePollInterval = TimeSpan.Zero,
|
|
151
|
-
// UseRecommendedIsolationLevel = true,
|
|
152
|
-
// DisableGlobalLocks = true
|
|
153
|
-
// }));
|
|
154
|
-
//
|
|
155
|
-
// builder.Services.AddHangfireServer(options =>
|
|
156
|
-
// {
|
|
157
|
-
// options.Queues = new[] { "default", "batch" };
|
|
158
|
-
// options.WorkerCount = Environment.ProcessorCount * 2;
|
|
159
|
-
// });
|
|
160
|
-
//
|
|
161
|
-
// // Register jobs
|
|
162
|
-
// builder.Services.AddScoped<I{{pascalCase FEATURE_NAME}}ProcessorJob, {{pascalCase FEATURE_NAME}}ProcessorJob>();
|
|
163
|
-
// builder.Services.AddScoped<{{pascalCase FEATURE_NAME}}BatchProcessorJob>();
|
|
164
|
-
//
|
|
165
|
-
// // In app pipeline
|
|
166
|
-
// app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|
167
|
-
// {
|
|
168
|
-
// Authorization = new[] { new HangfireAuthorizationFilter() }
|
|
169
|
-
// });
|
|
170
|
-
//
|
|
171
|
-
// ============================================================
|
|
1
|
+
// ============================================================
|
|
2
|
+
// HANGFIRE JOB TEMPLATE
|
|
3
|
+
// Generated by MORPH Framework
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
using Hangfire;
|
|
7
|
+
using Microsoft.Extensions.Logging;
|
|
8
|
+
|
|
9
|
+
namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Jobs;
|
|
10
|
+
|
|
11
|
+
/// <summary>
|
|
12
|
+
/// Background job for processing {{pascalCase FEATURE_NAME}}.
|
|
13
|
+
/// Uses Hangfire for scheduling and retry handling.
|
|
14
|
+
/// </summary>
|
|
15
|
+
public class {{pascalCase FEATURE_NAME}}ProcessorJob(
|
|
16
|
+
I{{pascalCase FEATURE_NAME}}Service service,
|
|
17
|
+
I{{pascalCase FEATURE_NAME}}AnalyzerAgent analyzer,
|
|
18
|
+
ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob
|
|
19
|
+
{
|
|
20
|
+
/// <summary>
|
|
21
|
+
/// Executes the {{pascalCase FEATURE_NAME}} processing job.
|
|
22
|
+
/// </summary>
|
|
23
|
+
/// <param name="id">The {{pascalCase FEATURE_NAME}} ID to process</param>
|
|
24
|
+
/// <param name="cancellationToken">Cancellation token</param>
|
|
25
|
+
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
|
|
26
|
+
[Queue("default")]
|
|
27
|
+
[JobDisplayName("{{pascalCase FEATURE_NAME}} Processing - ID: {0}")]
|
|
28
|
+
public async Task ExecuteAsync(int id, CancellationToken cancellationToken)
|
|
29
|
+
{
|
|
30
|
+
logger.LogInformation("Starting {{pascalCase FEATURE_NAME}} processing for ID {Id}", id);
|
|
31
|
+
|
|
32
|
+
try
|
|
33
|
+
{
|
|
34
|
+
// Get the entity
|
|
35
|
+
var item = await service.GetByIdAsync(id, cancellationToken);
|
|
36
|
+
if (item is null)
|
|
37
|
+
{
|
|
38
|
+
logger.LogWarning("{{pascalCase FEATURE_NAME}} with ID {Id} not found, skipping", id);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if already processed
|
|
43
|
+
if (item.Status == {{pascalCase FEATURE_NAME}}Status.Completed)
|
|
44
|
+
{
|
|
45
|
+
logger.LogInformation("{{pascalCase FEATURE_NAME}} {Id} already completed, skipping", id);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Perform analysis (if applicable)
|
|
50
|
+
var analysisData = new {{pascalCase FEATURE_NAME}}Data(item.Name);
|
|
51
|
+
var analysis = await analyzer.AnalyzeAsync(analysisData, cancellationToken);
|
|
52
|
+
|
|
53
|
+
logger.LogInformation(
|
|
54
|
+
"{{pascalCase FEATURE_NAME}} {Id} analyzed. Confidence: {Confidence:P0}",
|
|
55
|
+
id, analysis.ConfidenceScore);
|
|
56
|
+
|
|
57
|
+
// Update status
|
|
58
|
+
// Note: You might need to add a method to update with analysis results
|
|
59
|
+
// await service.CompleteWithAnalysisAsync(id, analysis, cancellationToken);
|
|
60
|
+
|
|
61
|
+
logger.LogInformation("Completed {{pascalCase FEATURE_NAME}} processing for ID {Id}", id);
|
|
62
|
+
}
|
|
63
|
+
catch (Exception ex)
|
|
64
|
+
{
|
|
65
|
+
logger.LogError(ex, "Failed to process {{pascalCase FEATURE_NAME}} {Id}", id);
|
|
66
|
+
throw; // Re-throw to trigger Hangfire retry
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================
|
|
72
|
+
// RECURRING JOB CONFIGURATION
|
|
73
|
+
// ============================================================
|
|
74
|
+
//
|
|
75
|
+
// For scheduled/recurring jobs, configure in Program.cs:
|
|
76
|
+
//
|
|
77
|
+
// // Run every hour
|
|
78
|
+
// RecurringJob.AddOrUpdate<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
79
|
+
// "{{kebabCase FEATURE_NAME}}-processor",
|
|
80
|
+
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
81
|
+
// Cron.Hourly);
|
|
82
|
+
//
|
|
83
|
+
// // Run daily at midnight
|
|
84
|
+
// RecurringJob.AddOrUpdate<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
85
|
+
// "{{kebabCase FEATURE_NAME}}-daily-processor",
|
|
86
|
+
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
87
|
+
// Cron.Daily);
|
|
88
|
+
//
|
|
89
|
+
// // Custom cron expression (every 15 minutes)
|
|
90
|
+
// RecurringJob.AddOrUpdate<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
91
|
+
// "{{kebabCase FEATURE_NAME}}-frequent-processor",
|
|
92
|
+
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
93
|
+
// "*/15 * * * *");
|
|
94
|
+
//
|
|
95
|
+
// ============================================================
|
|
96
|
+
|
|
97
|
+
// ============================================================
|
|
98
|
+
// BATCH JOB TEMPLATE
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
/// <summary>
|
|
102
|
+
/// Batch job for processing multiple {{pascalCase FEATURE_NAME}}s.
|
|
103
|
+
/// </summary>
|
|
104
|
+
public class {{pascalCase FEATURE_NAME}}BatchProcessorJob(
|
|
105
|
+
I{{pascalCase FEATURE_NAME}}Service service,
|
|
106
|
+
I{{pascalCase FEATURE_NAME}}ProcessorJob itemProcessor,
|
|
107
|
+
ILogger<{{pascalCase FEATURE_NAME}}BatchProcessorJob> logger)
|
|
108
|
+
{
|
|
109
|
+
/// <summary>
|
|
110
|
+
/// Processes all pending {{pascalCase FEATURE_NAME}}s.
|
|
111
|
+
/// </summary>
|
|
112
|
+
[AutomaticRetry(Attempts = 1)]
|
|
113
|
+
[Queue("batch")]
|
|
114
|
+
[JobDisplayName("{{pascalCase FEATURE_NAME}} Batch Processing")]
|
|
115
|
+
public async Task ProcessAllPendingAsync(CancellationToken cancellationToken)
|
|
116
|
+
{
|
|
117
|
+
logger.LogInformation("Starting batch processing for pending {{pascalCase FEATURE_NAME}}s");
|
|
118
|
+
|
|
119
|
+
var items = await service.GetAllAsync(cancellationToken);
|
|
120
|
+
var pending = items.Where(x => x.Status == {{pascalCase FEATURE_NAME}}Status.Pending).ToList();
|
|
121
|
+
|
|
122
|
+
logger.LogInformation("Found {Count} pending {{pascalCase FEATURE_NAME}}s to process", pending.Count);
|
|
123
|
+
|
|
124
|
+
foreach (var item in pending)
|
|
125
|
+
{
|
|
126
|
+
// Enqueue individual processing jobs
|
|
127
|
+
BackgroundJob.Enqueue<I{{pascalCase FEATURE_NAME}}ProcessorJob>(
|
|
128
|
+
job => job.ExecuteAsync(item.Id, CancellationToken.None));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.LogInformation("Enqueued {Count} processing jobs", pending.Count);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================
|
|
136
|
+
// HANGFIRE CONFIGURATION
|
|
137
|
+
// ============================================================
|
|
138
|
+
//
|
|
139
|
+
// In Program.cs:
|
|
140
|
+
//
|
|
141
|
+
// // Add Hangfire services
|
|
142
|
+
// builder.Services.AddHangfire(config => config
|
|
143
|
+
// .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
|
144
|
+
// .UseSimpleAssemblyNameTypeSerializer()
|
|
145
|
+
// .UseRecommendedSerializerSettings()
|
|
146
|
+
// .UseSqlServerStorage(connectionString, new SqlServerStorageOptions
|
|
147
|
+
// {
|
|
148
|
+
// CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
|
149
|
+
// SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
|
|
150
|
+
// QueuePollInterval = TimeSpan.Zero,
|
|
151
|
+
// UseRecommendedIsolationLevel = true,
|
|
152
|
+
// DisableGlobalLocks = true
|
|
153
|
+
// }));
|
|
154
|
+
//
|
|
155
|
+
// builder.Services.AddHangfireServer(options =>
|
|
156
|
+
// {
|
|
157
|
+
// options.Queues = new[] { "default", "batch" };
|
|
158
|
+
// options.WorkerCount = Environment.ProcessorCount * 2;
|
|
159
|
+
// });
|
|
160
|
+
//
|
|
161
|
+
// // Register jobs
|
|
162
|
+
// builder.Services.AddScoped<I{{pascalCase FEATURE_NAME}}ProcessorJob, {{pascalCase FEATURE_NAME}}ProcessorJob>();
|
|
163
|
+
// builder.Services.AddScoped<{{pascalCase FEATURE_NAME}}BatchProcessorJob>();
|
|
164
|
+
//
|
|
165
|
+
// // In app pipeline
|
|
166
|
+
// app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|
167
|
+
// {
|
|
168
|
+
// Authorization = new[] { new HangfireAuthorizationFilter() }
|
|
169
|
+
// });
|
|
170
|
+
//
|
|
171
|
+
// ============================================================
|