Passa al contenuto principale

Librerie per code e job

Quando i job devono sopravvivere al riavvio del processo o richiedono scheduling avanzato, si usano librerie dedicate. Le due scelte principali nell'ecosistema .NET sono Hangfire e Quartz.NET.

Hangfire

Hangfire persiste i job su database (SQL Server, PostgreSQL, Redis, altri) e li esegue tramite un server in background. Offre un'interfaccia web di monitoraggio.

Dipendenze

dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.PostgreSql # o Hangfire.SqlServer, Hangfire.Redis.StackExchange

Configurazione

// Program.cs
builder.Services.AddHangfire(config =>
config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UsePostgreSqlStorage(o =>
o.UseNpgsqlConnection(builder.Configuration.GetConnectionString("Hangfire"))));

builder.Services.AddHangfireServer(options =>
{
options.WorkerCount = 5;
options.Queues = ["critical", "default"];
});

// Dashboard (solo ambienti non-produzione, o con autenticazione)
app.UseHangfireDashboard("/hangfire");

Tipi di job

// Fire-and-forget — eseguito una volta, subito
BackgroundJob.Enqueue<InviaEmail>(x => x.ExecuteAsync(emailId));

// Ritardato — eseguito dopo un intervallo
BackgroundJob.Schedule<InviaReminderEmail>(
x => x.ExecuteAsync(ordineId),
TimeSpan.FromHours(24));

// Ricorrente — eseguito secondo uno schedule cron
RecurringJob.AddOrUpdate<SincronizzaPrezzi>(
"sincronizza-prezzi",
x => x.ExecuteAsync(),
Cron.Hourly);

// Continuazione — eseguito al completamento di un altro job
var jobId = BackgroundJob.Enqueue<GeneraFattura>(x => x.ExecuteAsync(ordineId));
BackgroundJob.ContinueJobWith<InviaFattura>(jobId, x => x.ExecuteAsync(ordineId));

Classi job con DI

I job sono classi normali con dipendenze iniettate dal container:

public class SincronizzaPrezzi
{
private readonly AppDbContext _db;
private readonly ICatalogoPrezziClient _catalogo;
private readonly ILogger<SincronizzaPrezzi> _logger;

public SincronizzaPrezzi(
AppDbContext db,
ICatalogoPrezziClient catalogo,
ILogger<SincronizzaPrezzi> logger)
{
_db = db;
_catalogo = catalogo;
_logger = logger;
}

public async Task ExecuteAsync()
{
_logger.LogInformation("Avvio sincronizzazione prezzi");

var prezzi = await _catalogo.GetPrezziAsync();

foreach (var prezzo in prezzi)
{
var prodotto = await _db.Prodotti.FindAsync(prezzo.ProdottoId);
if (prodotto is not null)
prodotto.AggiornaPrezzzo(prezzo.Valore);
}

await _db.SaveChangesAsync();
_logger.LogInformation("Sincronizzati {Count} prezzi", prezzi.Count);
}
}

Quartz.NET

Quartz.NET è più orientato al job scheduling complesso. Offre trigger avanzati, supporto al clustering e separazione esplicita tra job (cosa fare) e trigger (quando farlo).

Dipendenze

dotnet add package Quartz.AspNetCore
dotnet add package Quartz.Extensions.Hosting
dotnet add package Quartz.Serialization.Json

Configurazione

// Program.cs
builder.Services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();

// Job + Trigger nello stesso blocco
var jobKey = new JobKey("sincronizza-prezzi");

q.AddJob<SincronizzaPrezziJob>(opts => opts.WithIdentity(jobKey));

q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("sincronizza-prezzi-trigger")
.WithCronSchedule("0 0 * * * ?")); // ogni ora
});

builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

Implementazione di un job

public class SincronizzaPrezziJob : IJob
{
private readonly AppDbContext _db;
private readonly ICatalogoPrezziClient _catalogo;
private readonly ILogger<SincronizzaPrezziJob> _logger;

public SincronizzaPrezziJob(
AppDbContext db,
ICatalogoPrezziClient catalogo,
ILogger<SincronizzaPrezziJob> logger)
{
_db = db;
_catalogo = catalogo;
_logger = logger;
}

public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Avvio sincronizzazione prezzi");

var prezzi = await _catalogo.GetPrezziAsync(context.CancellationToken);

foreach (var prezzo in prezzi)
{
var prodotto = await _db.Prodotti.FindAsync(prezzo.ProdottoId);
if (prodotto is not null)
prodotto.AggiornaPrezzzo(prezzo.Valore);
}

await _db.SaveChangesAsync(context.CancellationToken);
}
}

Confronto

CaratteristicaHangfireQuartz.NET
Persistenza✅ Database✅ Database (opzionale)
Dashboard web✅ Integrata❌ (tool esterni)
Fire-and-forget✅ Nativo❌ (workaround)
Continuazioni✅ Nativo
Cron scheduling
Clustering
Separazione job/trigger
ComplessitàBassaMedia

Hangfire è preferibile per job semplici, fire-and-forget e quando si vuole una dashboard senza configurazioni aggiuntive.

Quartz.NET è preferibile per scheduling complesso, clustering avanzato o quando la separazione esplicita tra job e trigger è importante.

Per code in-process senza persistenza, vedi 08-code-native.