Compressing Web API responses to reduce payload

The abundance and easy availability of CPU at the expense of network bandwidth can be a good reason to use content compression in Web API for faster responses and improved performance

webapicontentcompression
Joydip Kanjilal

Web API is the technology of choice for building RESTful web services in .Net. It supports many media types, with application/json the one that's widely used. JSON is text based and lightweight and has already become a popular data exchange format for transmitting data over the wire. Compression is an easy way to reduce the network traffic and increase the speed of communication between a resource residing on a web server and a client or a consumer.

The two popular algorithms available to achieve this include GZip and Deflate. Both of these algorithms are recognized by the modern day browsers. Since JSON is text-based, it can be compressed using Gzip or Deflate compression to reduce the payload even further. This article presents an overview on how Web API responses can be compressed to reduce network traffic.

Reducing the payload

There are many ways in which you can compress content that is sent over the wire from your Web API. One way to compress content is by using IIS compression. Using IIS compression, you can perform compression of both static and dynamic content. I'll discuss IIS compression and its caveats in a post sometime later. For now, you can take a look at this nice article on IIS compression.

Another way to compress content in Web API is by using the Microsoft.AspNet.WebApi.MessageHandlers.Compression package. You can install it via NuGet and use it to compress content. To do this, you should add the following statement in the Register method of the WebApiConfig class.

GlobalConfiguration.Configuration.MessageHandlers.Insert(0, new ServerCompressionHandler(new GZipCompressor(), new DeflateCompressor()));

You can implement a custom delegating handler and attach it to the processing pipeline to compress content but I've used an action filter in the next section to demonstrate a simpler way of compressing content in Web API.

Implementing a custom action filter to compress content in Web API

It's quite easy to implement GZip encoding using the inbuilt GZipStream and DeflateStream classes. In this section, we will learn how we can use a custom ActionFilter to compress content. To create a custom action filter, create a class that extends the ActionFilterAttribute class as shown in the code snippet below.

public class CustomCompressionAttribute : ActionFilterAttribute

    {

        public override void OnActionExecuted(HttpActionExecutedContext actionContext)

        {

            //Write the custom implementation here

        }

    }

The next step is to implement the OnActionExecuted method and specify the compression strategies based on the value of the accept-encoding in the request header.

public override void OnActionExecuted(HttpActionExecutedContext actionContext)

        {

            bool isCompressionSupported = CompressionHelper.IsCompressionSupported();

            string acceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];

            if (isCompressionSupported)

            {

                var content = actionContext.Response.Content;

                var byteArray = content == null ? null : content.ReadAsByteArrayAsync().Result;

                MemoryStream memoryStream = new MemoryStream(byteArray);

                if (acceptEncoding.Contains("gzip"))

                {

                    actionContext.Response.Content = new ByteArrayContent(CompressionHelper.Compress(memoryStream.ToArray(), false));

                    actionContext.Response.Content.Headers.Remove("Content-Type");

                    actionContext.Response.Content.Headers.Add

("Content-encoding", "gzip");

                    actionContext.Response.Content.Headers.Add

("Content-Type", "application/json");

                }

                else

                {

                    actionContext.Response.Content = new ByteArrayContent(CompressionHelper.Compress

(memoryStream.ToArray()));

                    actionContext.Response.Content.Headers.Remove("Content-Type");

                    actionContext.Response.Content.Headers.Add("Content-encoding", "deflate");

                    actionContext.Response.Content.Headers.Add("Content-Type", "application/json");

                }

            }

            base.OnActionExecuted(actionContext);

        } 

Refer to the implementation of the OnActionExecuted method shown above. Note that we first checked if compression is supported using the IsCompressionSupported method of the CompressionHelper class. Here’s how the IsCompressionSupported method looks:

        public static bool IsCompressionSupported()

        {

            string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];

            return ((!string.IsNullOrEmpty(AcceptEncoding) &&

                    (AcceptEncoding.Contains("gzip") || AcceptEncoding.Contains("deflate"))));

        }

Next, we check if the value of "Accept-Encoding" in the request header and based on its value, we apply the compression logic and the assign the compressed content to the Response.Content property of the "HttpActionExecutedContext" instance named "actionContext". To compress the content, we take advantage of the Compress method of the CompressionHelper class. You can also use the DotNetZip library available via NuGet to perform the compression, but I’ve used the in-built compression GZipStream and DeflateStream classes available in the System.IO.Compression namespace. Here's the complete code of the CompressionHelper class for your reference:

 public class CompressionHelper

    {

        public static byte[] Compress(byte[] data, bool useGZipCompression = true)

        {

            System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Fastest;

            using (MemoryStream memoryStream = new MemoryStream())

            {

                if(useGZipCompression)

                {

                    using (System.IO.Compression.GZipStream gZipStream = new System.IO.Compression.GZipStream(memoryStream,

                   compressionLevel, true))

                    {

                        gZipStream.Write(data, 0, data.Length);

                    }

                }

                else

                {

                    using (System.IO.Compression.GZipStream gZipStream = new System.IO.Compression.GZipStream(memoryStream,

                    compressionLevel, true))

                    {

                        gZipStream.Write(data, 0, data.Length);

                    }

                }

               return memoryStream.ToArray();

            }

        }

        public static bool IsCompressionSupported()

        {

            string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];

            return ((!string.IsNullOrEmpty(AcceptEncoding) &&

                    (AcceptEncoding.Contains("gzip") || AcceptEncoding.Contains("deflate"))));

        }

    }     

Once the custom action filter has been implemented, you can apply the filter at the controller or the action level based on your needs. The following code snippet illustrates how this attribute can be applied at the action level in your controller class.

public class DefaultController : ApiController

    {

        [CustomCompression]

        public HttpResponseMessage Get()

        {

            var result = new List<string>();

            for (int i = 0; i < 50; i++)

                result.Add("Hello World");

            return Request.CreateResponse(result);

        }

    }

You can now execute your Web API controller’s action method to see the compression at work!

Copyright © 2017 IDG Communications, Inc.