// ============================================================ // HANGFIRE JOB TEMPLATE (VSA Pattern — ApplicationDbContext direto) // Generated by MORPH Framework // ============================================================ using Hangfire; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using {{pascalCase PROJECT_NAME}}.Database; namespace {{pascalCase PROJECT_NAME}}.Features.{{pascalCase FEATURE_NAME}}.Jobs; /// /// Background job for processing {{pascalCase FEATURE_NAME}}. /// Uses Hangfire for scheduling and retry handling. /// Injects ApplicationDbContext directly (no service layer, no generic repository). /// public class {{pascalCase FEATURE_NAME}}ProcessorJob( ApplicationDbContext db, I{{pascalCase FEATURE_NAME}}AnalyzerAgent analyzer, ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob { /// /// Executes the {{pascalCase FEATURE_NAME}} processing job. /// /// The {{pascalCase FEATURE_NAME}} ID to process /// Cancellation token [AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })] [Queue("default")] [JobDisplayName("{{pascalCase FEATURE_NAME}} Processing - ID: {0}")] public async Task ExecuteAsync(int id, CancellationToken cancellationToken) { logger.LogInformation("Starting {{pascalCase FEATURE_NAME}} processing for ID {Id}", id); try { // Get the entity directly from the DbSet var entity = await db.Set<{{pascalCase FEATURE_NAME}}>().FindAsync(new object[] { id }, cancellationToken); if (entity is null) { logger.LogWarning("{{pascalCase FEATURE_NAME}} with ID {Id} not found, skipping", id); return; } // Check if already processed if (entity.Status == {{pascalCase FEATURE_NAME}}Status.Completed) { logger.LogInformation("{{pascalCase FEATURE_NAME}} {Id} already completed, skipping", id); return; } // Perform MAF agent analysis (if applicable) var analysisData = new {{pascalCase FEATURE_NAME}}Data(entity.Name); var analysis = await analyzer.AnalyzeAsync(analysisData, cancellationToken); logger.LogInformation( "{{pascalCase FEATURE_NAME}} {Id} analyzed. Confidence: {Confidence:P0}", id, analysis.ConfidenceScore); // Update the tracked entity and persist entity.MarkAsCompleted(analysis); await db.SaveChangesAsync(cancellationToken); logger.LogInformation("Completed {{pascalCase FEATURE_NAME}} processing for ID {Id}", id); } catch (Exception ex) { logger.LogError(ex, "Failed to process {{pascalCase FEATURE_NAME}} {Id}", id); throw; // Re-throw to trigger Hangfire retry } } } // ============================================================ // RECURRING JOB CONFIGURATION // ============================================================ // // For scheduled/recurring jobs, configure in Program.cs: // // // Run every hour // RecurringJob.AddOrUpdate( // "{{kebabCase FEATURE_NAME}}-processor", // job => job.ExecuteAsync(0, CancellationToken.None), // Cron.Hourly); // // // Run daily at midnight // RecurringJob.AddOrUpdate( // "{{kebabCase FEATURE_NAME}}-daily-processor", // job => job.ExecuteAsync(0, CancellationToken.None), // Cron.Daily); // // // Custom cron expression (every 15 minutes) // RecurringJob.AddOrUpdate( // "{{kebabCase FEATURE_NAME}}-frequent-processor", // job => job.ExecuteAsync(0, CancellationToken.None), // "*/15 * * * *"); // // ============================================================ // ============================================================ // BATCH JOB TEMPLATE // ============================================================ /// /// Batch job for processing multiple {{pascalCase FEATURE_NAME}}s. /// Queries pending items via LINQ on the DbSet and enqueues individual jobs. /// public class {{pascalCase FEATURE_NAME}}BatchProcessorJob( ApplicationDbContext db, ILogger<{{pascalCase FEATURE_NAME}}BatchProcessorJob> logger) { /// /// Processes all pending {{pascalCase FEATURE_NAME}}s. /// [AutomaticRetry(Attempts = 1)] [Queue("batch")] [JobDisplayName("{{pascalCase FEATURE_NAME}} Batch Processing")] public async Task ProcessAllPendingAsync(CancellationToken cancellationToken) { logger.LogInformation("Starting batch processing for pending {{pascalCase FEATURE_NAME}}s"); var pending = await db.Set<{{pascalCase FEATURE_NAME}}>() .Where(x => x.Status == {{pascalCase FEATURE_NAME}}Status.Pending) .ToListAsync(cancellationToken); logger.LogInformation("Found {Count} pending {{pascalCase FEATURE_NAME}}s to process", pending.Count); foreach (var item in pending) { // Enqueue individual processing jobs BackgroundJob.Enqueue( job => job.ExecuteAsync(item.Id, CancellationToken.None)); } logger.LogInformation("Enqueued {Count} processing jobs", pending.Count); } } // ============================================================ // HANGFIRE CONFIGURATION // ============================================================ // // In Program.cs: // // // Add Hangfire services // builder.Services.AddHangfire(config => config // .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) // .UseSimpleAssemblyNameTypeSerializer() // .UseRecommendedSerializerSettings() // .UseSqlServerStorage(connectionString, new SqlServerStorageOptions // { // CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), // SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), // QueuePollInterval = TimeSpan.Zero, // UseRecommendedIsolationLevel = true, // DisableGlobalLocks = true // })); // // builder.Services.AddHangfireServer(options => // { // options.Queues = new[] { "default", "batch" }; // options.WorkerCount = Environment.ProcessorCount * 2; // }); // // // Register jobs (ApplicationDbContext already registered via AddDbContext) // builder.Services.AddScoped(); // builder.Services.AddScoped<{{pascalCase FEATURE_NAME}}BatchProcessorJob>(); // // // In app pipeline // app.UseHangfireDashboard("/hangfire", new DashboardOptions // { // Authorization = new[] { new HangfireAuthorizationFilter() } // }); // // ============================================================