Passa al contenuto principale

Background services

Un background service è un componente che gira in background per tutta la vita dell'applicazione, in parallelo con la gestione delle richieste HTTP. Casi d'uso tipici: consumare una coda di messaggi, inviare notifiche in batch, eseguire pulizie periodiche.

IHostedService vs BackgroundService

IHostedService è l'interfaccia base: richiede l'implementazione di StartAsync e StopAsync.

BackgroundService è la classe astratta che implementa IHostedService e gestisce il loop di esecuzione: basta implementare ExecuteAsync.

In quasi tutti i casi si estende BackgroundService.

Pattern base

public class EmailWorker : BackgroundService
{
private readonly ILogger<EmailWorker> _logger;
private readonly IServiceScopeFactory _scopeFactory;

public EmailWorker(
ILogger<EmailWorker> logger,
IServiceScopeFactory _scopeFactory)
{
_logger = logger;
this._scopeFactory = _scopeFactory;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("EmailWorker avviato.");

while (!stoppingToken.IsCancellationRequested)
{
try
{
await ElaboraEmailInAttesaAsync(stoppingToken);
}
catch (OperationCanceledException)
{
// shutdown richiesto, uscita pulita
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'elaborazione delle email.");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}

_logger.LogInformation("EmailWorker fermato.");
}

private async Task ElaboraEmailInAttesaAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

var email = await db.EmailInAttesa
.Where(e => !e.Inviata)
.FirstOrDefaultAsync(ct);

if (email is null)
{
await Task.Delay(TimeSpan.FromSeconds(10), ct);
return;
}

// ... invio email ...

email.Inviata = true;
await db.SaveChangesAsync(ct);
}
}

Il DbContext è scoped, ma BackgroundService è singleton. Si usa sempre IServiceScopeFactory per creare uno scope per ogni unità di lavoro. Vedi 16-dependency-injection.

Registrazione:

builder.Services.AddHostedService<EmailWorker>();

Integrazione con Channel<T>

Il pattern più efficiente per produttori/consumatori interni è affidare la coda a un Channel<T> singleton e far leggere il worker da quel canale. Niente polling sul database, latenza minima.

// Registrazione del canale come singleton
builder.Services.AddSingleton(_ =>
Channel.CreateBounded<EmailMessage>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait
}));
public class EmailWorker : BackgroundService
{
private readonly ChannelReader<EmailMessage> _reader;

public EmailWorker(Channel<EmailMessage> channel)
{
_reader = channel.Reader;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var messaggio in _reader.ReadAllAsync(stoppingToken))
{
try
{
await InviaAsync(messaggio, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore invio email a {Destinatario}", messaggio.Destinatario);
}
}
}
}

ReadAllAsync attende nuovi messaggi senza polling e rispetta il CancellationToken: quando il token viene annullato allo shutdown, il foreach termina pulitamente. Vedi 08-code-native per i dettagli sui channel.

Graceful shutdown

ASP.NET Core invia il CancellationToken di stop quando l'applicazione riceve il segnale di shutdown (SIGTERM, Ctrl+C). Il worker deve terminare il lavoro corrente entro il timeout di shutdown (default: 30 secondi) e uscire.

// Program.cs — aumentare il timeout se il worker ha operazioni lunghe
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(60);
});

Non bloccare ExecuteAsync ignorando il token: il processo viene terminato forzatamente allo scadere del timeout, con possibile perdita di dati.

Esecuzione periodica

Per task che girano a intervalli fissi, il pattern è await Task.Delay alla fine di ogni ciclo:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await PulisciSessioniScaduteAsync(stoppingToken);
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}

Per scheduling più complesso (cron expression, orari fissi) si valuta Quartz.NET. Vedi 09-librerie-code.