.NET 9 Hybrid Cache

In questo articolo vedremo una delle ultime novità di Microsoft, ancora in versione preview nel momento in cui sto scrivendo questo articolo: la nuova libreria per gestire le cache in locale o distribuite senza nessuna dipendenza esterna, sto parlando di Hybrid Cache!

L’utilizzo delle cache nel mondo applicativo è una strategia ormai diffusa per poter velocizzare e rendere più performante la nostra applicazione. La memorizzazione in cache più semplice e veloce è senza dubbio quella in memoria.

Tutto questo è molto bello, fino a quando il nostro sistema non diventa un sistema distribuito. Se pensiamo ad un sistema a micro-servizi, con più istanze della stessa entità allora non possiamo pensare di salvare (per esempio lo stato di un utente) in un memoria, poiché questo sarebbe accessibile solamente da un’istanza dell’applicazione. In questo scenario bisogna quindi spostarsi verso un sistema di cache distribuite, come può essere Redis.

Prima di Hybrid Cache

Prima di questa libreria Microsoft ci metteva a disposizione 2 interfacce: IMemoryCache e IDistributedCache. Le strategie di utilizzo di una libreria piuttosto che un’altra era a nostra discrezione, scrivendo magari una nostra interfaccia custom per gestire la tipologia di cache in base al nostro contesto applicativo.

Un’altra alternativa era quella di utilizzare librerie fuori dal perimetro di Microsoft, come ad esempio NCache o EasyCaching, solo per citarne alcune.

Hybrid Cache ci mette a disposizione IMemoryCache (con una cache di primo livello, L1 ) e IDistributedCache (con una cache distribuita, L2, non obbligatoria). Un altro vantaggio di questa libreria è che risolve i problemi di cache stampede, ossia quando un processo accede alla cache nel momento in cui è appena stata svuotata per poi essere aggiornata. Altri vantaggi riguardano la possibilità di raggruppare cache differenti con i tag, gestire serializzazioni custom ed ottenere le metriche.

Installazione e configurazione

Per questo esempio usiamo un semplice progetto WebAPI in .NET 9 e andremo a migliorare questo semplice controller che ritorna una previsione metereologica.

C#
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Neutral", "Warm", "Hot"
    };


    [HttpGet(Name = "GetWeatherForecast")]
    public async Task<IEnumerable<WeatherForecast>> GetAsync()
    {
        return await GetForecastAsync();
    }

    //Service implementation
    private async Task<WeatherForecast[]> GetForecastAsync()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

Come prima cosa installiamo il pacchetto dal package manager di Visual Studio

C#
Install-Package Microsoft.Extensions.Caching.Hybrid -Version 1.0.0-preview1

Spostiamoci nel file Program.cs e aggiungiamo nel sistema della Dependency Injection la nostra libreria:

C#
builder.Services.AddHybridCache();

Di default verrà utilizzata una cache di primo livello (L1), ossia quella in RAM. Nella seconda parte di questo articolo vedremo anche come configurare una cache distribuita (L2), sfruttando, ad esempio, Redis.

Volendo possiamo andare ad aggiungere una configurazione di default:

C#
builder.Services.AddHybridCache(options =>
{
    // Maximum size of cached items
    options.MaximumPayloadBytes = 1024 * 1024 * 10; // 10MB
    options.MaximumKeyLength = 512;

    // Default timeouts
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(30),
        LocalCacheExpiration = TimeSpan.FromMinutes(30)
    };
});

Alle righe 4 e 5 abbiamo definito che la chiave usata per storicizzare i valori dovrà avere una dimensione massima di 512 caratteri ed un payload di massimo 10MB.

Alla riga 8 invece abbiamo definito dopo quanto scadono le cache di primo livello (LocalCacheExpiration) e dopo quando scadono quelle di secondo livello (Expiration).

Vediamo ora di approfondire i principali metodi di questa libreria.

Get or Create

La libreria ci mette a disposizione il metodo GetOrCreateAsync, il quale cerca il valore nella cache, se lo trova lo ritorna sennò richiamerà la funzione passata come parametro per recuperare il dato, salvarselo e successivamente ritornarlo.

TODO immagine

Trasformiamo il metodo GetAsync aggiungendo GetOrCreateAsync:

C#
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Neutral", "Warm", "Hot"
        };

        private HybridCache _cache;

        public WeatherForecastController(
            HybridCache cache
            )
        {
            _cache = cache;
        }

        [HttpGet(Name = "GetWeatherForecastWithCache")]
        public async Task<IEnumerable<WeatherForecast>> GetAsync()
        {
            //Hybrid Cache options
            var options = new HybridCacheEntryOptions
            {
                Expiration = TimeSpan.FromHours(1),
                LocalCacheExpiration = TimeSpan.FromMinutes(30)
            };

            return await _cache
                .GetOrCreateAsync(
                    "weatherForecast",  // --> Cache item name
                    GetForecastAsync(), // --> Value
                    async (state, cancellationToken) => await state, // --> configuration of cancellation token
                    options // --> Cache options
                );
        }

        private async Task<WeatherForecast[]> GetForecastAsync()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }

Alla riga 12 abbiamo iniettato tramite Dependency Injection l’istanza di Hibryd Cache, alla riga 19 abbiamo configurato un oggetto che descrive le caratteristiche della cache (la sua durata) ed infine alla riga 25 abbiamo usato il metodo citato poco fa.

Aggiunta di un elemento nella cache

La libreria ci mette a disposizione il metodo SetAsync, il quale ci permette solamente di inserire il valore in cache.

C#

[HttpPut(Name = "location/{locationCode}")]
public async Task<IActionResult> ChangeLocationAsync(string locationCode)
{
    //Service implementation
    await ChangeMyLocationAsync(locationCode);

    var options = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromHours(1),
        LocalCacheExpiration = TimeSpan.FromMinutes(30)
    };

    await _cache.SetAsync(
        $"myLocationCode",
        locationCode,
        options
    );

    return Ok(new { result = "Location changed!" });
}

private async Task ChangeMyLocationAsync(string locationCode)
{
    //service implementation
    await Task.FromResult(() => { });
}

In questo esempio abbiamo simulato un endpoint che aggiorna un valore immaginario della propria posizione. In uno scenario reale andremo a recuperare la posizione dell’utente dalle cache prima di andare a ritornare le previsioni della temperatura.

Nel caso in cui utilizzassimo sia le cache i primo livello (L1, in RAM) che quelle di secondo livello (L2, distribuite), il metodo andrebbe ad aggiornarle entrambe, sovrascrivendo il valore precedente.

Rimuovere un elemento dalle cache

Ovviamente oltre ai metodi di aggiunta abbiamo anche quelli per rimuovere un valore, in questo caso è RemoveAsync.

C#
[HttpDelete(Name = "location/{locationCode}")]
public async Task<IActionResult> RemoveLocationAsync(string locationCode)
{
    //Service implementation
    await RemoveMyLocationAsync(locationCode);

    await _cache.RemoveAsync("myLocationCode");

    return Ok(new { result = "Location removed!" });
}

Nel caso in cui una cache non esista non succederà nulla e non verrà generata nessuna Exception.

Utilizzo dei tag per raggruppare più elementi

Un altro aspetto interessante di questa libreria è la possibilità di aggiungere dei tag (ad un elemento) in modo tale da andare poi a gestire una cancellazione massiva di tutti quegli elementi che hanno un tag simile. Vediamo un esempio:

C#
        [HttpGet(Name = "GetWeatherForecast")]
        public async Task<IEnumerable<WeatherForecast>> GetWithCacheAsync()
        {
            //Hybrid Cache options
            var options = new HybridCacheEntryOptions
            {
                Expiration = TimeSpan.FromHours(1),
                LocalCacheExpiration = TimeSpan.FromMinutes(30)
            };

            //get my location code from a service
            string mylocation = await GetMyLocationAsync();

            return await _cache
                .GetOrCreateAsync(
                    "weatherForecast",  // --> Cache item name
                    GetForecastAsync(), // --> Value
                    async (state, cancellationToken) => await state, // configuration of cancellation token
                    options, // --> Cache options
                    tags: ["weather", mylocation]
                );
        }

        [HttpPost(Name = "GetWeatherForecast/location/{locationCode}/reset")]
        public async Task<IActionResult> ResetWeatcherOfLocation(string locationCode)
        {
            //Remove all cache with locationCode
            await _cache.RemoveByTagAsync(locationCode);

            return Ok(new { results = "Location reset!" });
        }

Nel primo endpoint abbiamo aggiunto il tempo atmosferico del paese dell’utente aggiungendo anche dei tag, nel secondo endpoint viene cancellato dalle cache tutto quello che ha un tag legato al paese passato come parametro.

Configurare una cache di secondo livello (L2)

In questo articolo vedremo come aggiungere Redis come cache L2. Il primo passaggio è quello di aggiungere la libreria Microsoft ufficiale al nostro progetto:

C#
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

Spostiamoci nuovamente nel program.cs e aggiungiamo delle opzioni dove prima abbiamo aggiunto Hybrid Cache alla Dependency Injection.

C#
// Add Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "RedisConnectionString";
});

// Add HybridCache - it will automatically use Redis as L2
builder.Services.AddHybridCache();

Alla riga 8, quando il servizio verrà inserito nel container della Dependency Injection, rileverà automaticamente la configurazione Redis e attiverà l’utilizzo di una cache L2.

Framework supportati

Hybrid Cache supporta .NET Core, .NET Framework >= 4.7.2 e .NET Standard 2.0

Conclusioni

In questo articolo abbiamo visto come utilizzare la nuova libreria di Microsoft per gestire sia le cache di primo livello (in locale) sia quelle di secondo livello (distribuite) ed il suo utilizzo è davvero molto semplice. Ad oggi la libreria è ancora in versione preview ma è previsto che verrà rilasciata la prima versione ufficiale nei prossimi mesi.

Grazie per aver letto questo articolo, alla prossima!

Condividi questo articolo
Shareable URL
Post precedente

Salvataggio delle password nel database

Prosimo post

Polly per gestire la scadenza di un token JWT

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Leggi il prossimo articolo