Passa al contenuto principale

Caching

Il caching riduce latenza e carico su database o servizi esterni memorizzando temporaneamente il risultato di operazioni costose. Va usato con consapevolezza: aggiunge complessità e introduce la possibilità di servire dati stantii.

IMemoryCache

Cache in memoria del processo. Adatta per dati condivisi tra richieste, non condivisi tra istanze. Se si scala su più pod, ogni istanza ha la propria cache indipendente.

// Program.cs
builder.Services.AddMemoryCache();
public class CategorieService
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _db;

public CategorieService(IMemoryCache cache, AppDbContext db)
{
_cache = cache;
_db = db;
}

public async Task<List<Categoria>> GetCategorieAsync(CancellationToken ct)
{
return await _cache.GetOrCreateAsync("categorie", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
entry.SlidingExpiration = TimeSpan.FromMinutes(2);
entry.Size = 1; // richiesto se si imposta un size limit sul cache

return await _db.Categorie.ToListAsync(ct);
}) ?? [];
}
}

AbsoluteExpiration — scade comunque dopo N tempo, anche se usata di frequente. SlidingExpiration — ogni accesso rinnova il timer; scade solo se inutilizzata per N tempo. Si possono usare entrambe: la sliding evita cache calde inutili, l'absolute garantisce un refresh periodico.

Size limit

Senza un limite, la cache cresce senza controllo fino a pressione di memoria. Si imposta un limite in AddMemoryCache e si assegna una dimensione a ogni entry:

builder.Services.AddMemoryCache(options =>
{
options.SizeLimit = 1024; // unità arbitrarie, coerenti con entry.Size
});

IDistributedCache

Cache condivisa tra istanze, tipicamente Redis. L'interfaccia lavora con byte[]; in pratica si serializza in JSON.

// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "myapp:";
});
public async Task<Utente?> GetUtenteAsync(int id, CancellationToken ct)
{
var key = $"utente:{id}";

var cached = await _cache.GetStringAsync(key, ct);
if (cached is not null)
return JsonSerializer.Deserialize<Utente>(cached);

var utente = await _db.Utenti.FindAsync([id], ct);
if (utente is null)
return null;

var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
};
await _cache.SetStringAsync(key, JsonSerializer.Serialize(utente), options, ct);

return utente;
}

Invalidazione esplicita

Quando i dati cambiano, la cache va invalidata per evitare di servire valori stantii:

public async Task AggiornaUtenteAsync(Utente utente, CancellationToken ct)
{
_db.Utenti.Update(utente);
await _db.SaveChangesAsync(ct);
await _cache.RemoveAsync($"utente:{utente.Id}", ct);
}

Output caching (ASP.NET Core 7+)

L'output caching memorizza la risposta HTTP completa di un endpoint. È la forma di caching con meno codice: non serve toccare la logica, si decora l'endpoint.

// Program.cs
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("breve", policy => policy.Expire(TimeSpan.FromSeconds(30)));
options.AddPolicy("lungo", policy => policy.Expire(TimeSpan.FromMinutes(10)));
});

app.UseOutputCache();
[HttpGet]
[OutputCache(PolicyName = "lungo")]
public async Task<IActionResult> GetCatalogo(CancellationToken ct)
{
var catalogo = await _db.Prodotti.ToListAsync(ct);
return Ok(catalogo);
}

L'output caching non è adatto per endpoint che restituiscono dati personalizzati per utente (richiede vary-by) o che hanno effetti collaterali.

Cosa non mettere in cache

  • Dati che cambiano frequentemente e dove la coerenza è critica (saldi, disponibilità in tempo reale).
  • Risultati di operazioni di scrittura: la cache va invalidata, non aggiornata ottimisticamente.
  • Dati sensibili senza considerare la condivisione tra utenti: una cache mal configurata può restituire dati di un altro utente.