2016-02-17 5 views
20

Sto lavorando all'applicazione API Web ASP.NET Core (ASP.NET 5) e devo implementare la memorizzazione nella cache HTTP con l'aiuto di tag di entità. In precedenza ho usato CacheCow per lo stesso, ma a quanto pare ora non supporta ASP.NET Core. Inoltre, non ho trovato altre librerie rilevanti o dettagli di supporto framework per lo stesso.Implementare la cache HTTP (ETag) nell'API Web principale di ASP.NET

Posso scrivere codice personalizzato per lo stesso, ma prima voglio vedere se qualcosa è già disponibile. Condividi se qualcosa è già disponibile e qual è il modo migliore per implementarlo.

Grazie mille in anticipo.

+3

secondo [questo] (http://blog.lesierse.com/2015/12/20/cache-busting-using-aspnet5.html) gli etags sono implementati per i file statici se si usa app.UseStaticFiles(). –

risposta

14

Dopo aver provato a farlo funzionare con il middleware, ho capito che MVC action filters sono effettivamente più adatti per questa funzionalità.

public class ETagFilter : Attribute, IActionFilter 
{ 
    private readonly int[] _statusCodes; 

    public ETagFilter(params int[] statusCodes) 
    { 
     _statusCodes = statusCodes; 
     if (statusCodes.Length == 0) _statusCodes = new[] { 200 }; 
    } 

    public void OnActionExecuting(ActionExecutingContext context) 
    { 
    } 

    public void OnActionExecuted(ActionExecutedContext context) 
    { 
     if (context.HttpContext.Request.Method == "GET") 
     { 
      if (_statusCodes.Contains(context.HttpContext.Response.StatusCode)) 
      { 
       //I just serialize the result to JSON, could do something less costly 
       var content = JsonConvert.SerializeObject(context.Result); 

       var etag = ETagGenerator.GetETag(context.HttpContext.Request.Path.ToString(), Encoding.UTF8.GetBytes(content)); 

       if (context.HttpContext.Request.Headers.Keys.Contains("If-None-Match") && context.HttpContext.Request.Headers["If-None-Match"].ToString() == etag) 
       { 
        context.Result = new StatusCodeResult(304); 
       } 
       context.HttpContext.Response.Headers.Add("ETag", new[] { etag }); 
      } 
     } 
    }   
} 

// Helper class that generates the etag from a key (route) and content (response) 
public static class ETagGenerator 
{ 
    public static string GetETag(string key, byte[] contentBytes) 
    { 
     var keyBytes = Encoding.UTF8.GetBytes(key); 
     var combinedBytes = Combine(keyBytes, contentBytes); 

     return GenerateETag(combinedBytes); 
    } 

    private static string GenerateETag(byte[] data) 
    { 
     using (var md5 = MD5.Create()) 
     { 
      var hash = md5.ComputeHash(data); 
      string hex = BitConverter.ToString(hash); 
      return hex.Replace("-", ""); 
     }    
    } 

    private static byte[] Combine(byte[] a, byte[] b) 
    { 
     byte[] c = new byte[a.Length + b.Length]; 
     Buffer.BlockCopy(a, 0, c, 0, a.Length); 
     Buffer.BlockCopy(b, 0, c, a.Length, b.Length); 
     return c; 
    } 
} 

E poi usarlo sulle azioni o controller che si desidera come un attributo:

[HttpGet("data")] 
[ETagFilter(200)] 
public async Task<IActionResult> GetDataFromApi() 
{ 
} 

l'importante distinzione tra Middleware e filtri è che il middleware può essere eseguito prima e dopo MVC middlware e solo può lavorare con HttpContext. Inoltre, una volta che MVC inizia a inviare la risposta al client, è troppo tardi per apportare eventuali modifiche.

I filtri d'altra parte fanno parte del middleware MVC. Hanno accesso al contesto MVC, con il quale in questo caso è più semplice implementare questa funzionalità. More on Filters e la loro pipeline in MVC.

+0

solo una nota per le persone che potrebbero pensare di usare questa risposta per le pagine web (invece di una API). non sembra tenere conto delle modifiche alla visualizzazione dei file. – jimasp

0

Ecco una versione più estesa per MVC (testato con anima asp.net 1.1):

using System; 
using System.IO; 
using System.Security.Cryptography; 
using System.Text; 
using System.Threading.Tasks; 
using Microsoft.AspNetCore.Http; 
using Microsoft.AspNetCore.Http.Extensions; 
using Microsoft.Net.Http.Headers; 

namespace WebApplication9.Middleware 
{ 
    // This code is mostly here to generate the ETag from the response body and set 304 as required, 
    // but it also adds the default maxage (for client) and s-maxage (for a caching proxy like Varnish) to the cache-control in the response 
    // 
    // note that controller actions can override this middleware behaviour as needed with [ResponseCache] attribute 
    // 
    // (There is actually a Microsoft Middleware for response caching - called "ResponseCachingMiddleware", 
    // but it looks like you still have to generate the ETag yourself, which makes the MS Middleware kinda pointless in its current 1.1.0 form) 
    // 
    public class ResponseCacheMiddleware 
    { 
     private readonly RequestDelegate _next; 
     // todo load these from appsettings 
     const bool ResponseCachingEnabled = true; 
     const int ActionMaxAgeDefault = 600; // client cache time 
     const int ActionSharedMaxAgeDefault = 259200; // caching proxy cache time 
     const string ErrorPath = "/Home/Error"; 

     public ResponseCacheMiddleware(RequestDelegate next) 
     { 
      _next = next; 
     } 

     // THIS MUST BE FAST - CALLED ON EVERY REQUEST 
     public async Task Invoke(HttpContext context) 
     { 
      var req = context.Request; 
      var resp = context.Response; 
      var is304 = false; 
      string eTag = null; 

      if (IsErrorPath(req)) 
      { 
       await _next.Invoke(context); 
       return; 
      } 


      resp.OnStarting(state => 
      { 
       // add headers *before* the response has started 
       AddStandardHeaders(((HttpContext)state).Response); 
       return Task.CompletedTask; 
      }, context); 


      // ignore non-gets/200s (maybe allow head method?) 
      if (!ResponseCachingEnabled || req.Method != HttpMethods.Get || resp.StatusCode != StatusCodes.Status200OK) 
      { 
       await _next.Invoke(context); 
       return; 
      } 


      resp.OnStarting(state => { 
       // add headers *before* the response has started 
       var ctx = (HttpContext)state; 
       AddCacheControlAndETagHeaders(ctx, eTag, is304); // intentional modified closure - values set later on 
       return Task.CompletedTask; 
      }, context); 


      using (var buffer = new MemoryStream()) 
      { 
       // populate a stream with the current response data 
       var stream = resp.Body; 
       // setup response.body to point at our buffer 
       resp.Body = buffer; 

       try 
       { 
        // call controller/middleware actions etc. to populate the response body 
        await _next.Invoke(context); 
       } 
       catch 
       { 
        // controller/ or other middleware threw an exception, copy back and rethrow 
        buffer.CopyTo(stream); 
        resp.Body = stream; // looks weird, but required to keep the stream writable in edge cases like exceptions in other middleware 
        throw; 
       } 



       using (var bufferReader = new StreamReader(buffer)) 
       { 
        // reset the buffer and read the entire body to generate the eTag 
        buffer.Seek(0, SeekOrigin.Begin); 
        var body = bufferReader.ReadToEnd(); 
        eTag = GenerateETag(req, body); 


        if (req.Headers[HeaderNames.IfNoneMatch] == eTag) 
        { 
         is304 = true; // we don't set the headers here, so set flag 
        } 
        else if (// we're not the only code in the stack that can set a status code, so check if we should output anything 
         resp.StatusCode != StatusCodes.Status204NoContent && 
         resp.StatusCode != StatusCodes.Status205ResetContent && 
         resp.StatusCode != StatusCodes.Status304NotModified) 
        { 
         // reset buffer and copy back to response body 
         buffer.Seek(0, SeekOrigin.Begin); 
         buffer.CopyTo(stream); 
         resp.Body = stream; // looks weird, but required to keep the stream writable in edge cases like exceptions in other middleware 
        } 
       } 

      } 
     } 


     private static void AddStandardHeaders(HttpResponse resp) 
     { 
      resp.Headers.Add("X-App", "MyAppName"); 
      resp.Headers.Add("X-MachineName", Environment.MachineName); 
     } 


     private static string GenerateETag(HttpRequest req, string body) 
     { 
      // TODO: consider supporting VaryBy header in key? (not required atm in this app) 
      var combinedKey = req.GetDisplayUrl() + body; 
      var combinedBytes = Encoding.UTF8.GetBytes(combinedKey); 

      using (var md5 = MD5.Create()) 
      { 
       var hash = md5.ComputeHash(combinedBytes); 
       var hex = BitConverter.ToString(hash); 
       return hex.Replace("-", ""); 
      } 
     } 


     private static void AddCacheControlAndETagHeaders(HttpContext ctx, string eTag, bool is304) 
     { 
      var req = ctx.Request; 
      var resp = ctx.Response; 

      // use defaults for 404s etc. 
      if (IsErrorPath(req)) 
      { 
       return; 
      } 

      if (is304) 
      { 
       // this will blank response body as well as setting the status header 
       resp.StatusCode = StatusCodes.Status304NotModified; 
      } 

      // check cache-control not already set - so that controller actions can override caching 
      // behaviour with [ResponseCache] attribute 
      // (also see StaticFileOptions) 
      var cc = resp.GetTypedHeaders().CacheControl ?? new CacheControlHeaderValue(); 
      if (cc.NoCache || cc.NoStore) 
       return; 

      // sidenote - https://tools.ietf.org/html/rfc7232#section-4.1 
      // the server generating a 304 response MUST generate any of the following header 
      // fields that WOULD have been sent in a 200(OK) response to the same 
      // request: Cache-Control, Content-Location, Date, ETag, Expires, and Vary. 
      // so we must set cache-control headers for 200s OR 304s 

      cc.MaxAge = cc.MaxAge ?? TimeSpan.FromSeconds(ActionMaxAgeDefault); // for client 
      cc.SharedMaxAge = cc.SharedMaxAge ?? TimeSpan.FromSeconds(ActionSharedMaxAgeDefault); // for caching proxy e.g. varnish/nginx 
      resp.GetTypedHeaders().CacheControl = cc; // assign back to pick up changes 

      resp.Headers.Add(HeaderNames.ETag, eTag); 
     } 

     private static bool IsErrorPath(HttpRequest request) 
     { 
      return request.Path.StartsWithSegments(ErrorPath); 
     } 
    } 
} 
0

Sto usando un middleware che funziona bene per me.

Aggiunge le intestazioni HttpCache alle risposte (Cache-Control, Expires, ETag, Last-Modified) e implementa i modelli di convalida della cache &.

Puoi trovarlo su nuget.org come pacchetto chiamato Marvin.Cache.Headers.

Si potrebbe trovare ulteriori informazioni dalla sua home page Github: https://github.com/KevinDockx/HttpCacheHeaders

+1

Le risposte di solo collegamento sono generalmente [disapprovate] (http://meta.stackexchange.com/a/8259/204922) su Stack Overflow. Con il tempo è possibile che i collegamenti atrofia e diventino non disponibili, il che significa che la tua risposta è inutile per gli utenti in futuro. Sarebbe meglio se potessi fornire i dettagli generali della tua risposta nel tuo post effettivo, citando il tuo link come riferimento. – herrbischoff

+0

@herrbischoff, ho aggiunto più dettagli alla mia risposta, spero che ora sia meglio. – JFE

0

Come un addendum al Erik Božič's answer ho trovato che l'oggetto HttpContext non riportava indietro lo StatusCode correttamente quando eredita da ActionFilterAttribute, e applicato a livello controller. HttpContext.Response.StatusCode era sempre 200, a indicare che probabilmente non era stato impostato da questo punto nella pipeline. Ero invece in grado di prendere lo StatusCode dal contesto ActionExecutedContext.Result.StatusCode.

+0

Un addendum è più adatto come commento. – ToothlessRebel

+0

@ToothlessRebel ha provato prima questo, troppo poco rep :( – cagefree

+0

Questo non è un motivo valido per aggirare il sistema in atto e postare un commento come risposta – ToothlessRebel

0

Edificio su Eric's answer, vorrei utilizzare un'interfaccia che potrebbe essere implementata su un'entità per supportare la codifica dell'entità. Nel filtro si aggiunge solo ETag se l'azione restituisce un'entità con questa interfaccia.

Ciò consente di essere più selettivi su quali entità vengono taggate e consente a ciascuna entità di controllare come viene generato il tag. Questo sarebbe molto più efficiente di serializzare tutto e creare un hash. Elimina anche la necessità di controllare il codice di stato. Potrebbe essere tranquillamente e facilmente aggiunto come filtro globale poiché si sta "optando-in" per la funzionalità implementando l'interfaccia sulla classe del modello.

public interface IGenerateETag 
{ 
    string GenerateETag(); 
} 

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] 
public class ETagFilterAttribute : Attribute, IActionFilter 
{ 
    public void OnActionExecuting(ActionExecutingContext context) 
    { 
    } 

    public void OnActionExecuted(ActionExecutedContext context) 
    { 
     var request = context.HttpContext.Request; 
     var response = context.HttpContext.Response; 

     if (request.Method == "GET" && 
      context.Result is ObjectResult obj && 
      obj.Value is IGenerateETag entity) 
     { 
      string etag = entity.GenerateETag(); 

      // Value should be in quotes according to the spec 
      if (!etag.EndsWith("\"")) 
       etag = "\"" + etag +"\""; 

      string ifNoneMatch = request.Headers["If-None-Match"]; 

      if (ifNoneMatch == etag) 
      { 
       context.Result = new StatusCodeResult(304); 
      } 

      context.HttpContext.Response.Headers.Add("ETag", etag); 
     } 
    } 
}