Implement HTTP Cache (ETag) in ASP.NET Core Web API

后端 未结 6 1533
悲&欢浪女
悲&欢浪女 2020-12-13 18:44

I am working on ASP.NET Core (ASP.NET 5) Web API application and have to implement HTTP Caching with the help of Entity Tags. Earlier I used CacheCow for the same but it see

6条回答
  •  遥遥无期
    2020-12-13 18:58

    Here's a more extensive version for MVC Views (tested with asp.net core 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);
            }
        }
    }
    

提交回复
热议问题