首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在ETag核心Web中实现HTTP (ETag)

在ETag核心Web中实现HTTP (ETag)
EN

Stack Overflow用户
提问于 2016-02-17 13:59:26
回答 6查看 26.6K关注 0票数 35

我正在开发HTTP (ASP.NET 5) Web应用程序,并且必须借助实体标记来实现ASP.NET缓存。早些时候,我也使用了CacheCow,但目前看来它不支持ASP.NET核心。我也没有找到任何其他相关的库或框架支持同样的细节。

我可以为同样的代码编写自定义代码,但在此之前,我想看看是否已经有可用的东西了。如果已经有什么东西可用,那么请分享一下,有什么更好的方法来实现这一点。

EN

回答 6

Stack Overflow用户

回答已采纳

发布于 2016-11-06 22:00:05

经过一段时间的尝试,我发现MVC动作过滤器实际上更适合这个功能。

代码语言:javascript
复制
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;
    }
}

然后将其用于您想要的操作或控制器上,作为属性:

代码语言:javascript
复制
[HttpGet("data")]
[ETagFilter(200)]
public async Task<IActionResult> GetDataFromApi()
{
}

中间件和过滤器的重要区别在于,您的中间件可以在MVC中间件之前和之后运行,并且只能使用HttpContext。而且,一旦MVC开始将响应发送回客户端,对其进行任何更改都为时已晚。

另一方面,过滤器是MVC中间件的一部分。他们可以访问MVC上下文,在这种情况下,实现这个功能更简单。更多关于过滤器的信息及其管道在MVC中的应用。

票数 42
EN

Stack Overflow用户

发布于 2017-10-27 13:44:23

基于埃里克的回答,我将使用一个可以在实体上实现的接口来支持实体标记。在筛选器中,只有当操作返回具有此接口的实体时,才会添加ETag。

这允许您对哪些实体进行标记进行更多的选择,并允许每个实体控制如何生成其标记。这将比序列化所有内容和创建散列更有效。它还消除了检查状态代码的需要。它可以安全且容易地作为全局过滤器添加,因为您通过在模型类上实现接口来“选择”到功能中。

代码语言:javascript
复制
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);
        }
    }
}
票数 5
EN

Stack Overflow用户

发布于 2017-03-25 14:19:10

下面是一个更广泛的MVC视图版本(用asp.net核心1.1进行测试):

代码语言:javascript
复制
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);
        }
    }
}
票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/35458737

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档