2016-02-17 6 views
20

Ich arbeite an ASP.NET Core (ASP.NET 5) Web API-Anwendung und muss HTTP-Caching mit Hilfe von Entity Tags implementieren. Früher habe ich CacheCow für das gleiche verwendet, aber es scheint, dass es ASP.NET Core ab sofort nicht unterstützt. Ich fand auch keine anderen relevanten Bibliotheks- oder Framework-Support-Details für dasselbe.Implementierung von HTTP-Cache (ETag) in ASP.NET Core Web API

Ich kann benutzerdefinierten Code für das gleiche schreiben, aber vorher möchte ich sehen, ob etwas bereits verfügbar ist. Teilen Sie uns bitte mit, wenn etwas bereits verfügbar ist und was ist der beste Weg, dies zu implementieren.

Vielen Dank im Voraus.

+3

Laut [this] (http://blog.lesierse.com/2015/12/20/cache-busting-using-aspnet5.html) werden Etags für statische Dateien implementiert, wenn app.UseStaticFiles() verwendet wird. –

Antwort

14

Nachdem ich eine Weile versucht habe, es mit Middleware arbeiten zu lassen, habe ich herausgefunden, dass MVC action filters tatsächlich besser für diese Funktionalität geeignet sind.

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; 
    } 
} 

und dann verwenden, auf die Aktionen oder Controller Sie als Attribut wollen:

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

Der wichtige Unterschied zwischen Middleware und Filter ist, dass Ihre Middleware vor und nach dem MVC middlware laufen und kann nur arbeite mit HttpContext. Auch wenn MVC beginnt, die Antwort an den Client zu senden, ist es zu spät, um Änderungen daran vorzunehmen.

Filter auf der anderen Seite sind ein Teil von MVC Middleware. Sie haben Zugriff auf den MVC-Kontext, mit dem es in diesem Fall einfacher ist, diese Funktionalität zu implementieren. More on Filters und ihre Pipeline in MVC.

+0

nur eine Anmerkung für Leute, die vielleicht denken, diese Antwort für Webseiten zu verwenden (anstelle einer API). Änderungen werden anscheinend nicht berücksichtigt, um Dateien anzuzeigen. – jimasp

0

Hier ist eine umfangreichere Version für MVC (mit asp.net Kern getestet 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

ich eine Middleware verwende, die für mich gut funktioniert.

Er fügt den Antworten HttpCache-Header hinzu (Cache-Control, Expires, ETag, Last-Modified) und implementiert Cache-Ablaufdaten & Validierungsmodelle.

Sie können es auf nuget.org als Paket Marvin.Cache.Headers finden.

Sie könnten mehr Informationen aus seiner Github-Homepage finden: https://github.com/KevinDockx/HttpCacheHeaders

+1

Link-only-Antworten werden in der Regel auf Stack Overflow ignoriert (http://meta.stackexchange.com/a/8259/204922). Mit der Zeit ist es möglich, dass Links verkümmern und nicht mehr verfügbar sind, was bedeutet, dass Ihre Antwort in Zukunft für die Benutzer nutzlos ist. Es wäre am besten, wenn Sie die allgemeinen Details Ihrer Antwort in Ihrem tatsächlichen Beitrag bereitstellen könnten, indem Sie Ihren Link als Referenz angeben. – herrbischoff

+0

@herrbischoff, ich habe meiner Antwort weitere Details hinzugefügt, hoffe, dass es jetzt besser ist. – JFE

0

Als Nachtrag zu Erik Božič's answer finde ich, dass das Httpcontext-Objekt den Statuscode nicht korrekt berichtet zurück, wenn sie von Action vererben, und angewandten Controller weit. HttpContext.Response.StatusCode war immer 200, was darauf hinweist, dass er wahrscheinlich nicht von diesem Punkt in der Pipeline gesetzt wurde. Ich konnte stattdessen den StatusCode von ActionExecutedContext context.Result.StatusCode abrufen.

+0

Ein Addendum ist besser als Kommentar geeignet. – ToothlessRebel

+0

@ToothlessRebel versuchte das zuerst, zu wenig rep :( – cagefree

+0

Das ist kein gültiger Grund, das System zu umgehen und einen Kommentar als Antwort zu posten. – ToothlessRebel

0

Aufbauend auf Eric's answer, würde ich eine Schnittstelle verwenden, die auf einer Entität implementiert werden könnte, um Entitätskennzeichnung zu unterstützen. Im Filter würden Sie das ETag nur hinzufügen, wenn die Aktion eine Entität mit dieser Schnittstelle zurückgibt.

Dies ermöglicht es Ihnen selektiver zu sein, welche Entitäten getaggt werden und ermöglicht es Ihnen, dass jede Entität steuert, wie ihr Tag generiert wird. Dies wäre viel effizienter, als alles zu serialisieren und einen Hash zu erzeugen. Außerdem muss der Statuscode nicht mehr überprüft werden. Es kann sicher und einfach als globaler Filter hinzugefügt werden, da Sie sich der Funktionalität durch Implementierung der Schnittstelle in Ihrer Modellklasse "anschließen".

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); 
     } 
    } 
}