blexin

Consulenza
Sviluppo e
Formazione IT


Blog

Evolvi la tua azienda

Vediamo come ottimizzare le performance delle applicazioni ASP.NET Core con il caching

In-memory caching in ASP.NET Core

Martedì 2 Luglio 2019

Credo sia capitato a tutti, nel nostro lavoro, di ricevere richieste da parte del cliente, o feedback da utenti dei nostri applicativi, di incrementare la velocità di risposta.

Se non basta seguire le best practices nel nostro codice, oppure ottimizzare la lettura dati, dobbiamo allora far ricorso al caching per dare una "spintarella" alle nostre applicazioni.

Il caching consiste nel conservare da qualche parte le informazioni che cambiano meno frequentemente. Cosa voglia dire “frequentemente” è un requisito business della nostra applicazione.

In questo articolo vedremo cosa offre ASP.NET Core in tema di caching.

IMemoryCache e IDistributedCache

Queste due interfacce rappresentano il meccanismo built-in per il caching in .NET Core. Tutte le altre tecniche, di cui magari avete già sentito parlare, si basano su di esse. In questo articolo ci occuperemo dell'in-memory cache, mentre nel prossimo articolo esamineremo la distributed cache.

Abilitare in-memory caching in ASP.NET Core

La cache in-memory di ASP.NET Core è una funzionalità che possiamo incorporare nella nostra applicazione tramite il metodo ConfigureServices nella classe Startup, come mostrato nello snippet di codice che segue:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddMemoryCache();
}

Il metodo AddMemoryCache ci permette di registrare l'interfaccia IMemoryCache che, come detto sopra, è la base da utilizzare per il caching. Di seguito, vediamo la definizione dell'interfaccia nel framework:

public interface IMemoryCache : IDisposable
{
    bool TryGetValue(object key, out object value);
    ICacheEntry CreateEntry(object key);
    void Remove(object key);
}

I metodi presenti nell'interfaccia non sono i soli disponibili per poter lavorare con la cache. Esistono diverse estensioni che arricchiscono le API disponibili e ne facilitano enormemente l'utilizzo, come vedremo in seguito. Ad esempio:

public static class CacheExtensions
{
    public static TItem Get<titem>(this IMemoryCache cache, object key);
    
    public static TItem Set<titem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options);

    public static bool TryGetValue<titem>(this IMemoryCache cache, object key, out TItem value);
    ...
}

Una volta registrata, l'interfaccia è iniettabile nei costruttori delle classi interessate a usarla, come di seguito:

private IMemoryCache cache;
public MyCacheController(IMemoryCache cache)
{    
        this.cache = cache;
}

Vediamo ora come aggiungere e leggere oggetti in una cache.

Lettura e scrittura oggetti in cache tramite IMemoryCache

Per scrivere un oggetto utilizzando l'interfaccia IMemoryCache, bisogna utilizzare il metodo generico Set<T>() :

[HttpGet]
public string Get()
{
    cache.Set(“MyKey”, DateTime.Now.ToString());
    return “This is a test method...”;
}

Il metodo in questione accetta due parametri: il primo è la chiave con cui verrà identificato l'oggetto inserito nella cache, il secondo è invece il valore. Per recuperare un oggetto dalla cache, viene utilizzato il metodo generico Get<T>():

[HttpGet(“{key}”)]
public string Get(string key)
{
    return cache.Get<string>(key);
}

Se non siamo sicuri che una specifica chiave sia presente nella cache, possiamo invocare il metodo TryGetValue(), che restituisce un booleano indicante l'esistenza o meno della chiave richiesta.

Di seguito, come è possibile modificare il metodo Get() utilizzando TryGetValue.

[HttpGet(“{key}”)]
public string Get(string key)
{
    string obj;
    if (!cache.TryGetValue<string>(key, out obj))
    
        obj = DateTime.Now.ToString();
        cache.Set<string>(key, obj);
    
    return obj;
}

Possiamo, in alternativa, scegliere il metodo GetOrCreate() che esegue il check sull'esistenza della chiave richiesta e, in caso negativo, la crea.

[HttpGet(“{key}”)]
public string Get(string key)
{
    return cache.GetOrCreate<string>(“key”,
        cacheEntry => {
            return DateTime.Now.ToString();
        });
}

Come impostare le politiche di scadenza dei dati in cache

La classe MemoryCacheEntryOptions ci mette a disposizione varie possibilità per poter gestire la scadenza dei dati cachati.

Possiamo indicare un tempo fisso, dopo il quale far scadere una determinata chiave (absolute expiry), oppure farla scadere, nel caso in cui non venga letta dopo un certo intervallo di tempo (sliding expiry). Inoltre, c'è la possibilità di creare dipendenze tra oggetti in cache utilizzando gli Expiration Token. Ecco qualche esempio:

//absolute expiration using TimeSpan
_cache.Set("key", item, TimeSpan.FromDays(1));

//absolute expiration using DateTime
_cache.Set("key", item, new DateTime(2020, 1, 1));

//sliding expiration (scade se non viene letta per 7 giorni)
_cache.Set("key", item, new MemoryCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromDays(7)
});

//use both absolute and sliding expiration
_cache.Set("key", item, new MemoryCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30),
    SlidingExpiration = TimeSpan.FromDays(7)
});

// This method adds a trigger to refresh the data from background
private void UpdateReset()
{
    var mo = new MemoryCacheEntryOptions();
    mo.RegisterPostEvictionCallback(RefreshAllPlacessCache_PostEvictionCallback);
    mo.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(TimeSpan.FromMinutes(35)).Token));
    Cache.Set(CACHE_KEY_PLACES_RESET, DateTime.Now, mo);
}

// Method triggered by the cancellation token that triggers the PostEvictionCallBack
private async void RefreshAllPlacesCache_PostEvictionCallback(object key, object value, EvictionReason reason, object state)
{
    // Regenerate a set of updated data
    var places = await GetLongGeneratingData();
    Cache.Set(CACHE_KEY_PLACES, places, TimeSpan.FromMinutes(40));

    // Re-set the cache to be reloaded in 35min
    UpdateReset();
}

Cache callback

Un'altra interessante feature utilizzabile tramite la classe MemoryCacheEntryOptions è quella che ci permette di registrare una callback che venga eseguita quando un item è rimosso dalla cache:

MemoryCacheEntryOptions cacheOption = new MemoryCacheEntryOptions()  
{  
    AbsoluteExpirationRelativeToNow = (DateTime.Now.AddMinutes(1) - DateTime.Now),  
};  
cacheOption.RegisterPostEvictionCallback(  
    (key, value, reason, substate) =>  
    {  
        Console.Write("Cache expired!");  
    }); 

Cache tramite Tag Helper

Esistono altre implementazioni dell’interfaccia IMemoryCache, che possono tornare molto utili. Ad esempio, in ambito Web, se utilizziamo il framework ASP.NET Core, possiamo memorizzare porzioni di pagina tramite l'uso del Tag helper cache. L'utilizzo di questo tag in una View è particolarmente semplice:

<cache>
<p>Ora: @DateTime.Now</p>
</cache>

Ad ogni successiva richiesta della pagina in cui è contenuto questo tag, il corpo del paragrafo verrà letto dalla cache. Naturalmente, il modo in cui l'abbiamo utilizzato ha la sola funzione di esempio, ma ne potete apprezzare le capacità quando provate a renderizzare un pezzo di pagina che richieda molte risorse. Generalmente, il candidato principale ad essere cachato è una chiamata ad un view component.

<cache expires-on="@TimeSpan.FromSeconds(600)">
    @await Component.InvokeAsync("BlogPosts", new { tag = "popular" })
</cache>

Nello snippet di codice, potete vedere anche come gestire il periodo di permanenza nella cache tramite l'attributo expires-on. Ne esistono altri due alternativi:

  • expires-after: da valorizzare con un TimeSpan per indicare un periodo di tempo, trascorso il quale, il contenuto dovrà essere rigenerato
  • expires-sliding: da valorizzare anch'esso con un TimeSpan indicante un periodo di inattività. Ogni volta che il contenuto viene letto dalla cache, la sua rimozione viene rimandata

Altro aspetto personalizzabile è la possibilità di configurare il criterio di inserimento in cache. Potremmo, infatti, avere l'esigenza di aggiornare il nostro oggetto cachato in base ad alcune variabili. Tali esigenze sono coperte dai seguenti attributi vary-by- :

  • vary-by-route: viene valorizzato con il nome di un route parameter, per esempio id, per indicare che il contenuto deve essere rigenerato al variare dell'id della risorsa che stiamo visualizzando
  • vary-by-query: il contenuto viene generato e messo in cache al variare della chiave querystring indicata
  • vary-by-user: va impostato a true quando visualizziamo dati specifici per l'utente loggato come ad esempio il riquadro del suo profilo, contenente il nome e la foto.
  • vary-by-header: per far variare la cache in base ad un intestazione della richiesta HTTP, come ad esempio "Accept-Language" se la stiamo usando per visualizzare contenuti in lingua;
  • vary-by-cookie: permette di far variare la cache in base al contenuto di un cookie, di cui dobbiamo indicare il nome

È possibile utilizzare uno o più attributi vary-by- per realizzare politiche di caching avanzate, ma, riprendendo una famosa citazione, "da grandi poteri derivano grandi responsabilità".

Conclusioni

L'utilizzo del caching in-memory consente di archiviare i dati nella memoria del server e ci aiuta a migliorare le prestazioni dell'applicazione, rimuovendo richieste non necessarie a origini dati esterne. Come abbiamo visto, è estremamente semplice da utilizzare e flessibile da configurare.

Vi ricordo che quest'approccio non è utilizzabile quando la vostra app è ospitata su più server o in un ambiente di hosting cloud. Descriveremo questo scenario nel prossimo articolo, dove parleremo del caching distribuito.

Alla prossima!

Autore

Servizi

Evolvi la tua azienda