# Servizi di background in ASP.NET Core - Parte 1: Gli approcci

<!--category-- ASP.NET Core, IHostedService, BackgroundService, Hangfire -->
<datetime class="hidden">2025-11-27T09:00</datetime>

Ogni moderna applicazione web ha un lavoro che non dovrebbe bloccare una richiesta HTTP email di invio, elaborazione di file, sincronizzazione con servizi esterni, esecuzione di manutenzione programmata. ASP.NET Core fornisce molteplici approcci per la gestione di questo lavoro di background, da semplice `IHostedService` implementazioni a quadri sofisticati come Hangfire. In questa prima parte, esploreremo i modelli fondamentali e quando utilizzare ciascuno.

# Introduzione

Confessione; Mi piace Background Services un LOT, questo sito (un sito BLOG!) ha più di una mezza dozzina di loro che fanno compiti di background carie, ma come tutto hanno alcune ODDITY e pratiche che renderanno il vostro uso di loro molto più piacevole.
I servizi di background sono gli eroi unsung delle moderne applicazioni web. Mentre i controller gestiscono le richieste HTTP in primo piano, i servizi di background elaborano silenziosamente le email in coda, indicizzano i contenuti per la ricerca, controllano le API esterne, ripuliscono i file temporanei e gestiscono innumerevoli altre attività che altrimenti bloccherebbero la pipeline di richiesta.

In questa serie in due parti, esploreremo i diversi approcci all'implementazione di servizi di background in ASP.NET Core, dal built-in `IHostedService` e `BackgroundService` Astrazioni a soluzioni più sofisticate come Hangfire. Nella Parte 1, esamineremo gli approcci fondamentali e le loro caratteristiche. [Parte 2](/blog/background-services-in-aspnetcore-part2), ci immergeremo nelle implementazioni del mondo reale da una base di codici di produzione.

> **Importante:** Faremo particolare attenzione alla gestione del ciclo di vita `StopAsync` metodo, che è dove molti sviluppatori incontrano eccezioni criptiche quando le loro applicazioni si spengono.

[TOC]

# Perché i servizi di background?

Prima di tuffarsi nel "come," consideriamo brevemente il "perché." I servizi di sfondo consentono:

1. **Scarica operazioni lente** - Non fare attendere gli utenti mentre si invia e-mail o generare PDF
2. **Programma le attività ricorrenti** - Ripulire vecchi dischi ogni sera alle 2 del mattino
3. **Coda di processo** - Gestire messaggi da canali o broker di messaggi
4. **Monitorare lo stato esterno** - API Poll o file system per le modifiche
5. **Coordina flussi di lavoro complessi** - Gestire processi multi-step che durano minuti o ore

ASP.NET Core fornisce diversi approcci all'implementazione di questi servizi, ciascuno con diversi compromessi.

# Il contesto storico: perché i servizi di background sono ora fattibile

Nei "vecchi giorni" (pre-2010), eseguire il lavoro di background nella vostra applicazione web è stato generalmente considerato una cattiva idea. La saggezza convenzionale era: "I server Web gestiscono le richieste web. Il lavoro di background appartiene a un server separato."

Questo non era solo la saggezza cargo-sette si basava su reali limitazioni tecniche:

## L'era della moneta unica

I primi server web (e onestamente ora, i servizi Azure 'a buon mercato') tipicamente eseguito su **CPU single-core o dual-core**. Se hai eseguito un'intensa attività di background della CPU, è direttamente in competizione con le richieste web per lo stesso core:

```
Single Core (2005):
┌─────────────────────┐
│  Background Task    │  ← Uses 80% CPU
│  (80% of core)      │
├─────────────────────┤
│  Web Requests       │  ← Only 20% left!
│  (20% of core)      │  ← Slow responses
└─────────────────────┘
```

Risultato: Il tuo sito web è diventato lento il lavoro di sfondo momento calci.

## Starvation della piscina filettata

Classico ASP.NET utilizzato **thread-per-request**. Il thread pool era relativamente piccolo (25-100 thread in genere), e le attività di sfondo rubare thread che dovrebbero gestire le richieste web:

```csharp
// Classic ASP.NET (2008)
ThreadPool.QueueUserWorkItem(_ =>
{
    // This steals a thread from the pool!
    ProcessLongRunningTask();
});

// Meanwhile, web requests are queued waiting for threads
// HTTP 503 Service Unavailable
```

## IIS Application Pool Recycling

IIS ricicla in modo aggressivo i pool di applicazioni (riavviare l'app) in base ai limiti di memoria, al conteggio delle richieste o agli orari.

```
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
      - Background task killed
      - Work lost, must start again
```

## Supporto Async/Await limitato

Prima di .NET 4.5 (2012), la programmazione asincrona era (relativamente) dolorosa. Le attività di sfondo spesso bloccavano i thread inutilmente:

```csharp
// Pre-async (2008)
void ProcessEmails()
{
    foreach (var email in GetEmails())
    {
        smtp.Send(email);  // Blocks thread for 500ms per email
    }
}
// 100 emails = 50 seconds of blocked thread time
```

## Cosa è cambiato: L'era moderna

Il paesaggio di oggi è drammaticamente diverso:

### 1. Multi-Core è conveniente

L'economia è crollata. Le VM cloud con più core hanno un prezzo ragionevole, e i server blade metal sono sorprendentemente economici. Questo blog gira su un server dedicato a 8 core che costa meno di un analogo Azure VM e ho tutti quei core per me, nessun vicino rumoroso. Un'attività di background su un core non ha un impatto significativo sulle richieste web su altri core:

```
8-Core Server (2024):
Core 1: ████████████████████ Web Requests
Core 2: ████████████████████ Web Requests
Core 3: ████████████████████ Web Requests
Core 4: ████████████████████ Web Requests
Core 5: ████████████████████ Background Task ← Isolated
Core 6: ████████████████████ Background Task
Core 7: ████████████████████ Background Task
Core 8: ████████████████████ Background Task
```

### 2. Async/Await Everywhere

Moderno .NET rende banale la programmazione asincrona. Le attività di sfondo possono attendere I/O senza bloccare i thread:

```csharp
// Modern async (2024)
async Task ProcessEmailsAsync(CancellationToken ct)
{
    await foreach (var email in GetEmailsAsync(ct))
    {
        await smtp.SendAsync(email, ct);  // Doesn't block thread!
    }
}
// 100 emails processed efficiently, thread returns to pool during I/O
```

### 3. Migliore hosting di processo

- [**DockerCity name (optional, probably does not need a translation)**](https://www.docker.com/) - I servizi di background in contenitori non vengono riciclati arbitrariamente
- [**KubernetesCity name (optional, probably does not need a translation)**](https://kubernetes.io/) - Gestione corretta e graziosa dello spegnimento con `SIGTERM`
- [**systemd**](https://systemd.io/) - Servizi Linux che riavviano in modo affidabile
- **Servizi Windows** - Modello di processo corretto e di lunga durata

### 4. Canali e primitive moderne

.NET ora ha il supporto di prima classe per la programmazione concorrente con [`System.Threading.Channels`](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels):

```csharp
// System.Threading.Channels
var channel = Channel.CreateBounded<Email>(100);

// Producer (web request)
await channel.Writer.WriteAsync(email);  // Fast, non-blocking

// Consumer (background service)
await foreach (var email in channel.Reader.ReadAllAsync())
{
    await ProcessAsync(email);  // Efficient, async
}
```

### 5. Limiti delle risorse e gruppi

Moderni orchestratori di container ti permettono **limitare l'uso delle risorse**:

```yaml
# Kubernetes resource limits
resources:
  limits:
    cpu: "500m"        # Background task can't use more than 0.5 CPU
    memory: "512Mi"    # Or more than 512 MB RAM
```

Questo significa che un'attività di background in fuga non può far morire di fame il tuo livello web.

La domanda non è più "Possiamo eseguire servizi di background nella nostra app web?" ma "**Dovremmo?**" Esploreremo questa decisione nella sezione "Quando NON utilizzare i servizi di background" in seguito.

# Le opzioni integrate

## IHostedService: La Fondazione

Al suo centro, ogni servizio di background in ASP.NET Core implementa [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice). Questa interfaccia è splendidamente semplice:

```csharp
public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}
```

Ecco, due metodi. `StartAsync` viene chiamato quando la tua applicazione inizia, e `StopAsync` quando si spegne.

Registrate il vostro servizio in `Program.cs`:

```csharp
builder.Services.AddHostedService<MyBackgroundService>();
```

Ecco il ciclo di vita visualizzato:

```mermaid
graph LR
    A[Application Starts] --> B[StartAsync Called]
    B --> C[Service Running]
    C --> D[Application Shutting Down]
    D --> E[StopAsync Called]
    E --> F[Application Stopped]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style C stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style F stroke:#dc2626,stroke-width:3px,color:#ef4444
```

### StartAsync: Avvio sincrono vs Asincrono

Una decisione critica all'atto dell'attuazione `IHostedService` è se il tuo `StartAsync` metodo dovrebbe bloccare o tornare immediatamente.

**Inizio sincrono (blocco):**

```csharp
public class BlockingStartService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // This blocks application startup until complete
        await InitializeDatabaseAsync(cancellationToken);
        await LoadConfigurationAsync(cancellationToken);

        // Only now will the application continue starting
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

**Avvio asincrono (non bloccante):**

```csharp
public class NonBlockingStartService : IHostedService
{
    private Task _backgroundTask;
    private readonly CancellationTokenSource _cts = new();

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Start background work but return immediately
        _backgroundTask = Task.Run(async () =>
        {
            // Give other services time to initialise
            await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token);
            await DoLongRunningWorkAsync(_cts.Token);
        }, _cts.Token);

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _cts.Cancel();
        await _backgroundTask; // Wait for completion
    }
}
```

**Quando usare ogni approccio:**

- **Blocco:** Quando il servizio deve completare l'inizializzazione prima che l'applicazione possa gestire le richieste (ad esempio, configurazione critica di caricamento, cache di riscaldamento)
- **Non bloccanti:** Quando il servizio può inizializzare in background mentre altri servizi iniziano (ad esempio, l'indicizzazione dei contenuti esistenti, la sincronizzazione con servizi esterni)

### StopAsync: La caduta comune

Qui è dove le cose ottengono i problemi interessanti e dove molti sviluppatori incontrano i problemi. Quando la vostra applicazione si spegne, ASP.NET core chiama `StopAsync` su tutti i servizi ospitati. Hai una finestra limitata (default 5 secondi) per pulire con grazia. Puoi estendere questo in `Program.cs`:

```csharp
builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
```

**L'errore più comune:**

```csharp
public class BrokenService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // WRONG: The channel is still open, ProcessMessagesAsync
        // will hang on WaitToReadAsync forever!
        return Task.CompletedTask;
    }

    private async Task ProcessMessagesAsync()
    {
        // This will never exit because the channel is never completed
        await foreach (var message in _channel.Reader.ReadAllAsync())
        {
            await ProcessAsync(message);
        }
    }
}
```

Quando esegui questo servizio e fermi l'applicazione, vedrai errori come:

```
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
```

O l'applicazione sarà semplicemente appeso per il periodo di spegnimento prima di terminare con forza.

**L'approccio corretto:**

```csharp
public class CorrectService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private readonly CancellationTokenSource _cts = new();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync(_cts.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // CORRECT: Signal cancellation and complete the channel
        await _cts.CancelAsync();
        _channel.Writer.Complete();

        try
        {
            // Wait for processing to finish or for the shutdown timeout
            await Task.WhenAny(_processingTask,
                Task.Delay(Timeout.Infinite, cancellationToken));
        }
        catch (OperationCanceledException)
        {
            // Expected when shutdown timeout is reached
        }
    }

    private async Task ProcessMessagesAsync(CancellationToken token)
    {
        await foreach (var message in _channel.Reader.ReadAllAsync(token))
        {
            try
            {
                await ProcessAsync(message);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested, exit gracefully
                break;
            }
        }
    }
}
```

**Punti chiave per una corretta implementazione di StopAsync:**

1. **Cancellazione del segnale** - Usare un `CancellationTokenSource` e cancellarlo
2. **Canali completi** - Se stai usando i canali, chiama `Writer.Complete()`
3. **Attendere le attività di sfondo** - Uso `Task.WhenAny` con il token di annullamento di spegnimento
4. **Gestisci operazioneCancellataEccezione** - Questo è atteso e dovrebbe essere catturato
5. **Non lanciare eccezioni** - Eccezioni in `StopAsync` può causare un comportamento imprevedibile

## BackgroundService: La classe base comoda

Scrittura `IHostedService` Le implementazioni possono essere ripetitive. Hai sempre bisogno di un'attività di background, di una sorgente token di cancellazione e dello stesso modello di pulizia. [`BackgroundService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) gestisce questa piastra caldaia per voi:

```csharp
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executeTask;
    private CancellationTokenSource _stoppingCts;

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executeTask = ExecuteAsync(_stoppingCts.Token);
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executeTask == null) return;

        try
        {
            _stoppingCts.Cancel();
        }
        finally
        {
            await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
    }

    public virtual void Dispose()
    {
        _stoppingCts?.Cancel();
    }
}
```

Devi solo implementare `ExecuteAsync` e lasciare la classe base gestire l'impianto idraulico:

```csharp
public class SimpleBackgroundService : BackgroundService
{
    private readonly ILogger<SimpleBackgroundService> _logger;

    public SimpleBackgroundService(ILogger<SimpleBackgroundService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Service starting");

        // Wait for app to finish starting
        await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DoWorkAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in background service");
            }
        }

        _logger.LogInformation("Service stopping");
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        _logger.LogInformation("Doing work...");
        // Your actual work here
        await Task.Delay(1000, token);
    }
}
```

### Quando usare BackgroundService vs IHostedService

**Uso `BackgroundService` quando:**

- Hai bisogno di un lungo ciclo di background
- Vuoi una semplice esecuzione periodica
- Non hai bisogno di un bel controllo su `StartAsync`/`StopAsync` tempi

**Uso `IHostedService` quando:**

- Devi controllare esattamente quello che succede in `StartAsync` vs lavoro di background
- Stai impostando gestori di eventi o osservatori piuttosto che un ciclo continuo
- È necessario coordinarsi con altri servizi durante l'avvio

# Avanzato: Coordinamento Startup

A volte avete bisogno di servizi per aspettare l'un l'altro. Ad esempio, si potrebbe desiderare che l'indexer di ricerca semantica attendere fino a quando il processore di file markdown ha finito il suo carico iniziale.

Ecco uno schema per coordinare l'avvio del servizio:

```csharp
public interface IStartupCoordinator
{
    void RegisterService(string serviceName);
    void SignalReady(string serviceName);
    bool IsServiceReady(string serviceName);
    Task WaitForServiceAsync(string serviceName, CancellationToken cancellationToken = default);
    Task WaitForAllServicesAsync(CancellationToken cancellationToken = default);
}

public class StartupCoordinator : IStartupCoordinator
{
    private readonly ConcurrentDictionary<string, TaskCompletionSource> _services = new();
    private readonly ILogger<StartupCoordinator> _logger;

    public void RegisterService(string serviceName)
    {
        _services.TryAdd(serviceName, new TaskCompletionSource());
    }

    public void SignalReady(string serviceName)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            tcs.TrySetResult();
            _logger.LogInformation("{Service} is ready", serviceName);
        }
    }

    public async Task WaitForServiceAsync(string serviceName, CancellationToken ct = default)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            await tcs.Task.WaitAsync(ct);
        }
    }

    public async Task WaitForAllServicesAsync(CancellationToken ct = default)
    {
        await Task.WhenAll(_services.Values.Select(tcs => tcs.Task)).WaitAsync(ct);
    }
}
```

Uso in un servizio:

```csharp
public class DependentService : IHostedService
{
    private readonly IStartupCoordinator _coordinator;
    private readonly ILogger<DependentService> _logger;

    public DependentService(
        IStartupCoordinator coordinator,
        ILogger<DependentService> logger)
    {
        _coordinator = coordinator;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Wait for another service to be ready
        await _coordinator.WaitForServiceAsync("MarkdownProcessor", cancellationToken);

        _logger.LogInformation("Dependencies ready, starting work");

        // Do your work...

        // Signal you're ready for services that depend on you
        _coordinator.SignalReady("DependentService");
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

Questo modello diventa particolarmente utile quando si dispone di più servizi di background con interdipendenze.

# Coordinamento distribuito con Redis

Il coordinatore startup funziona all'interno di un'unica istanza di applicazione. Ma cosa succede quando si scala a più istanze? Non si vogliono tre istanze che eseguono tutte la stessa attività programmata contemporaneamente.

[RedisCity name (optional, probably does not need a translation)](https://redis.io/) fornisce una soluzione semplice: utilizzare flag (chiavi) per coordinare chi fa cosa.

## Semplice elezione di Leader

```csharp
public class DistributedBackgroundService : BackgroundService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<DistributedBackgroundService> _logger;
    private readonly string _instanceId = Guid.NewGuid().ToString();
    private const string LeaderKey = "background:newsletter:leader";

    public DistributedBackgroundService(
        IConnectionMultiplexer redis,
        ILogger<DistributedBackgroundService> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var db = _redis.GetDatabase();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Try to become the leader (SET NX with expiry)
            var acquired = await db.StringSetAsync(
                LeaderKey,
                _instanceId,
                TimeSpan.FromMinutes(5),
                When.NotExists);

            if (acquired)
            {
                _logger.LogInformation("This instance is the leader, running task");

                try
                {
                    await DoScheduledWorkAsync(stoppingToken);
                }
                finally
                {
                    // Release leadership
                    await db.KeyDeleteAsync(LeaderKey);
                }
            }
            else
            {
                var leader = await db.StringGetAsync(LeaderKey);
                _logger.LogDebug("Another instance ({Leader}) is the leader", leader);
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

## Blocco distribuito per sezioni critiche

Per i compiti che non devono essere eseguiti contemporaneamente tra i casi:

```csharp
public async Task ProcessWithLockAsync(CancellationToken cancellationToken)
{
    var db = _redis.GetDatabase();
    var lockKey = "locks:critical-task";
    var lockValue = _instanceId;

    // Try to acquire lock
    if (await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromMinutes(10)))
    {
        try
        {
            _logger.LogInformation("Lock acquired, processing...");
            await DoCriticalWorkAsync(cancellationToken);
        }
        finally
        {
            await db.LockReleaseAsync(lockKey, lockValue);
        }
    }
    else
    {
        _logger.LogDebug("Could not acquire lock, another instance is processing");
    }
}
```

## Quando usare il coordinamento distribuito

- **Attività pianificate** - Solo una istanza dovrebbe inviare la newsletter quotidiana
- **Elaborazione coda con ordinazione** - Assicurarsi che i messaggi siano elaborati in ordine
- **Operazioni ad alta intensità di risorse** - Impedire a più istanze di schiacciare un'API esterna
- **Migrazioni di banche dati** - Solo un'istanza dovrebbe eseguire le migrazioni all'avvio

Per scenari più complessi (job multi-step, programmazione affidabile attraverso riavvii), considerare Hangfire che gestisce il blocco distribuito automaticamente con il suo backend del database.

# Quando NON utilizzare i servizi di background

Prima di tuffarci in strumenti più sofisticati come Hangfire, parliamo di quando tu *Non dovrebbe* utilizzare i servizi di background nella tua applicazione web principale.

## Segni che si dovrebbe dividere in un progetto separato

I servizi di background in esecuzione nell'applicazione web condividono le risorse con la pipeline di richiesta HTTP. Ciò può causare problemi:

### 1. Contenuto delle risorse

**Problema:** Il tuo servizio di background consuma connessioni significative di CPU, memoria o database.

```csharp
// This will starve your web application
public class VideoTranscodingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var video = await _queue.DequeueAsync();
            // This uses 100% of 4 CPU cores for 5 minutes
            await TranscodeVideoAsync(video);
        }
    }
}
```

**Quando le richieste web arrivano durante la transcodifica, sono lenti perché la CPU è occupata.**

**Soluzione:** Passare a un servizio di lavoro separato:

```bash
# Your solution structure
/YourApp.Web          # ASP.NET Core web app - no background services
/YourApp.Worker       # .NET Worker Service - handles background work
/YourApp.Shared       # Shared models, interfaces
```

### 2. Requisiti di scalatura diversi

**Problema:** Il vostro lavoro di sfondo ha bisogno di scala diversa rispetto al vostro livello web.

- **Livello Web:** Scala per il traffico HTTP (potrebbe essere necessario 10 istanze durante il giorno, 2 di notte)
- **Livello di sfondo:** Scala per la profondità della coda (potrebbe aver bisogno di 1 istanza normalmente, 20 durante l'elaborazione di un batch)

Se sono nello stesso processo, non puoi scalarli in modo indipendente.

**Scenario di esempio:**

```
09:00 - High web traffic, low background work → Need 10 web instances, 1 worker
14:00 - Newsletter time! Low web traffic, high background work → Need 2 web instances, 20 workers
```

Mettere servizi di background nella tua applicazione web significa che dovresti eseguire 20 istanze web solo per gestire la newsletter, sprecando risorse.

### 3. Indipendenza dell'occupazione

**Problema:** Vuoi distribuire modifiche web senza riavviare i servizi di background (o viceversa).

```csharp
// If this is in your web app, deploying a CSS change restarts the service
public class LongRunningImportService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // This import takes 2 hours
        await ImportMillionsOfRecordsAsync(stoppingToken);
    }
}
```

Ogni spiegamento interrompe l'importazione. Spostala su un servizio operaio separato che schierate indipendentemente.

### 4. Domini diversi di fallimento

**Problema:** Un bug nel servizio di background blocca l'intera applicazione web.

```csharp
// This null reference exception crashes your web app
public class BuggyBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        string value = null;
        // Unhandled exception - takes down the whole app
        await ProcessAsync(value.Length);
    }
}
```

Se il lavoro di background è in un processo separato, può bloccarsi e riavviare senza influenzare le richieste web.

## Come Factor Background Services

Quando si decide di dividere, ecco l'architettura consigliata:

### Opzione 1: .NET Worker Service

Crea un nuovo progetto utilizzando il modello del servizio di lavoro:

```bash
dotnet new worker -n YourApp.Worker
```

Struttura:

```
/YourApp.Worker
  /Services
    VideoTranscodingService.cs
    EmailSenderService.cs
  /Program.cs
  /appsettings.json
```

Program.cs:

```csharp
var builder = Host.CreateApplicationBuilder(args);

// Register your background services
builder.Services.AddHostedService<VideoTranscodingService>();
builder.Services.AddHostedService<EmailSenderService>();

// Share configuration with web app
builder.Services.Configure<VideoConfig>(
    builder.Configuration.GetSection("Video"));

// Share database context
builder.Services.AddDbContext<YourDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

var host = builder.Build();
host.Run();
```

Distribuisci separatamente:

```bash
# Web app on ports 80/443
/YourApp.Web → web-server-1, web-server-2, web-server-3

# Worker service doesn't listen on any port
/YourApp.Worker → worker-server-1, worker-server-2
```

### Opzione 2: Progetto separato con coda condivisa

Utilizzare una coda di messaggi per disaccoppiare web e lavoratori:

```mermaid
graph LR
    A[Web App] --> B[Message Queue]
    B --> C[Worker 1]
    B --> D[Worker 2]
    B --> E[Worker N]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style B stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style C stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style D stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style E stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
```

Le code delle app web funzionano:

```csharp
// In your web controller
public class VideoController : ControllerBase
{
    private readonly IMessageQueue _queue;

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile video)
    {
        await _storage.SaveAsync(video);

        // Queue for processing - don't process in web app
        await _queue.PublishAsync(new VideoTranscodeJob
        {
            VideoId = video.Id,
            Priority = Priority.Normal
        });

        return Accepted(); // Return immediately
    }
}
```

Il lavoratore consuma dalla coda:

```csharp
// In your worker service
public class VideoWorker : BackgroundService
{
    private readonly IMessageQueue _queue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in _queue.SubscribeAsync<VideoTranscodeJob>(stoppingToken))
        {
            await TranscodeAsync(job);
        }
    }
}
```

**Opzioni di coda dei messaggi popolari:**

- [**ConiglioMQunit synonyms for matching user input**](https://www.rabbitmq.com/) - Più popolare, ricco di funzionalità
- [**Autobus di servizio Azure**](https://azure.microsoft.com/en-us/products/service-bus/) - Se sei su Azure
- [**AWS SQS**](https://aws.amazon.com/sqs/) - Se sei su AWS
- [**Redis StreamsCity name (optional, probably does not need a translation)**](https://redis.io/docs/data-types/streams/) - Più semplice, buona per scala più piccola

### Opzione 3: Lavoratori specializzati multipli

Per i sistemi complessi, suddivisi per responsabilità:

```
/YourApp.Web              # HTTP requests only
/YourApp.EmailWorker      # Sends emails
/YourApp.VideoWorker      # Transcodes videos
/YourApp.ReportWorker     # Generates reports
/YourApp.Scheduler        # Runs scheduled jobs (Hangfire)
```

Ogni lavoratore può:

- Scala in modo indipendente
- Distribuisci in modo indipendente
- Utilizzare risorse diverse (email worker needs SMTP, video worker needs GPU)
- Avere un monitoraggio e un'allerta diversi

## Quando mantenere i servizi di background nella tua App Web

Nonostante quanto sopra, alcuni scenari sono perfettamente validi per i servizi di background in-process:

### Attività periodiche leggere

```csharp
// Fine to keep in web app
public class CacheWarmingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _cache.WarmupAsync(); // Quick operation
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

### Ascoltatori di eventi

```csharp
// Fine to keep in web app
public class FileWatcherService : IHostedService
{
    // Reacts to events, doesn't consume significant resources
    private FileSystemWatcher _watcher;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _watcher = new FileSystemWatcher("/config");
        _watcher.Changed += OnConfigChanged;
        _watcher.EnableRaisingEvents = true;
        return Task.CompletedTask;
    }
}
```

### Coda a base di canali (per lavori non critici)

```csharp
// Fine to keep in web app if work is quick and not critical
public class EmailQueueService : BackgroundService
{
    // Sends emails in background, but each email takes < 1 second
    // If the app restarts, losing a few queued emails is acceptable
}
```

### Coordinamento startup

```csharp
// Fine to keep in web app
public class WarmupService : IHostedService
{
    // Runs once at startup, then does nothing
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _database.WarmupConnectionPoolAsync();
        await _cache.LoadCriticalDataAsync();
    }
}
```

## Matrice della decisione

| Caratteristica | Mantenere nell'app web | Spostarsi al servizio del lavoratore |
|---------------|-----------------|------------------------|
| Uso della CPU per operazione | < 100ms | > 1 secondo |
| Memoria per operazione | < 10 MB | > 100 MB |
| Frequenza | Periodico (minuti/ore) | Continuo o ad alta frequenza |
| Critica | Non critica | Critica |
| Durata | Secondi | Minuti alle ore |
| Scale con | Traffico Web | Profondità coda di lavoro |
| Esempio | Riscaldamento cache, ricarica config | Elaborazione video, grandi importazioni |

## Esempio di Real-World: La Piattaforma del Blog

Nella piattaforma del blog il cui codice esaminiamo nella Parte 2:

**Ha tenuto in app web:**

- `MarkdownDirectoryWatcherService` - Lightweight file watcher
- `UmamiBackgroundSender` - Eventi di analisi rapida
- `EmailSenderHostedService` - Piccolo volume, non critico
- `MarkdownReAddPostsService` - Solo avvio, configurazione-gated

**Dovrebbe passare al servizio dei lavoratori in caso di aumento della scala:**

- `BrokenLinkCheckerBackgroundService` - Fa molte richieste HTTP
- `SemanticIndexingBackgroundService` - Chiama API di integrazione esterna

**Già in servizio separato:**

- `Mostlylucid.SchedulerService` - Hangfire cruscotto e newsletter invio

Questo è un approccio pragmatico: iniziare semplice (in-process), dividere quando si dispone di prove è necessario.

# Oltre le basi: Hangfire

Mentre `IHostedService` e `BackgroundService` sono eccellenti per i servizi che possiedi e controlli, a volte hai bisogno di una programmazione più sofisticata. [HangfireCity name (optional, probably does not need a translation)](https://www.hangfire.io/) Entra.

Hangfire fornisce:

- **Code di lavoro persistenti** - I lavori sopravvivono ai riavvii dell'applicazione
- **Posti di lavoro ricorrenti** - Programmazione in stile Cron
- **UI da cruscotto** - Vedere cosa sta funzionando, cosa è fallito, riprovare lavori
- **Esecuzione distribuita** - Più server possono elaborare la stessa coda di lavoro
- **Ricerchi automatici** - I lavori falliti vengono automaticamente recuperati con un backoff esponenziale

Ecco un semplice esempio:

```csharp
// In Program.cs
builder.Services.AddHangfire(config => config
    .UsePostgreSqlStorage(connectionString)
    .UseRecommendedSerializerSettings());

builder.Services.AddHangfireServer();

var app = builder.Build();

// Schedule recurring jobs
app.UseHangfireDashboard();
app.Services.GetRequiredService<IRecurringJobManager>()
    .AddOrUpdate<NewsletterService>(
        "send-daily-newsletter",
        x => x.SendDailyNewsletter(),
        Cron.Daily(17)); // 5 PM every day
```

Il vostro servizio è solo una classe normale:

```csharp
public class NewsletterService
{
    private readonly IEmailService _emailService;
    private readonly ISubscriberRepository _subscribers;

    public NewsletterService(
        IEmailService emailService,
        ISubscriberRepository subscribers)
    {
        _emailService = emailService;
        _subscribers = subscribers;
    }

    public async Task SendDailyNewsletter()
    {
        var subscribers = await _subscribers.GetDailySubscribersAsync();

        foreach (var subscriber in subscribers)
        {
            await _emailService.SendNewsletterAsync(subscriber);
        }
    }
}
```

Maniglie a sospensione:

- Garantire il funzionamento del lavoro all'ora prevista
- Riprova se fallisce
- Archiviazione cronologia delle esecuzioni
- Fornire un cruscotto per monitorare tutto

```mermaid
graph TD
    A[Hangfire Server] --> B{Check Schedule}
    B -->|Job Due| C[Dequeue Job]
    C --> D[Execute Job Method]
    D -->|Success| E[Mark Complete]
    D -->|Failure| F[Retry with Backoff]
    F --> G{Max Retries?}
    G -->|No| C
    G -->|Yes| H[Mark Failed]
    E --> I[Update Dashboard]
    H --> I
    I --> B

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style E stroke:#059669,stroke-width:3px,color:#10b981
    style H stroke:#dc2626,stroke-width:3px,color:#ef4444
```

**Quando usare Hangfire:**

- Hai bisogno di code di lavoro persistenti che sopravvivano riavvia
- Vuoi un cruscotto per monitorare e attivare manualmente i lavori
- È necessario distribuire l'elaborazione del lavoro su più server
- Vuoi built-in retry logica e gestione dei guasti
- È necessario cron-style pianificazione delle attività ricorrenti

**Quando restare con IHostedService/BackgroundService:**

- Hai bisogno di un controllo accurato sul ciclo di vita di servizio
- Il vostro servizio deve reagire agli eventi in tempo reale
- Vuoi ridurre al minimo le dipendenze
- Stai costruendo un semplice compito periodico che non ha bisogno di persistenza

# Altre opzioni

Mentre Hangfire è popolare, ci sono altre librerie che vale la pena considerare:

[**Quartz.NET**](https://www.quartz-scheduler.net/):

- Programmazione più flessibile di Hangfire
- Supporta le espressioni cron e la programmazione basata sul calendario
- Può persistere in più database
- API più complesse ma più potenti

[**MassTransit**](https://masstransit.io/)/[**NServiceBus**](https://particular.net/nservicebus):

- Implementazioni complete del bus messaggi
- Meglio per i sistemi distribuiti e i microservizi
- Sagas di supporto (flussi di lavoro a lungo termine)
- Curva di apprendimento steeper

[**Funzioni Azure**](https://azure.microsoft.com/en-us/products/functions/)/[**AWS LambdaCity name (optional, probably does not need a translation)**](https://aws.amazon.com/lambda/):

- Se sei nel cloud, considera senza server
- Pay per esecuzione piuttosto che mantenere un servizio in esecuzione
- Ridimensionamento automatico
- Un po' di latenza in testa per gli inizi freddi

# Sommario

Nella Parte 1, abbiamo trattato gli approcci fondamentali ai servizi di background in ASP.NET Core:

1. **IHostedService** - Il fondamento, massima flessibilità
2. **Servizio di background** - Conveniente classe base per loop a lunga durata
3. **Coordinamento dell'avviamento** - Fare i servizi aspettano l'un l'altro
4. **Coordinamento distribuito** - Utilizzo di Redis per scenari multi-instance
5. **HangfireCity name (optional, probably does not need a translation)** - Quando avete bisogno di lavori persistenti e programmazione sofisticata

Le lezioni più importanti:

- **Completa sempre i canali e annulla i token in StopAsync**
- **Decidere se StartAsync dovrebbe bloccare o tornare immediatamente**
- **Gestisci operazioneCancellataEccezione con grazia**
- **Usa Task.WhenAny con il token di spegnimento per rispettare i timeout di spegnimento**

Dentro [Parte 2](/blog/background-services-in-aspnetcore-part2), esamineremo le implementazioni del mondo reale da una piattaforma del blog di produzione:

- Osservatori di file di sistema che sincronizzano i file di markdown in un database
- Mittenti di posta elettronica con politiche di riprova e interruttori di circuito
- Coda di eventi di analisi che richiede il batch
- Indexer di ricerca semantici che elaborano i contenuti in modo asincrono
- Checker di collegamento rotti che convalidano periodicamente gli URL esterni

Questi esempi dimostrano i modelli della parte 1 in azione, compreso il modello di coordinamento dell'avvio e la corretta gestione dell'arresto.

# Ulteriore lettura

- [Microsoft Docs: attività di background con servizi ospitati](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services)
- [Documentazione Hangfire](https://docs.hangfire.io/)
- [Canali in C#](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels)
- [Documentazione Quartz.NET](https://www.quartz-scheduler.net/)
- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) - Client Redis per .NET