Могу ли я использовать [CompressFilter] в ASP.NET MVC без нарушения кэширования пончиков

Я пытаюсь получить [CompressFilter], работая с кэшированием пончиков и сталкиваясь с проблемами.

Что происходит, так это то, что вся страница кэшируется, а не только пончик. Источник для CompressFilter, который я использую, приведен ниже. Я изменил это из исходного источника, чтобы использовать OnResultExecuted вместо OnActionExecuting(), потому что мне нужен был доступ к типу результата, чтобы избежать кеширования определенных Подклассы ActionResult.

Глядя на фактический исходный код MVC v1 для OutputCacheAttribute, похоже, что он также использует OnResultExecuted(), но я не думаю, что этот факт непосредственно вызывает конфликт.

Я не знаю достаточно о том, как работает кэширование подстановок, чтобы понять, почему оно ведет себя так, как оно делает. Я думаю, что можно сказать, что это не заканчивается каким-либо искаженным дисплеем. Это просто ведет себя, как нет пончика!

Похоже, мне придется использовать какой-то плагин IIs для обработки кеширования, чего я действительно хотел избежать, но его похоже, что мне нужно кэширование пончиков.

На самом деле я больше заинтересован в том, чтобы понять, почему он имеет этот эффект, но решение, если возможно, тоже будет большим.

public class CompressFilter : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        HttpRequestBase request = filterContext.HttpContext.Request;

        // dont encode images!
        if (filterContext.Result is ImageResult)
        {
            return;
        }

        string acceptEncoding = request.Headers["Accept-Encoding"];

        if (string.IsNullOrEmpty(acceptEncoding)) return;

        acceptEncoding = acceptEncoding.ToUpperInvariant();

        HttpResponseBase response = filterContext.HttpContext.Response;

        if (acceptEncoding.Contains("GZIP"))
        {
            response.AppendHeader("Content-encoding", "gzip");
            response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
        }
        else if (acceptEncoding.Contains("DEFLATE"))
        {
            response.AppendHeader("Content-encoding", "deflate");
            response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
        }
    }
}

Ответ 1

Это плохая реализация класса CompressFilter.

Прочтите это: Поиск предпочтительного кодирования Accept на С#

Я написал свой собственный, который будет подчиняться AcceptEncoding на основе вышеуказанной статьи:

public class CompressFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        HttpRequestBase request = filterContext.HttpContext.Request;

        string[] supported = new string[] { "gzip", "deflate" };

        IEnumerable<string> preferredOrder = new AcceptList(request.Headers["Accept-Encoding"], supported);

        string preferred = preferredOrder.FirstOrDefault();

        HttpResponseBase response = filterContext.HttpContext.Response;

        switch (preferred)
        {
            case "gzip":
                response.AppendHeader("Content-Encoding", "gzip");
                response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
                break;

            case "deflate":
                response.AppendHeader("Content-Encoding", "deflate");
                response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
                break;

            case "identity":
            default:
                break;
        }
    }
}

public class AcceptList : IEnumerable<string>
{
    Regex parser = new Regex(@"(?<name>[^;,\r\n]+)(?:;q=(?<value>[\d.]+))?", RegexOptions.Compiled);

    IEnumerable<string> encodings;

    public AcceptList(string acceptHeaderValue, IEnumerable<string> supportedEncodings)
    {
        List<KeyValuePair<string, float>> accepts = new List<KeyValuePair<string, float>>();

        if (!string.IsNullOrEmpty(acceptHeaderValue))
        {
            MatchCollection matches = parser.Matches(acceptHeaderValue);

            var values = from Match v in matches
                         where v.Success
                         select new
                         {
                             Name = v.Groups["name"].Value,
                             Value = v.Groups["value"].Value
                         };

            foreach (var value in values)
            {
                if (value.Name == "*")
                {
                    foreach (string encoding in supportedEncodings)
                    {
                        if (!accepts.Where(a => a.Key.ToUpperInvariant() == encoding.ToUpperInvariant()).Any())
                        {
                            accepts.Add(new KeyValuePair<string, float>(encoding, 1.0f));
                        }
                    }

                    continue;
                }

                float desired = 1.0f;
                if (!string.IsNullOrEmpty(value.Value))
                {
                    float.TryParse(value.Value, out desired);
                }

                if (desired == 0.0f)
                {
                    continue;
                }

                accepts.Add(new KeyValuePair<string, float>(value.Name, desired));
            }
        }

        this.encodings = from a in accepts
                         where supportedEncodings.Where(se => se.ToUpperInvariant() == a.Key.ToUpperInvariant()).Any() || a.Key.ToUpperInvariant() == "IDENTITY"
                         orderby a.Value descending
                         select a.Key;
    }

    IEnumerator<string> IEnumerable<string>.GetEnumerator()
    {
        return this.encodings.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)this.encodings).GetEnumerator();
    }
}

Ответ 2

Я переопределяю метод OnResultExecuting. Это вызывается до отображения ActionResult. Прежде чем проверять, принимает ли клиент сжатие, я проверяю тип результата, который я пытаюсь выполнить. Если это не ViewResult, я не применяю какой-либо компресс.

Чтобы это сработало, ваши действия должны явно вызвать либо View(), либо PartialView().

Вот что выглядит CompressOutputAttrtibute:

public class CompressOutputAttribute : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext) 
    {
        var result = filterContext.Result;
        if (!(result is ViewResult))
            return;

        HttpRequestBase request = filterContext.HttpContext.Request;
        string acceptEncoding = request.Headers["Accept-Encoding"];
        if (string.IsNullOrEmpty(acceptEncoding))
            return;

        acceptEncoding = acceptEncoding.ToUpperInvariant();

        HttpResponseBase response = filterContext.HttpContext.Response;
        if (acceptEncoding.Contains("GZIP"))
        {        
            // we want to use gzip 1st
            response.AppendHeader("Content-encoding", "gzip");
            //Add DeflateStream to the pipeline in order to compress response on the fly 
            response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
        }
        else if (acceptEncoding.Contains("DEFLATE"))
        {
            //If client accepts deflate, we'll always return compressed content 
            response.AppendHeader("Content-encoding", "deflate");
            //Add DeflateStream to the pipeline in order to compress response on the fly 
            response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
        }
    }
}

Внутри контроллера:

[CompressOutput]
public class ArticleController : Controller

    public PartialViewResult MostPopular()
    {
        var viewModel = ArticleMostPopularViewModel();
        viewModel.Articles = CmsService.GetMostPopularArticles();
        return PartialView(viewModel);
    }

    public ViewResult Show(int id)
    {
        var viewModel = ArticleShowViewModel();
        viewModel.Article = CmsService.GetArticle(id);
        return View(viewModel);
    }
}