Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

GZip/Deflate Compression in ASP.NET MVC


:P
On this page:

A long while back I wrote about GZip compression in ASP.NET. In that article I describe two generic helper methods that I've used in all sorts of ASP.NET application from WebForms apps to HttpModules and HttpHandlers that require gzip or deflate compression. The same static methods also work in ASP.NET MVC.

Here are the two routines:

/// <summary>
/// Determines if GZip is supported
/// </summary>
/// <returns></returns>
public static bool IsGZipSupported()
{
    string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];
    if (!string.IsNullOrEmpty(AcceptEncoding) &&
            (AcceptEncoding.Contains("gzip") || AcceptEncoding.Contains("deflate")))
        return true;
    return false;
}

/// <summary>
/// Sets up the current page or handler to use GZip through a Response.Filter
/// IMPORTANT:  
/// You have to call this method before any output is generated!
/// </summary>
public static void GZipEncodePage()
{
    HttpResponse Response = HttpContext.Current.Response;

    if (IsGZipSupported())
    {
        string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];

        if (AcceptEncoding.Contains("gzip"))
        {
            Response.Filter = new System.IO.Compression.GZipStream(Response.Filter,
                                        System.IO.Compression.CompressionMode.Compress);
            Response.Headers.Remove("Content-Encoding");
            Response.AppendHeader("Content-Encoding", "gzip");
        }
        else
        {
            Response.Filter = new System.IO.Compression.DeflateStream(Response.Filter,
                                        System.IO.Compression.CompressionMode.Compress);
            Response.Headers.Remove("Content-Encoding");
            Response.AppendHeader("Content-Encoding", "deflate");
        }
    }

    // Allow proxy servers to cache encoded and unencoded versions separately
    Response.AppendHeader("Vary", "Content-Encoding");
}

The first method checks whether the client sending the request includes the accept-encoding for either gzip or deflate, and if if it does it returns true. The second function uses IsGzipSupported() to decide whether it should encode content and uses an Response Filter to do its job. Basically response filters look at the Response output stream as it's written and convert the data flowing through it. Filters are a bit tricky to work with but the two .NET filter streams for GZip and Deflate Compression make this a snap to implement.

In my old code and even now in MVC I can always do:

public ActionResult List(string keyword=null, int category=0)
{
    WebUtils.GZipEncodePage();    …}

to encode my content. And that works just fine.

The proper way: Create an ActionFilterAttribute

However in MVC this sort of thing is typically better handled by an ActionFilter which can be applied with an attribute. So to be all prim and proper I created an CompressContentAttribute ActionFilter that incorporates those two helper methods and which looks like this:

/// <summary>
/// Attribute that can be added to controller methods to force content
/// to be GZip encoded if the client supports it
/// </summary>
public class CompressContentAttribute : ActionFilterAttribute
{

    /// <summary>
    /// Override to compress the content that is generated by
    /// an action method.
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        GZipEncodePage();
    }

    /// <summary>
    /// Determines if GZip is supported
    /// </summary>
    /// <returns></returns>
    public static bool IsGZipSupported()
    {
        string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];
        if (!string.IsNullOrEmpty(AcceptEncoding) &&
                (AcceptEncoding.Contains("gzip") || AcceptEncoding.Contains("deflate")))
            return true;
        return false;
    }

    /// <summary>
    /// Sets up the current page or handler to use GZip through a Response.Filter
    /// IMPORTANT:  
    /// You have to call this method before any output is generated!
    /// </summary>
    public static void GZipEncodePage()
    {
        HttpResponse Response = HttpContext.Current.Response;

        if (IsGZipSupported())
        {
            string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];

            if (AcceptEncoding.Contains("gzip"))
            {
                Response.Filter = new System.IO.Compression.GZipStream(Response.Filter,
                                            System.IO.Compression.CompressionMode.Compress);
                Response.Headers.Remove("Content-Encoding");
                Response.AppendHeader("Content-Encoding", "gzip");
            }
            else
            {
                Response.Filter = new System.IO.Compression.DeflateStream(Response.Filter,
                                            System.IO.Compression.CompressionMode.Compress);
                Response.Headers.Remove("Content-Encoding");
                Response.AppendHeader("Content-Encoding", "deflate");
            }


        }

        // Allow proxy servers to cache encoded and unencoded versions separately
        Response.AppendHeader("Vary", "Content-Encoding");
    }
}

It's basically the same code wrapped into an ActionFilter attribute, which intercepts requests MVC requests to Controller methods and lets you hook up logic before and after the methods have executed. Here I want to override OnActionExecuting() which fires before the Controller action is fired.

With the CompressContentAttribute created, it can now be applied to either the controller as a whole:

[CompressContent]
public class ClassifiedsController : ClassifiedsBaseController
{ … } 

or to one of the Action methods:

[CompressContent]    
public ActionResult List(string keyword=null, int category=0)
{ … }

The former applies compression to every action method, while the latter is selective and only applies it to the individual action method.

Is the attribute better than the static utility function? Not really, but it is the standard MVC way to hook up 'filter' content and that's where others are likely to expect to set options like this. In fact,  you have a bit more control with the utility function because you can conditionally apply it in code, but this is actually much less likely in MVC applications than old WebForms apps since controller methods tend to be more focused.

Compression Caveats

Http compression is very cool and pretty easy to implement in ASP.NET but you have to be careful with it - especially if your content might get transformed or redirected inside of ASP.NET. A good example, is if an error occurs and a compression filter is applied. ASP.NET errors don't clear the filter, but clear the Response headers which results in some nasty garbage because the compressed content now no longer matches the headers. Another issue is Caching, which has to account for all possible ways of compression and non-compression that the content is served. Basically compressed content and caching don't mix well. I wrote about several of these issues in an old blog post and I recommend you take a quick peek before diving into making every bit of output Gzip encoded.

None of these are show stoppers, but you have to be aware of the issues.

Related Posts

Posted in ASP.NET  MVC  

The Voices of Reason


 

Mike Chaliy
April 28, 2012

# re: GZip/Deflate Compression in ASP.NET MVC

But IIS already can do this. Just install dynamic content compression module.

Rick Strahl
April 28, 2012

# re: GZip/Deflate Compression in ASP.NET MVC

@Mike - yes IIS 7 can do this but it's all or nothing. You don't need or want this on all content - I don't care about compression for 10k or less files, but I do for those handful of requests that serve 50k or more. IIS dynamic compression also has its own set of quirks - I've had lots of issues to even get this to work consistently.

Ray
April 28, 2012

# re: GZip/Deflate Compression in ASP.NET MVC

I haven't tried this but there's also an Order property on the ActionFilter where you can specify precedence, which might the caveat you mention when combining this with outputcaching.

Phil Bolduc
April 30, 2012

# re: GZip/Deflate Compression in ASP.NET MVC

Is there a reason you did not use ActionExecutingContext.HttpContext property over accessing HttpContext.Current? I could see the IsGZipSupported() and GZipEncodePage() be extension methods on HttpContextBase. Although, I probably rename GZipEncodePage() to GZipEncodeResponse().

Dan Napierski
November 19, 2014

# re: GZip/Deflate Compression in ASP.NET MVC

Great article. Thank you.

I had to make the following change to have it work on my development machine:

if (HttpRuntime.UsingIntegratedPipeline)
{
response.Headers.Remove("Content-Encoding");
}

Rick Strahl
November 19, 2014

# re: GZip/Deflate Compression in ASP.NET MVC

@Dan - not sure why the bracketing would be required? Headers.Remove() will remove the headers if the value exists, otherwise it does nothing so either way it would work.

Phil0001
January 05, 2015

# re: GZip/Deflate Compression in ASP.NET MVC

The 'Vary' HTTP header is used to indicate the set of *request-header* fields that fully determines, while the response is fresh, whether a cache is permitted to use the response to reply to a subsequent request.

The header you've used is a response header. It should probably be Accept-Encoding

Fixbug Tester
June 29, 2016

# re: GZip/Deflate Compression in ASP.NET MVC

Say using this on controller and there is 500 error on some action (may be due to improper values sent from browser ajax request), the it displays/returns unreadable characters.

Let me know if any fix, in-case there is 4xx, 5xx errors.

Thanks

Rick Strahl
June 29, 2016

# re: GZip/Deflate Compression in ASP.NET MVC

@Fixbug - if you're using any error filters or ASP.NET global error handlers you should ensure that those are removing Response.Filter - this is good practice regardless of this implementation. Otherwise use an exception handler on the controller to remove the filter if there's an unhandled exception. I guess we could probably add that to this filter implementation to automatically remove compression.

Austin Salgat
October 15, 2018

# re: GZip/Deflate Compression in ASP.NET MVC

For those curious, here is a version updated for ASP.NET Web Api 2 (the CompressedContent class is copied from: https://github.com/WebApiContrib/WebAPIContrib/blob/master/src/WebApiContrib/Content/CompressedContent.cs).

    // SOURCE: https://weblog.west-wind.com/posts/2012/Apr/28/GZipDeflate-Compression-in-ASPNET-MVC
    /// <summary>
    /// Attribute that can be added to controller methods to force content
    /// to be GZip encoded if the client supports it
    /// </summary>
    public class CompressContentAttribute : ActionFilterAttribute
    {

        /// <summary>
        /// Override to compress the content that is generated by
        /// an action method.
        /// </summary>
        /// <param name="filterContext"></param>
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            GZipEncodePage(actionExecutedContext);
        }

        /// <summary>
        /// Determines if GZip is supported
        /// </summary>
        /// <returns></returns>
        public static bool IsGZipSupported(HttpActionExecutedContext actionExecutedContext)
        {
            var acceptEncodings = actionExecutedContext.Request.Headers.AcceptEncoding.Select(entry => entry.Value).ToList();
            return acceptEncodings.Contains("gzip") || acceptEncodings.Contains("deflate");
        }

        /// <summary>
        /// Sets up the current page or handler to use GZip through a Response.Filter
        /// IMPORTANT:  
        /// You have to call this method before any output is generated!
        /// </summary>
        public static void GZipEncodePage(HttpActionExecutedContext actionExecutedContext)
        {
            var response = actionExecutedContext.Response;
            if (response == null) return;

            if (IsGZipSupported(actionExecutedContext))
            {
                var acceptEncoding = actionExecutedContext.Request.Headers.AcceptEncoding.Select(entry => entry.Value);

                response.Content = acceptEncoding.Contains("gzip") ? 
                    new CompressedContent(response.Content, "gzip") : 
                    new CompressedContent(response.Content, "deflate");
            }

            // Allow proxy servers to cache encoded and unencoded versions separately
            response.Headers.Add("Vary", "Content-Encoding");
        }
    }

Ahmed
December 06, 2018

# re: GZip/Deflate Compression in ASP.NET MVC

Dear Rick, Thanks a lot for your post really it is too useful for me. i just have a question , if this approach will fit to dynamic and static compression or for dynamic only? What should i do to handle static compression ? do i have to handle it through IIS ?


Rick Strahl
December 09, 2018

# re: GZip/Deflate Compression in ASP.NET MVC

@ahmed - this is for programmatic compression.

Static and Dynamic compression are configured in IIS. If you're using dynamic compression in IIS then you probably don't want to use programmatic compression as well - one or the other.

Here's another blog post that talks about those options: https://weblog.west-wind.com/posts/2011/May/05/Builtin-GZipDeflate-Compression-on-IIS-7x


Hamed Hossani
February 12, 2019

# re: GZip/Deflate Compression in ASP.NET MVC

Hi,
Is This better way or setting httpCompression in web.config?
related this link: https://stackoverflow.com/questions/25684575/how-to-enable-http-compression-to-compress-json-documents


Rick Strahl
February 12, 2019

# re: GZip/Deflate Compression in ASP.NET MVC

@Hamed - depends on what you want to do. If you want to compress all your content the Web.config setting is the right choice. I don't think it's a good idea to compress everything - I think it makes sense only for large documents that are serving tons of data down to the client. If you have pages that are relatively small (under 10k) it's probably not worth compressing.

I tend to use compression on specific pages that are either large or high volume.


Jo
February 28, 2019

# re: GZip/Deflate Compression in ASP.NET MVC

Hi Rick, Thank you for this code, I use it to compress some of my web API methods.
But what if someone posts compressed data to my web API? I can't find a way do decompress POST's to my web api. How can I achieve this? My method is like public ResponseDto Sync(InputDto input) If I post compressed data to this method, the input-parameter is null. If I post uncompressed data, the parameter is filled in. Thanks for your reply and your time.


Sachin Singh
August 09, 2020

# re: GZip/Deflate Compression in ASP.NET MVC

It only works for html , what should i do to compress css and javacript. Google page insights checks everything so i am getting "F" grade for Gzip compression.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024