Nello script precedente (https://www.aspitalia.com/script/1285/Aumentare-Scalabilita-ASP.NET-Core-Web-API-Caching-Client-Side.aspx) abbiamo visto come il caching client side sia uno dei modi più efficaci per aumentare la scalabilità del nostro strato di servizi: quando è possibile, i client possono mantenere una cache locale delle risposte così da evitare del tutto la chiamata al server.
Il problema fondamentale di questo approccio, tuttavia, è che perdiamo il controllo di quanto memorizzato sul client, fintanto che non scada il TTL che abbiamo settato tramite l'header cache-control: se i dati dovessero cambiare, infatti, non abbiamo infatti un sistema per invalidare le cache dei client dal server. Quindi per forza di cose il TTL è tipicamente piuttosto basso, in modo da essere efficace per chiamate ripetute, ma comunque garantire un ragionevole refresh rate delle risposte. Questo fa sì che comunque i client, a intervalli regolari, invalideranno la loro cache, chiameranno il server e, pertanto, consumeremo banda.
Un sistema molto utile per mitigare questo problema è l'utilizzo di ETag, uno standard HTTP il cui principio di funzionamento è descritto nell'immagine in basso.
L'ETag è un header della risposta HTTP che contiene tipicamente un identificativo o un hash. Esso viene generato dal server e mantenuto in cache dal client. Quando il client effettuerà nuovamente la richiesta, invierà in allegato l'ETag all'interno dell'header If-None-Match. Se il valore corrisponde alla versione corrente sul server, quest'ultimo ritornerà una risposta di tipo 304 - Not Modified, che non contiene alcun body (e pertanto è molto leggera e veloce) e che istruirà il client a mantenere i dati precedenti in cache per un ulteriore TTL.
ASP.NET Core Web API non supporta direttamente questa funzionalità, ma possiamo implementarla facilmente grazie a un Action Filter come il seguente:
public class EnableETagAttribute : Attribute, IActionFilter { public void OnActionExecuted(ActionExecutedContext context) { var clientEtag = context.HttpContext.Request.Headers["If-None-Match"].FirstOrDefault(); var result = context.Result as ObjectResult; if (result == null) return; var serverEtag = JsonConvert.SerializeObject(result.Value).GetHashCode().ToString(); if (serverEtag == clientEtag) { context.Result = new StatusCodeResult(304); } context.HttpContext.Response.Headers.Add("Etag", serverEtag); } public void OnActionExecuting(ActionExecutingContext context) { } }
Questo filtro usa come ETag l'hash .NET della risposta, ma possiamo utilizzare qualsiasi logica per estrarre una stringa che sia univocamente verificabile. Come prima cosa, recuperiamo il contenuto dell'header If-None-Match dalla request e successivamente, calcoliamo l'hash di quanto restituito dalla Action. Se i due valori corrispondono, modifichiamo la risposta del server in un 304. Il valore dell'ETag è poi in ogni caso ritornato come header.
La peculiarità di questo filtro è che funziona con qualsiasi Action o Controller di Web API: per utilizzarlo, infatti, è sufficiente aggiungerlo, avendo cura di impostare anche il caching client side come abbiamo visto nel precedente script:
[HttpGet, EnableETag, ResponseCache(Location = ResponseCacheLocation.Any, Duration = 30)] public async Task<IEnumerable<string>> Get() { // .. altro codice qui .. }
Un aspetto importante da sottolineare, tuttavia, è che l'action viene comunque eseguita in ogni caso. Quindi questa tecnica ottimizza l'utilizzo di banda, ma non il carico lato server. Una possibile alternativa, potrebbe essere quella di mantenere uno storage in cache degli ETag validi, così da evitare interamente il calcolo della risposta. Ovviamente una soluzione di questo tipo non è però sempre attuabile e, soprattutto, va tipicamente gestita caso per caso nella logica della action stessa, invece che con un filtro esterno.
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Miglioramenti agli screen reader e al contrasto in Angular
Eseguire query manipolando le liste contenute in un oggetto mappato verso una colonna JSON
Popolare una classe a partire dal testo, con Semantic Kernel e ASP.NET Core Web API
Gestire i dati con Azure Cosmos DB Data Explorer
Referenziare un @layer più alto in CSS
Eseguire una query su SQL Azure tramite un workflow di GitHub
Creare una custom property in GitHub
Creare una libreria CSS universale: Immagini
Utilizzare il metodo CountBy di LINQ per semplificare raggruppamenti e i conteggi
Effettuare il log delle chiamate a function di GPT in ASP.NET Web API
Creare alias per tipi generici e tuple in C#
Usare i servizi di Azure OpenAI e ChatGPT in ASP.NET Core con Semantic Kernel