2016-02-17 23 views
20

Pracuję nad aplikacją Web ASP.NET Core (ASP.NET 5) Web API i zaimplementuję buforowanie HTTP za pomocą znaczników encji. Wcześniej używałam CacheCow do tego samego, ale wygląda na to, że nie obsługuje on teraz ASP.NET Core. Nie znalazłem też żadnych innych przydatnych szczegółów dotyczących obsługi biblioteki lub struktury dla tego samego.Implementacja pamięci podręcznej HTTP (ETag) w ASP.NET Core Web API

Mogę napisać kod niestandardowy dla tego samego, ale wcześniej chcę sprawdzić, czy coś jest już dostępne. Uprzejmie się, jeśli coś jest już dostępne i jaki jest lepszy sposób na jego wdrożenie.

Dziękuję bardzo z góry.

+3

zgodnie z [this] (http://blog.lesierse.com/2015/12/20/cache-busting-using-aspnet5.html) etagi są implementowane dla plików statycznych, jeśli użyto app.UseStaticFiles(). –

Odpowiedz

14

Po chwili próbowania, aby działało z oprogramowaniem pośredniczącym, zorientowałem się, że MVC action filters są lepiej dostosowane do tej funkcji.

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

a następnie użyć go od działań lub kontrolerów chcesz jako atrybut:

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

Ważne rozróżnienie między Middleware i filtrów jest to, że middleware można uruchomić przed i po MVC middlware i może jedynie pracować z HttpContext. Także gdy MVC zacznie wysyłać odpowiedź z powrotem do klienta, jest już za późno, aby wprowadzić jakiekolwiek zmiany.

Filtry z drugiej strony są częścią oprogramowania pośredniego MVC. Mają dostęp do kontekstu MVC, w którym w tym przypadku łatwiej jest zaimplementować tę funkcjonalność. More on Filters i ich potok w MVC.

+0

po prostu wiadomość dla ludzi, którzy mogą myśleć o użyciu tej odpowiedzi na stronach internetowych (zamiast api). Wygląda na to, że nie uwzględnia zmian w wyświetlaniu plików. – jimasp

0

Oto szerszą wersję dla MVC (testowane z rdzeniem 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

Używam middleware, który działa dobrze dla mnie.

Dodaje nagłówki HttpCache do odpowiedzi (Cache-Control, Expires, ETag, Last-Modified) i implementuje modele walidacji wygasania pamięci podręcznej &.

Możesz go znaleźć na nuget.org jako pakiet o nazwie Marvin.Cache.Headers.

Można znaleźć więcej informacji z jego strony głównej GitHub: https://github.com/KevinDockx/HttpCacheHeaders

+1

Odpowiedzi dotyczące wyłącznie linków są ogólnie [mile widziane] (http://meta.stackexchange.com/a/8259/204922) w Stack Overflow. Z czasem linki do atrofii stają się niedostępne, co oznacza, że ​​twoja odpowiedź jest bezużyteczna dla użytkowników w przyszłości. Najlepiej byłoby, gdybyś mógł podać ogólne szczegóły swojej odpowiedzi w faktycznym poście, powołując się na link jako referencję. – herrbischoff

+0

@herrbischoff, dodałem więcej szczegółów do mojej odpowiedzi, mam nadzieję, że teraz jest lepiej. – JFE

0

Jako dodatek do Erik Božič's answer okazało się, że obiekt HttpContext nie raportowania z powrotem StatusCode poprawnie gdy dziedziczenie z ActionFilterAttribute i zastosowano regulator szerokości. HttpContext.Response.StatusCode był zawsze 200, wskazując, że prawdopodobnie nie został ustawiony przez ten punkt w potoku. Zamiast tego mogłem pobrać kod stanu z kontekstu ActionExecutedContext.Result.StatusCode.

+0

Dodatek lepiej nadaje się jako komentarz. – ToothlessRebel

+0

@ToothlessRebel próbował tego pierwszy, za mało rep :( – cagefree

+0

To nie jest ważny powód, aby obejść system w miejscu i wysłać komentarz jako odpowiedź – ToothlessRebel

0

Budując na Eric's answer, użyłbym interfejsu, który mógłby zostać zaimplementowany w jednostce w celu obsługi tagowania jednostek. W filtrze dodaje się tylko ETag, jeśli akcja zwraca obiekt z tym interfejsem.

Pozwala to na większą selektywność w zakresie oznaczania jednostek i pozwala każdej jednostce kontrolować sposób generowania tagu. Byłoby to o wiele bardziej wydajne niż serializowanie wszystkiego i tworzenie hasha. Eliminuje to również konieczność sprawdzania kodu statusu. Można go bezpiecznie i łatwo dodać jako filtr globalny, ponieważ "włączasz" się do funkcjonalności, implementując interfejs na klasie twojego modelu.

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); 
     } 
    } 
} 
Powiązane problemy