在ASP.NET MVC中创建ETag过滤器

时间:2011-07-10 17:55:28

标签: asp.net-mvc caching filter etag

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

我希望能够写出类似的东西

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

有什么想法吗?

4 个答案:

答案 0 :(得分: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);
    }
}

这应该有效,但事实并非如此。

显然Microsoft覆盖了System.Web.HttpResponseStream.Read(Byte []缓冲区,Int32偏移量,Int32计数),因此它返回“不支持指定方法。”,不知道为什么他们会这样做,因为它继承了System.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

答案 1 :(得分: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";
        }
    }
}

答案 2 :(得分:2)

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

这是我的带有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)]

答案 3 :(得分: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();
    }
}