Ottimizzare l'utilizzo di banda tramite ETag in ASP.NET Core Web API

di Marco De Sanctis, in ASP.NET Core,

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

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi