// ============================================================
// 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() }
// });
//
// ============================================================