Создать ETag-фильтр в ASP.NET MVC - PullRequest
28 голосов
/ 10 июля 2011

Я хотел бы создать фильтр ETag в MVC. Проблема в том, что я не могу управлять Response.OutputStream, если бы я мог это сделать, я бы просто рассчитал ETag в соответствии с потоком результатов. Я делал это раньше в WCF, но не нашел простой идеи сделать это в MVC.

Я хочу иметь возможность написать что-то подобное

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

Есть идеи?

Ответы [ 4 ]

27 голосов
/ 11 июля 2011

Это лучшее, что я мог придумать, я не совсем понял, что вы имели ввиду, что не можете контролировать 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);
    }
}

Это должно работать, но это не такt.

Очевидно, что 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

14 голосов
/ 12 июля 2011

Большое спасибо, это именно то, что я искал. Просто сделал небольшое исправление для 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 голосов
/ 08 марта 2018

Есть много многообещающих ответов. Но ни одно из них не является полным решением. Также это не было частью вопроса, и никто не упомянул об этом. Но ETag должен использоваться для проверки кэша. Поэтому его следует использовать с заголовком Cache-Control . Таким образом, клиентам даже не нужно вызывать сервер, пока не истечет срок действия кэша (это может быть очень короткий период времени, зависит от вашего ресурса). Когда срок действия кэша истек, клиент делает запрос с помощью ETag и проверяет его. Подробнее о кешировании читайте в этой статье .

Вот мое решение для атрибутов CacheControl с ETags. Это может быть улучшено, например, с включенным Public cache и т. д. Однако я настоятельно рекомендую вам разобраться в кешировании и тщательно его изменить. Если вы используете 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)]
1 голос
/ 18 июня 2014

это код, который я создал, чтобы решить эту проблему - я наследую от 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();
    }
}
...