CACHING IN ASP.NET CORE

NET CORECome abbiamo visto collegarsi a un database richiede un certo tempo, migliori sono le risorse hardware del server su cui risiede l’applicazione più questo tempo sarà contenuto. Per quanto possa essere potente una macchina, c’è comunque un limite fisico che verrà raggiunto con tanti utenti che utilizzano l’applicazione. Quando si arriva a ciò ci sono diverse soluzioni da mettere in pratica come ad esempio la scalabilità verticale, oppure orizzontale, mettiamo due macchine di modo che entrambe possano servire gli utenti. La soluzione della scalabilità funziona, tuttavia la nostra applicazione richiede maggiori risorse e quindi un maggior costo. Prima di arrivare a ciò dobbiamo prendere alcuni accorgimenti come, ad esempio, usare il servizio di caching.

Caching

Supponiamo che un utente faccia una richiesta per la lista dei corsi, la richiesta arriva al server che la gira al database. Ora però possiamo usare un piccolo stratagemma, siccome ci saranno più utenti che cercheranno la lista dei corsi possiamo mettere queste informazioni nella memoria RAM del server dove l’accesso per il secondo utente è decisamente più rapido. Il problema è che i dati potrebbero non essere aggiornati, ma questo lo è raramente perché gli oggetti in cache scadranno e siamo noi a decidere come e quando farli scadere.

Scadenza cache

Vediamo le caratteristiche dell’oggetto IMemoryCache.

IMemoryCache

Il metodo GetOrCreate funziona in questo modo. Se esiste la chiave che identifica l’oggetto, allora esso viene recuperato dalla RAM, se non esiste allora forniamo una lambda impostando la validità, 60 secondi nella slide e soprattutto dobbiamo fornire l’istanza dell’oggetto da mettere in cache. L’oggetto può essere di qualsiasi tipo, anche un tipo primitivo. GetOrCreate è Thread safe, cioè è in grado di gestire due Thread che tentano di accedere allo stesso oggetto. Vediamo un pattern per implementare il servizio di caching.

Decorator Pattern

Questo pattern indica semplicemente che anziché costruire il caching nel componente AdoNetCourseService, cioè usare li IMemoryCache, è il servizio che dovremo costruire che va ad inglobare AdoNetCourseService, così se un determinato oggetto è presente in cache lo trarremo da li, altrimenti accederemo al componente.

Controller

Se in futuro avremo bisogno di un CoursesAdminController con dati aggiornati, allora non lo faremo dipendere dal servizio di caching.

ICachedCourseService

Vediamo l’implementazione.

MemoryCachedCourseService
Occupazione Ram

METTERE IN CACHE L’INTERO CONTENUTO DI PAGINA CON RESPONSE CACHE

ASP.NET Core è in grado di mettere in cache anche il risultato del codice HTML prodotto da una View Razor, esattamente il codice HTML inviato al Browser dell’utente. Dalla seconda richiesta in poi non verrà eseguito il codice del Controller e la sua Action. Per capire come funziona il Response Caching andiamo ad aprire il controller Home. La nostra Home Page presto sarà visualizzata da molti utenti, abbiamo tutto l’interesse affinché il contenuto della Home Page possa essere messo in Cache. Decoriamo l’action Index con ResponseCache, così facendo stiamo dichiarando che la View() Razor associata all’action Index può essere messa in cache. La durata è specificata da Duration, ad esempio 60 secondi, dopo di che la View verrà presa nuovamente dal server.

Response Cache

Supponiamo che ci sia un utente che tramite il suo Browser che fa una richiesta al percorso / (la Home). Quando la richiesta arriva al server se stiamo usando l’attributo ResponseCache, la risposta non viene messa in cache dal server ma il server stesso aggiunge un’intestazione Cache-Control: public,max-age=60. Questa intestazione è oro per il Browser che leggendola potrà mettere in cache il contenuto della Home. Quando l’utente visiterà di nuovo la Home, questa verrà tratta dalla cache del Browser e si caricherà istantaneamente. Come sviluppatori non essendo la Response-Cache nella memoria dell’applicazione, non possiamo invalidare la cache prima della sua scadenza.

Dispositivi

Ci possono essere vari dispositivi che mettono in cache la risposta; infatti, non è detto che tra utente e server ci sia solo il Browser, la domanda è, quale è lo scopo di mettere in cache la risposta su tutti questi dispositivi? Se il Browser può servire un solo utente, un Proxy tutti i dipendenti di un’azienda ad esempio, comunque se non vogliamo che questi dispositivi intermedi mettano in cache la risposta basta usare l’attributo Location = ResponseCacheLocation.Client. Per non usare valori cablati nel codice dobbiamo scrivere un profilo:

[ResponseCache(CacheProfileName = “Home“)]

HomeProfile

Vediamo il file appsettings.json.

appsettings.json

MIGLIORARE I TEMPI DI RISPOSTA CON IL RESPONSE CACHING MIDDLEWARE

Adesso andremo a vedere come grazie ad un Middleware di ASP.NET Core sia possibile introdurre la cache precedentemente vista, all’interno della nostra applicazione. Questo perché la cache del browser riesce a migliorare l’esperienza dell’utente solo dalla seconda volta che visita la pagina, se invece teniamo la cache a livello di server già dalla prima risposta ci sarà un miglioramento per tutti gli utenti.

ResponseCaching
Response Caching Middleware

Il Middleware funziona in questo modo. Quando arriva una richiesta da parte dell’utente ad esempio per la Home Page, il Middleware si chiede: Posso già dare io una risposta? La risposta per la prima volta è no, nelle richieste successive dipende dal fatto o meno che ci sia un’intestazione Cache-Control: public, max-age=60. Tale Middleware si comporta come uno di quei dispositivi che sta tra utente e server come abbiamo visto, infatti l’intestazione deve essere public. Supponiamo che ci siano tutti i presupposti per mettere in cache la risposta che quindi ritorna al client. La seconda richiesta fatta sempre per la Home Page viene servita direttamente dal Middleware. In questo caso la risposta è rapida perché non viene coinvolto MVC, il controller le action.

Pippo

Se facciamo due richieste, una http porta 5000 e una https porta 5001 le richieste sono diverse in base alla chiave che si calcola, quindi lo stesso contenuto HTML verrebbe messo in cache due volte. Questo potrebbe portare ad un aumento del consumo della memoria occupata dalla cache. Bisogna fare in modo che l’applicazione sia raggiungibile sempre da un’origine. Nel secondo caso i due indirizzi sono uguali perché nella chiave non vi è un riferimento al query string.

RISOLVERE IL PROBLEMA NELLA CONFIGURAZIONE

Per risolvere questo problema si usa nella configurazione un parametro VaryByQueryKeys, in modo che il Middleware tenga conto anche della query string. È un array dove per il momento abbiamo indicato solo page. Finora sembra tutto perfetto, ma ci sono dei difetti; infatti, di solito le applicazioni web mostrano delle pagine che non sono uguali per tutti gli utenti, ad esempio se ho fatto il login come Mario Rossi, un altro utente vedrà sempre Mario Rossi e questo ovviamente è sbagliato perché queste sono informazioni dinamiche; quindi, dobbiamo separare tutte le informazioni che sono uguali per tutti gli utenti da quelle che sono proprie dell’utente autenticato.

Svantaggi
Svantaggi

RIEPILOGO DEL SERVIZIO DI CACHING

La qualità di un’applicazione è in parte determinata dalle sue prestazioni: se un utente riesce a navigare velocemente all’interno del nostro sito, avrà meno ostacoli nel raggiungere il suo obiettivo e perciò la sua esperienza di utilizzo risulterà positiva.

Quando ci sono molti utenti contemporanei, le prestazioni della nostra applicazione potrebbero deteriorare, soprattutto se non abbiamo un server con delle caratteristiche hardware in grado di reggere il carico.

Prima di migliorare l’hardware del nostro server, però, dovremmo attuare degli accorgimenti che ci permettono di accontentare più utenti in maniera quasi “gratis”. Se sfruttiamo il servizio di caching di ASP.NET Core, infatti, possiamo evitare di interrogare il database a ogni richiesta e mostrare all’utente un risultato già pronto, recuperato dalla memoria RAM dell’applicazione. Così manterremo ottime prestazioni anche con molti utenti contemporanei ma dobbiamo accettare il compromesso che tale risultato possa non essere il più aggiornato possibile.

Il servizio IMemoryCache ci permette di “salvare” degli oggetti nella memoria RAM, in modo che poi possano essere recuperati in seguito grazie a una chiave che li rappresenta. Questo è vantaggioso perché la memoria RAM è incredibilmente più rapida da leggere rispetto a un database.

Gli oggetti che dovremo valutare di mettere in cache sono quelli che devono essere visualizzati tali e quali da molti utenti, come il nostro elenco di corsi. Nel seguente esempio, quindi, vediamo come ricevere il servizio IMemoryCache dal costruttore di un nostro servizio applicativo.

  1. public class MemoryCacheCourseService : ICachedCourseService
  2. {
  3. private readonly IMemoryCache memoryCache;
  4. private readonly ICourseService courseService;
  5. //Riceviamo il servizio IMemoryCache dal costruttore
  6. //E così anche il servizio applicativo che ci permette di recuperare i dati dal database
  7. public MemoryCacheCourseService(IMemoryCache memoryCache, ICourseService courseService)
  8. {
  9. //Conserviamo i riferimenti su campi privati
  10. this.memoryCache = memoryCache;
  11. this.courseService = courseService;
  12. }
  13. public Task<List<CourseViewModel>> GetCoursesAsync()
  14. {
  15. //Gli oggetti sono scritti e letti dalla cache in base a una chiave che li rappresenta
  16. string key = “Courses”;
  17. //Invochiamo il metodo GetOrCreateAsync per recuperare l’oggetto dalla cache.
  18. //Se non dovesse esistere, allora verrà eseguita la lambda che si occuperà
  19. //di recuperare l’oggetto dal database e di impostare una scadenza
  20. return memoryCache.GetOrCreateAsync(key, cacheEntry =>
  21. {
  22. //Imposto la scadenza di permanenza in cache (60 secondi da adesso)
  23. cacheEntry.SetAbsoluteExpiration(TimeSpan.FromSeconds(60));
  24. //Recupero l’oggetto usando il servizio applicativo che lo ottiene dal database
  25. //Tale oggetto verrà automaticamente messo in cache
  26. return courseService.GetCoursesAsync();
  27. });
  28. }
  29. }

Chiamando il metodo GetOrCreateAsync siamo stati in grado, in maniera atomica (cioè con una sola operazione), di controllare la presenza in cache di un oggetto identificato dalla chiave “Courses” e, nel caso in cui non fosse presente, posizionarlo in cache dopo averlo recuperato dal database.

Impostare una scadenza per l’oggetto vuol dire che verrà automaticamente rimosso dalla cache al raggiungimento di una certa data/ora. Ci sono due approcci che possiamo seguire.

  • Absolute expiration(mostrata nell’esempio) ci permette di indicare una precisa data/ora alla quale l’oggetto verrà rimosso dalla cache;
  • Sliding expiration ci permette invece di indicare una scadenza (es. 60 secondi da adesso) che verrà automaticamente prorogata fintanto che l’oggetto continua ad essere recuperato dalla cache.

Se vogliamo rimuovere un oggetto prima della sua scadenza, possiamo usare il metodo Remove del servizio IMemoryCache.

LIMITARE IL CONSUMO DI RAM

Di tanto in tanto, facciamo attenzione alla quantità di RAM occupata dalla nostra applicazione perché se teniamo troppi oggetti in cache potremmo correre il rischio di esaurirla. Eventualmente possiamo porre dei limiti per evitare che troppi oggetti finiscano in cache, come nel prossimo esempio, in cui impostiamo un limite di “1000” dal metodo ConfigureServices della classe Startup.

  1. services.Configure<MemoryCacheOptions>(options =>
  2. {
  3. options.SizeLimit = 1000;
  4. });

Ogni qualvolta aggiungiamo un oggetto in cache, ricordiamoci anche di indicare la sua “dimensione”. In questo esempio, la dimensione viene impostata a 1 e ciò vuol dire che potremo mettere in cache ancora 999 oggetti prima del raggiungimento del limite.

  1. //Mettiamo questo nella lambda passata come parametro a GetOrCreate
  2. SetSize(1);

RESPONSE CACHING

ASP.NET Core dispone anche di un meccanismo di response caching che permette ai browser o a qualsiasi altro dispositivo si trovi fra il server e l’utente di porre in cache l’intero contenuto HTML di una risposta. Questo può avvenire se nella risposta fornita dal server è presente un’intestazione Cache-Control che dà le istruzioni sul tempo e sulle modalità di permanenza in cache. Ecco un esempio di tale intestazione.

Cache-Control: public,max-age=60

Per emettere questa intestazione nella risposta, ci basta porre l’attributo ResponseCache in corrispondenza di un controller o di un’action, come si vede nel seguente esempio.

  1. //La risposta prodotta da questa action può restare in cache per 60 secondi
  2. [ResponseCache(Duration = 60)]
  3. public IActionResult Index()
  4. {
  5. ViewData[“Title”] = “Benvenuto su MyCourse!”;
  6. return View();
  7. }

In alternativa possiamo avvalerci di un profilo, in modo che le impostazioni di caching possano essere riutilizzate in più punti dell’applicazione e, soprattutto, possano essere tratte da valori di configurazione anziché essere cablate nel codice C#.

  1. [ResponseCache(CacheProfileName = “Home”)]
  2. public IActionResult Index()
  3. {
  4. ViewData[“Title”] = “Benvenuto su MyCourse!”;
  5. return View();
  6. }

Poi, nel metodo ConfigureServices della classe Startup, andiamo a modificare così la chiamata a AddMvc per definire il nostro profilo.

  1. services.AddMvc(options =>
  2. {
  3. Configuration.Bind(“ResponseCache:Home”, homeProfile);
  4. options.CacheProfiles.Add(“Home”, homeProfile);
  5. });

Ed ecco il frammento di configurazione in appsettings.json, da cui otteniamo i valori.

  1. “ResponseCache”: {
  2. “Home”: {
  3.   “Duration”: 60,
  4.   “Location”: “Client”,
  5.   “VaryByQueryKeys”: [“page”]
  6. }
  7. }

Ogni profilo di response caching è caratterizzato da questi valori:

  • Duration esprime la durata in secondi di permanenza in cache;
  • Location indica quali sono i dispositivi autorizzati a mettere il risultato in cache. Se impostato su “Client”, allora solo il browser è autorizzato, altrimenti se “Any” (il default), qualunque altro dispositivo è autorizzato (ad esempio un proxy aziendale). Se invece è impostato su “None” allora nessun dispositivo è autorizzato e di fatto stiamo disabilitando esplicitamente l’intero meccanismo di response caching;
  • VaryByQueryKeys indica i nomi delle varabili querystring che influenzeranno il contenuto di pagina. Dunque, il browser non potrà avvalersi della cache se cambia anche uno soltanto dei valori querystring indicati. Utile quando abbiamo elenchi paginati;
  • VaryByHeader indica i nomi di intestazioni della richiesta che influenzano il contenuto di pagina. Utili per esempio quando da uno stesso indirizzo forniamo testi tradotti nella lingua dell’utente in base all’intestazione Accept-Language che indica la sua lingua preferita.

ASP.NET Core offre anche un ResponseCachingMiddleware che è in grado di tenere in cache le risposte HTML, proprio come farebbero un browser o un proxy. Il middleware si abilita così dal metodo Configure della classe Startup.

  1. UseStaticFiles();
  2. //Aggiungiamolo DOPO il middleware dei file statici ma PRIMA del middleware di routing di MVC
  3. UseResponseCaching();
  4. UseMvc(…)

Internamente, il middleware si avvale del servizio MemoryCache di cui abbiamo già parlato.

E’ importante ricordare che il risultato in cache sarà lo stesso servito a tutti gli utenti e per questo non deve contenere informazioni specifiche che variano da utente a utente, come per esempio lo username o il contenuto del suo carrello. Su questo aspetto torneremo affrontando il tema dell’autenticazione.

LINK AL CODICE SU GITHUB

GITHUB

Scaricare il codice della sezione12 o clonare il repository GITHUB per avere a disposizione tutte le sezioni nel tuo editor preferito.