29

我想在 MVC 中创建一个 ETag 过滤器。问题是我无法控制 Response.OutputStream,如果我能够做到这一点,我只需根据结果流计算 ETag。我以前在 WCF 中做过这件事,但在 MVC 中找不到任何简单的想法。

我希望能够写出这样的东西

[ETag]
public ActionResult MyAction()
{
    var myModel = Factory.CreateModel();
    return View(myModel);
}

任何的想法?

4

4 回答 4

27

这是我能想到的最好的方法,我真的不明白你无法控制 Response.OutputStream 的意思。

using System;
using System.IO;
using System.Security.Cryptography;
using System.Web.Mvc;

public class ETagAttribute : ActionFilterAttribute
{
    private string GetToken(Stream stream) {
        MD5 md5 = MD5.Create();
        byte [] checksum = md5.ComputeHash(stream);
        return Convert.ToBase64String(checksum, 0, checksum.Length);
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        filterContext.HttpContext.Response.AppendHeader("ETag", GetToken(filterContext.HttpContext.Response.OutputStream));
        base.OnResultExecuted(filterContext);
    }
}

这应该有效,但不是。

显然微软覆盖了 System.Web.HttpResponseStream.Read(Byte[] buffer, Int32 offset, Int32 count) 以便它返回“不支持指定的方法。”,不知道他们为什么会这样做,因为它继承了系统。 IO.Stream 基类...

混合了以下资源,Response.OutputStream 是一个只写流,所以我们必须使用 Response.Filter 类来读取输出流,有点奇怪,你必须在过滤器上使用过滤器,但是它有效=)

http://bytes.com/topic/c-sharp/answers/494721-md5-encryption-question-communication-java
http://www.codeproject.com/KB/files/Calculating_MD5_Checksum.aspx
http://blog。 gregbrant.com/post/Adding-Custom-HTTP-Headers-to-an-ASPNET-MVC-Response.aspx
http://www.infoq.com/articles/etags
http://www.w3.org/Protocols/ rfc2616/rfc2616-sec14.html

更新

经过多次战斗,我终于能够让它发挥作用:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Web;
using System.Web.Mvc;

public class ETagAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        try {
            filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response);
        } catch (System.Exception) {
            // Do Nothing
        };
    }
}

public class ETagFilter : MemoryStream {
    private HttpResponseBase o = null;
    private Stream filter = null;

    public ETagFilter (HttpResponseBase response) {
        o = response;
        filter = response.Filter;
    }

    private string GetToken(Stream stream) {
        byte[] checksum = new byte[0];
        checksum = MD5.Create().ComputeHash(stream);
        return Convert.ToBase64String(checksum, 0, checksum.Length);
    }

    public override void Write(byte[] buffer, int offset, int count) {
        byte[] data = new byte[count];
        Buffer.BlockCopy(buffer, offset, data, 0, count);
        filter.Write(data, 0, count);
        o.AddHeader("ETag", GetToken(new MemoryStream(data)));
    }
}

更多资源:

http://authors.aspalliance.com/aspxtreme/sys/Web/HttpResponseClassFilter.aspx
http://forums.asp.net/t/1380989.aspx/1

于 2011-07-11T02:27:15.997 回答
14

非常感谢,这正是我想要的。刚刚对 ETagFilter 做了一个小修复,如果内容没有改变,它将处理 304

public class ETagAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response, filterContext.RequestContext.HttpContext.Request);
    }
}

public class ETagFilter : MemoryStream
{
    private HttpResponseBase _response = null;
    private HttpRequestBase _request;
    private Stream _filter = null;

    public ETagFilter(HttpResponseBase response, HttpRequestBase request)
    {
        _response = response;
        _request = request;
        _filter = response.Filter;
    }

    private string GetToken(Stream stream)
    {
        byte[] checksum = new byte[0];
        checksum = MD5.Create().ComputeHash(stream);
        return Convert.ToBase64String(checksum, 0, checksum.Length);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        byte[] data = new byte[count];
        Buffer.BlockCopy(buffer, offset, data, 0, count);
        var token = GetToken(new MemoryStream(data));

        string clientToken = _request.Headers["If-None-Match"];

        if (token != clientToken)
        {
            _response.Headers["ETag"] = token;
            _filter.Write(data, 0, count);
        }
        else
        {
            _response.SuppressContent = true;
            _response.StatusCode = 304;
            _response.StatusDescription = "Not Modified";
            _response.Headers["Content-Length"] = "0";
        }
    }
}
于 2011-07-12T06:01:15.220 回答
2

有很多有希望的答案。但它们都不是完整的解决方案。这也不是问题的一部分,没有人提到它。但是ETag 应该用于缓存验证。因此它应该与 Cache-Control header 一起使用。因此,在缓存过期之前,客户端甚至不必调用服务器(这可能是非常短的时间,具体取决于您的资源)。当缓存过期时,客户端使用 ETag 发出请求并验证它。有关缓存的更多详细信息,请参阅这篇文章

这是我的带有 ETags 的 CacheControl 属性解决方案。它可以改进,例如启用公共缓存等...但是我强烈建议您了解缓存并仔细修改它。如果您使用 HTTPS 并且端点是安全的,那么这个设置应该没问题。

/// <summary>
/// Enables HTTP Response CacheControl management with ETag values.
/// </summary>
public class ClientCacheWithEtagAttribute : ActionFilterAttribute
{
    private readonly TimeSpan _clientCache;

    private readonly HttpMethod[] _supportedRequestMethods = {
        HttpMethod.Get,
        HttpMethod.Head
    };

    /// <summary>
    /// Default constructor
    /// </summary>
    /// <param name="clientCacheInSeconds">Indicates for how long the client should cache the response. The value is in seconds</param>
    public ClientCacheWithEtagAttribute(int clientCacheInSeconds)
    {
        _clientCache = TimeSpan.FromSeconds(clientCacheInSeconds);
    }

    public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
    {
        if (!_supportedRequestMethods.Contains(actionExecutedContext.Request.Method))
        {
            return;
        }
        if (actionExecutedContext.Response?.Content == null)
        {
            return;
        }

        var body = await actionExecutedContext.Response.Content.ReadAsStringAsync();
        if (body == null)
        {
            return;
        }

        var computedEntityTag = GetETag(Encoding.UTF8.GetBytes(body));

        if (actionExecutedContext.Request.Headers.IfNoneMatch.Any()
            && actionExecutedContext.Request.Headers.IfNoneMatch.First().Tag.Trim('"').Equals(computedEntityTag, StringComparison.InvariantCultureIgnoreCase))
        {
            actionExecutedContext.Response.StatusCode = HttpStatusCode.NotModified;
            actionExecutedContext.Response.Content = null;
        }

        var cacheControlHeader = new CacheControlHeaderValue
        {
            Private = true,
            MaxAge = _clientCache
        };

        actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{computedEntityTag}\"", false);
        actionExecutedContext.Response.Headers.CacheControl = cacheControlHeader;
    }

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

用法例如:使用 1 分钟客户端缓存:

[ClientCacheWithEtag(60)]
于 2018-03-08T09:36:32.300 回答
1

这是我为解决这个问题而创建的代码 - 我从 gzip 继承,因为我也想 gzip 流(你总是可以使用常规流)不同的是我为我的所有响应计算 etag 而不仅仅是块它。

public class ETagFilter : GZipStream
{
    private readonly HttpResponseBase m_Response;
    private readonly HttpRequestBase m_Request;
    private readonly MD5 m_Md5;
    private bool m_FinalBlock;



    public ETagFilter(HttpResponseBase response, HttpRequestBase request)
        : base(response.Filter, CompressionMode.Compress)
    {
        m_Response = response;
        m_Request = request;
        m_Md5 = MD5.Create();
    }

    protected override void Dispose(bool disposing)
    {
        m_Md5.Dispose();
        base.Dispose(disposing);
    }

    private string ByteArrayToString(byte[] arrInput)
    {
        var output = new StringBuilder(arrInput.Length);
        for (var i = 0; i < arrInput.Length; i++)
        {
            output.Append(arrInput[i].ToString("X2"));
        }
        return output.ToString();
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        m_Md5.TransformBlock(buffer, 0, buffer.Length, null, 0);
        base.Write(buffer, 0, buffer.Length);
    }

    public override void Flush()
    {
        if (m_FinalBlock)
        {
            base.Flush();
            return;
        }
        m_FinalBlock = true;
        m_Md5.TransformFinalBlock(new byte[0], 0, 0);
        var token = ByteArrayToString(m_Md5.Hash);
        string clientToken = m_Request.Headers["If-None-Match"];

        if (token != clientToken)
        {
            m_Response.Headers["ETag"] = token;
        }
        else
        {
            m_Response.SuppressContent = true;
            m_Response.StatusCode = 304;
            m_Response.StatusDescription = "Not Modified";
            m_Response.Headers["Content-Length"] = "0";
        }
        base.Flush();
    }
}
于 2014-06-18T11:30:10.710 回答