Search code examples
c#gzipwebclientgzipstreamwebclient.uploaddata

How to upload GZip compressed data using System.Net.WebClient in C#


In my project I need to upload large JSON data from a desktop app. So, I need it to be compressed.

I searched everywhere but I did not find a complex solution to my problem. So, I put several snippets together. See the answer below. I hope you find it useful.


Solution

  • I extended WebClient and shadow its UploadString method (all overloads):

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.IO.Compression;
    using System.Linq;
    using System.Net;
    using System.Text;
    
    namespace DesktopApp
    {
        public class ExtendedWebClient : WebClient
        {
            protected override WebRequest GetWebRequest(Uri uri)
            {
                WebRequest w = base.GetWebRequest(uri);
                w.Timeout = 60 * 60 * 1000;
                return w;
            }
    
            private byte[] GZipBytes(string data)
            {
                //Transform string into byte[]  
                byte[] ret = null;
    
                using (System.IO.MemoryStream outputStream = new System.IO.MemoryStream())
                {
                    using (GZipStream gzip = new GZipStream(outputStream, System.IO.Compression.CompressionMode.Compress))
                    {
                        //write to gzipper
                        StreamWriter writer = new StreamWriter(gzip);
                        writer.Write(data);
                        writer.Flush();
    
                        //write to output stream
                        gzip.Flush();
                        gzip.Close();
    
                        ret = outputStream.ToArray();
                    }
                }
    
                return ret;
            }
    
            /// <summary>
            /// Overriden method using GZip compressed data upload.
            /// </summary>
            /// <param name="address">Remote server address.</param>
            /// <param name="data">String data.</param>
            /// <returns>Server response string.</returns>
            public new string UploadString(string address, string data)
            {
                string ret = null;
                byte[] bytes = GZipBytes(data);
    
                this.Headers.Add("content-encoding", "gzip");
                bytes = base.UploadData(address, bytes);
                ret = System.Text.Encoding.UTF8.GetString(bytes);
    
                return ret;
            }
    
            /// <summary>
            /// Overriden method using GZip compressed data upload.
            /// </summary>
            /// <param name="address">Remote server URI.</param>
            /// <param name="data">String data.</param>
            /// <returns>Server response string.</returns>
            public new string UploadString(Uri address, string data)
            {
                string ret = null;
                byte[] bytes = GZipBytes(data);
    
                this.Headers.Add("content-encoding", "gzip");
                bytes = base.UploadData(address, bytes);
                ret = System.Text.Encoding.UTF8.GetString(bytes);
    
                return ret;
            }
    
            /// <summary>
            /// Overriden method using GZip compressed data upload.
            /// </summary>
            /// <param name="address">Remote server address.</param>
            /// <param name="method">HTTP method (e.g. POST, PUT, DELETE, GET).</param>
            /// <param name="data">String data.</param>
            /// <returns>Server response string.</returns>
            public new string UploadString(string address, string method, string data)
            {
                string ret = null;
                byte[] bytes = GZipBytes(data);
    
                this.Headers.Add("content-encoding", "gzip");
                bytes = base.UploadData(address, method,bytes);
                ret = System.Text.Encoding.UTF8.GetString(bytes);
    
                return ret;
            }
    
    
            /// <summary>
            /// Overriden method using GZip compressed data upload.
            /// </summary>
            /// <param name="address">Remote server URI.</param>
            /// <param name="method">HTTP method (e.g. POST, PUT, DELETE, GET).</param>
            /// <param name="data">String data.</param>
            /// <returns>Server response string.</returns>
            public new string UploadString(Uri address, string method, string data)
            {
                string ret = null;
                byte[] bytes = GZipBytes(data);
    
                this.Headers.Add("content-encoding", "gzip");
                bytes = base.UploadData(address, method, bytes);
                ret = System.Text.Encoding.UTF8.GetString(bytes);
    
                return ret;
            }
        }
    }
    

    You can use ExtendedWebClient like:

            using (ExtendedWebClient client = new ExtendedWebClient())
            {
                try
                {
                    //requestData object represents any data you need to send to server
                    string data = JsonConvert.SerializeObject(
                                    requestData,
                                    new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() });
    
                    client.Headers.Add("Content-Type", "application/json; charset=utf-8");
                    client.Encoding = System.Text.Encoding.UTF8;
                    string url = "http://yourdomain.com/api";
                    string response = client.UploadString(url, data);
    
                    //Deal with response as you need
                }
                catch (Exception ex)
                {
                    Console.Error.Write(ex.Message);
                }
            }
    

    On the server GZIP uploads are not normally supported, so I needed to add support for this.

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.IO.Compression;
    using System.Linq;
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    
    namespace WebServer
    {
        public class CompressedRequestHandler : DelegatingHandler
        {
            protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
            {
                if (IsRequetCompressed(request))
                {
                    request.Content = DecompressRequestContent(request);
                }
    
                return base.SendAsync(request, cancellationToken);
            }
    
            private bool IsRequetCompressed(HttpRequestMessage request)
            {
                if (request.Content.Headers.ContentEncoding != null &&
                    request.Content.Headers.ContentEncoding.Contains("gzip"))
                {
                    return true;
                }
    
                return false;
            }
    
            private HttpContent DecompressRequestContent(HttpRequestMessage request)
            {
                // Read in the input stream, then decompress in to the outputstream.
                // Doing this asynronously, but not really required at this point
                // since we end up waiting on it right after this.
                MemoryStream outputStream = new MemoryStream();
    
                Task task = request.Content.ReadAsStreamAsync().ContinueWith(t =>
                    {
                        Stream inputStream = t.Result;
    
                        using (GZipStream gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
                        {
                            gzipStream.CopyTo(outputStream);
                        }
    
                        // Setting output streem position to begin is ESSENTIAL!
                        outputStream.Seek(0, SeekOrigin.Begin);
                    });
    
                // Wait for inputstream and decompression to complete. Would be nice
                // to not block here and work async when ready instead, but I couldn't 
                // figure out how to do it in context of a DelegatingHandler.
                task.Wait();
    
                // Save the original content
                HttpContent origContent = request.Content;
    
                // Replace request content with the newly decompressed stream
                HttpContent newContent = new StreamContent(outputStream);
    
                // Copy all headers from original content in to new one
                // Change content-encoding and content-length
                foreach (var header in origContent.Headers)
                {
                    // Change content-encoding header to default value
                    if (header.Key.ToLowerInvariant() == "content-encoding")
                    {
                        newContent.Headers.Add(header.Key, "identity");
                        continue;
                    }
    
                    // Change content-length header value to decompressed length
                    if (header.Key.ToLowerInvariant() == "content-length")
                    {
                        newContent.Headers.Add(header.Key, outputStream.Length.ToString());
                        continue;
                    }
    
                    // Copy other headers
                    newContent.Headers.Add(header.Key, header.Value);
                }
    
                ////For testing purpose only!
                //Task task2 = newContent.ReadAsStringAsync().ContinueWith(x =>
                //{
                //    string strConent = x.Result;
                //});
    
                //task2.Wait();
    
    
                return newContent;
            }
        }
    }
    

    This handler must be registered in GlobalConfig file (under App_Start):

    using System.Web.Http;
    using Newtonsoft.Json.Serialization;
    using System.Web.Http.Filters;
    
    namespace WebServer
    {
        public static class GlobalConfig
        {
            public static void CustomizeConfig(HttpConfiguration config)
            {
                //REGISTER CompressedRequestHandler
                config.MessageHandlers.Add(new CompressedRequestHandler());
    
                // Remove Xml formatters. This means when we visit an endpoint from a browser,
                // Instead of returning Xml, it will return Json.
                // More information from Dave Ward: http://jpapa.me/P4vdx6
                config.Formatters.Remove(config.Formatters.XmlFormatter);
    
                // Configure json camelCasing per the following post: http://jpapa.me/NqC2HH
                // Here we configure it to write JSON property names with camel casing
                // without changing our server-side data model:
                //var json = config.Formatters.JsonFormatter;
                //json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                //json.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    
                //remove standard JSON formatter
                config.Formatters.Remove(config.Formatters.JsonFormatter);
    
                //add JSONP formatter to support both JSON and JSONP
                var jsonp = new JsonpMediaTypeFormatter();
                jsonp.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                jsonp.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
                config.Formatters.Add(jsonp);
    
                // Add model validation, globally
                config.Filters.Add(new ValidationActionFilter());
    
                //            config.Filters.Add(new AuthorizeAttribute());
    
                //
                //config.Filters.Add(new CustomSecurityAttribute());
                //config.Filters.Add(new XchangeSecurityAttribute());
                config.Filters.Add(new ExceptionHandlingAttribute());
            }
        }
    }