Uno dei vantaggi principali delle architetture cloud native è la capacità di scalare in base al carico e di mantenere la resilienza anche in caso di errori o guasti parziali.
Scalabilità significa che un’applicazione può crescere orizzontalmente (aggiungendo istanze) o verticalmente (aumentando le risorse) senza modifiche significative al codice. Resilienza, invece, è la capacità del sistema di resistere a problemi interni o esterni, degradando i servizi in modo controllato senza andare in crash.
Con .NET Aspire, queste due caratteristiche diventano parte integrante del modello di sviluppo.
Aspire consente di scalare i servizi in modo dichiarativo direttamente nell’AppHost.
Esempio: scalare un’API su 3 repliche.
AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.MyApi>("api")
.WithReplicas(3);
builder.Build().Run();
In questo caso, Aspire avvierà 3 istanze della stessa API e si occuperà di bilanciare le richieste in ingresso.
Quando un servizio viene replicato, Aspire crea automaticamente un endpoint bilanciato, in modo che i client non debbano preoccuparsi di quale istanza contattare.
Ad esempio, se il frontend chiama l’API, la configurazione sarà trasparente:
builder.AddProject<Projects.MyFrontend>("frontend")
.WithReference(api);
Il frontend vedrà un unico endpoint, mentre Aspire distribuirà le richieste alle 3 istanze disponibili.
Oltre alla scalabilità, Aspire fornisce strumenti per rendere i servizi resilienti a errori temporanei.
Grazie alle integrazioni con Polly, puoi configurare facilmente politiche di retry, circuit breaker e timeout.
Esempio – resilienza con HttpClient:
builder.Services.AddHttpClient("orders", client =>
{
client.BaseAddress = new Uri("http://api/orders");
})
.AddTransientHttpErrorPolicy(policy =>
policy.WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(retry)))
.AddTransientHttpErrorPolicy(policy =>
policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
In questo modo, se l’API degli ordini è temporaneamente non disponibile, il client riproverà fino a 3 volte prima di aprire il circuit breaker.
Aspire semplifica anche la gestione di worker service che devono elaborare code di messaggi. Se un messaggio non può essere processato, puoi inviarlo a una dead letter queue per non bloccare l’elaborazione degli altri.
Esempio – resilienza in un worker:
public class OrderWorker : BackgroundService
{
private readonly Channel<string> _channel;
public OrderWorker(Channel<string> channel) => _channel = channel;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var message in _channel.Reader.ReadAllAsync(stoppingToken))
{
try
{
Console.WriteLine($"Processing order: {message}");
// simulazione elaborazione
if (message.Contains("error"))
throw new Exception("Invalid order");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}, sent to DLQ");
// invio a dead letter queue
}
}
}
}
Con Aspire puoi definire sia la coda principale che la DLQ come risorse esterne, mantenendo il sistema resiliente anche in presenza di messaggi problematici.
Aspire non è un orchestratore, ma il modello dichiarativo che utilizzi in locale è compatibile con orchestratori come Kubernetes o ambienti PaaS (Azure Container Apps, AWS ECS, ecc.).
Questo significa che la stessa configurazione .WithReplicas(3) che usi in sviluppo può essere applicata in produzione, dove il cluster gestisce automaticamente l’autoscaling basato su metriche (CPU, traffico, code in attesa).
La scalabilità e resilienza gestite da Aspire si basano su alcuni automatismi importanti:
Con .NET Aspire, scalabilità e resilienza non sono un’aggiunta tardiva, ma parte integrante del modello di sviluppo. Puoi replicare i servizi, bilanciare il traffico, applicare retry e circuit breaker in modo dichiarativo e coerente.
Questo rende l’applicazione più robusta e pronta per affrontare ambienti cloud in cui carichi e guasti sono inevitabili.
Nel prossimo articolo vedremo l’ultimo punto della serie: deployment e ambienti in .NET Aspire, per capire come gestire la transizione da sviluppo locale a produzione.