views:

836

answers:

4

I am trying to get [CompressFilter] working with donut caching and running into issues.

What happens is that the whole page gets cached and not just the donut. The source for the CompressFilter I am using is below. I changed this from the original source to use OnResultExecuted instead of OnActionExecuting() because I needed access to the type of the result to avoid caching certain ActionResult subclasses.

Looking at the actual MVC v1 source code for OutputCacheAttribute it looks like it also is using OnResultExecuted(), but I dont think that fact directly is causing the conflict.

I don't know enough about how substitution caching works to understand quite why it behaves the way it does. I think it is notable to say though that this does not end up with any kind of corrupted display. It just behaves like there is no donut!

Its looking like I will have to use some kind of IIs 'plug-in' to handle caching, which I really wanted to avoid having to do, but its looking like I need donut caching too.

I'm actually more interested right now to know why it has this effect, but a solution if possible would be great too.

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);
        }
    }
}
A: 

Did you find a solution to this problem, i.e. were you able to combine donut caching with dynamic compression? I noticed that the source of OutputCacheAttribute uses OnResultExecutING instead of OnResultExecutED. Is that the problem?

chris166
@chris166 - i'm not sure yet. it took me long enough to discover there was a conflict here. caching for me added more problems than it solved so its completely off for now. it messed up authentication, compression and also my user tracking code (tracking cookies were being cached too!). donut caching only seems practical for really small strings (see my other question http://stackoverflow.com/questions/866634). until i can find a better solution i'm just improving my DAL layer caching and turning off OutputCache. be very careful with it! good luck!
Simon_Weaver
+1  A: 

I override the OnResultExecuting method. This is called prior to rendering the ActionResult. Before checking to see if the client accepts compression, I check the type of Result I am trying to render. If it's not a ViewResult, I do not apply any sort of compress.

For this to work, your Actions have to explicitly call either View() or PartialView().

Here's what the CompressOutputAttrtibute looks like:

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);
     }
    }
}

Inside the controller:

[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);
    }
}
LaptopHeaven
+3  A: 

That is a bad implementation of the CompressFilter class.

Please read this: Finding Preferred Accept Encoding in C#

I have written my own that will obey the AcceptEncoding based on the above article::

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();
    }
}
John Gietzen
thanks. is acceptencoding the only differnece? i dont have time to compare them right now. thx again
Simon_Weaver
Yeah, pretty much.
John Gietzen
A: 

Why not use IIS' built-in compression filter instead?

ssg
because i'm using IIS6 and it wont compress files such as /products/1001 because they dont end with .aspx. i'm not completely sure if the limitation exists in IIS7 but definitely in IIS6 it does
Simon_Weaver
Apparently you can set HcScriptFileExtensions to an empty string. (See also the performance note though) http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/d85ae418-e96e-4da1-aaee-5da1dd3e96d8.mspx?mfr=true
ssg