Master C# Logo banner
Welcome to MasterCSharp.com - Master C#, the easy way... - by Saurabh Nandu

 


Creating Custom SOAP Extensions - Compression Extension

Add Comment
 

 
Download SDK
compressionextensionproject.zip (65KB) v1.0.3705

Introduction
Web Services have already been standardized and its usage if definitely on the rise. Developers are quickly finding new and better ways to apply Web Services in their existing or new application designs. Reviewing many of the Web Services designs I have noticed that developers are increasingly performing heavy weight tasks (passing large SOAP messages) using Web Services. They are passing thousands of records through the Web Methods, there is nothing wrong with this approach, but the only problem (or nightmare) that comes forward is the size of data being passed around. Web Services use XML formatted SOAP messages to communicate but XML is a very verbose language and it can certainly add-up a lot to the size of data being transferred. Global bandwidth conditions do not currently warrant such massive data transfer easily.
One solution to this would be to compress (zip) the SOAP Message before sending it across the wire and then deflate (unzip) the data back on the client. This will help to reduce the amount of data transferred over the wire drastically! Luckily, the .NET Framework provides a very extensible Web Services API which makes it very easy to implement this functionality.

SOAP Extensions
The .NET Web Service API uses SOAP Extensions to provide hooks for developers into the messaging Web Service architecture, both on the client and server side. SOAP Extensions are used to inspect ( and modify ) the SOAP Message at each stage in the Web Services communication cycle.
To visualize the life cycle of a SOAP Request and Response, consider this scenario the client (of the Web Service) prepares the parameters (if any) and then calls the web method through the proxy class. Here the proxy class, inspects the method parameters, marshals them into XML format (serialization) and creates a SOAP Message. This SOAP message is then sent to the server  (Web Service). The Web Service receives the SOAP message it converts (deserializes) the XML formatted parameters into the language data-types and then invokes the relevant method. Next, the method on the server-side executes, does the necessary processing and it may return a value. The return value is again marshaled (serialized) into a SOAP message and sent back to the client. The client proxy receives the SOAP message converts (deserializes) the XML value and returns it back to the calling class. In short both client and server carry the task of serialization and deserialization.
SOAP Extensions in ASP.NET allow you to inspect and alter the SOAP Message at all the stages of the XML Web Service Lifetime both at client and server side. This provides you with a very powerful way to extend the existing Web Service framework to suit your requirements.
You can learn more about the Anatomy of an XML Web Services Lifetime in the MSDN documentation.

SOAP Message Compression
Currently, there are no standards involving compression of SOAP Messages, hence we are on our own implement a suitable extension. I have chosen to use zip algorithm, you can feel free to choose your own algorithm, but since I am using industry standard algorithms I can be sure they will be supported across platforms and programming languages. I am using the #ziplip library from ICSharpCode which is a GPL based implementation for the standard ZIP, GZIP, TAR and BZip2 algorithms in C# (with source code) by Mike Krueger. A special thanks to Mike Krueger for developing such a wonderful library in C#!

Another important question that comes into mind is Should we compress the whole SOAP Message before we send it across the wire or parts of it? The up-coming new standards rely a lot on the SOAP Headers, hence we chosen not to compress the SOAP Headers since they might be needed in plain XML format for your Web Service to function correctly. So we just compress the contents of the SOAP Body and then send the message across the wire. Also remember we are implementing this extension on both the client and server side, so all communication between the client and server will be compressed.

Please note that you will have to download and install the #ziplib library on both the client and server for this extension to work correctly. Instructions for installation of the library are clearly outlined in the support documentation.

Let's Dive into some code.
In order to implement a SOAP Extension we need to define 2 classes, the first class extends the SoapExtention class and it contains all the processing logic for the extension. The class extends the SoapExtensionAttribute class which is the attribute used to apply on the Web Methods that need to support the SOAP Extension.

CompressionExtension
In the CompressionExtension class I check for the two stages, AfterSerialize and BeforeDeserialize. In the AfterSerialize stage, I inspect the contents of the SOAP Body and zip them, then the zipped stream is saved back into the SOAP Body and the SOAP Message is passed over the wire. On the other hand in BeforeDeserialize stage, I inspect the SOAP Body and retrieve the zipped contents, unzip them, and restore the original content. Later, the SOAP Message is sent to the Web Method for processing.
Also remember while zipping the XML nodes we get a binary array, but binary arrays are not correctly represented in XML Web Services, hence we have to use Base64 encoding to convert the binary array into appropriate format.
 
Update: As per Tobias Gansen's suggestion, the source code has been updated. Now I use InnerXml instead of InnetText to retrieve the contents of the SOAP Body, so complex obhjects like DataSet's are correctly handled.
25 October 2002 - As per Don Bunt's suggestion the code has been updated. Now all the WebMethod parameters get compressed correctly.
 

using System;
using System.IO;
using System.Text ;
using System.Web.Services;
using System.Web.Services.Protocols ;
using ICSharpCode.SharpZipLib.Checksums;
using ICSharpCode.SharpZipLib.Zip;
using ICSharpCode.SharpZipLib.GZip;
using System.Xml ;

namespace MasterCSharp.WebServices
{
  /// <summary>
  /// Summary description for ConpressionExtension.
  /// </summary>
  public class CompressionExtension : System.Web.Services.Protocols.SoapExtension
  {
    Stream oldStream;
    Stream newStream;


    public override object GetInitializer(LogicalMethodInfo methodInfo,
				SoapExtensionAttribute attribute)
    {
      return attribute;
    }

    // Get the Type
    public override object GetInitializer(Type t)
    {
      return typeof(CompressionExtension);
    }

    // Get the CompressionExtensionAttribute
    public override void Initialize(object initializer)
    {
      CompressionExtensionAttribute attribute =
		(CompressionExtensionAttribute) initializer;

      return;
    }

    // Process the SOAP Message
    public override void ProcessMessage(SoapMessage message)
    {
      // Check for the various SOAP Message Stages 
      switch (message.Stage)
      {

        case SoapMessageStage.BeforeSerialize:
          break;

        case SoapMessageStage.AfterSerialize:
          // ZIP the contents of the SOAP Body after it has
          // been serialized
          Zip();
          break;

        case SoapMessageStage.BeforeDeserialize:
          // Unzip the contents of the SOAP Body before it is
          // deserialized
          Unzip();
          break;

        case SoapMessageStage.AfterDeserialize:
          break;

        default:
          throw new Exception("invalid stage");
      }
    }

    // Gives us the ability to get hold of the RAW SOAP message
    public override Stream ChainStream( Stream stream )
    {
      oldStream = stream;
      newStream = new MemoryStream();
      return newStream;
    }

    // Utility method to copy streams
    void Copy(Stream from, Stream to)
    {
      TextReader reader = new StreamReader(from);
      TextWriter writer = new StreamWriter(to);
      writer.WriteLine(reader.ReadToEnd());
      writer.Flush();
    }


    // Zip the SOAP Body
    private void Zip()
    {
      newStream.Position = 0;
      // Zip the SOAP Body
      newStream = ZipSoap(newStream);
      // Copy the streams
      Copy(newStream, oldStream);
    }

    // The actual ZIP method
    private byte[] Zip(string stringToZip)
    {
      byte[] inputByteArray = Encoding.UTF8.GetBytes(stringToZip);
      MemoryStream ms = new MemoryStream();

      // Check the #ziplib docs for more information
      ZipOutputStream zipOut = new ZipOutputStream( ms ) ;
      ZipEntry ZipEntry = new ZipEntry("ZippedFile");
      zipOut.PutNextEntry(ZipEntry);
      zipOut.SetLevel(9);
      zipOut.Write(inputByteArray, 0 , inputByteArray.Length ) ;
      zipOut.Finish();
      zipOut.Close();

      // Return the zipped contents
      return ms.ToArray();
    }

    // Select and Zip the appropriate parts of the SOAP message
    public MemoryStream ZipSoap(Stream streamToZip)
    {
      streamToZip.Position = 0;
      // Load a XML Reader
      XmlTextReader reader = new XmlTextReader(streamToZip);
      XmlDocument dom = new XmlDocument();
      dom.Load(reader);
      // Load a NamespaceManager to enable XPath selection
      XmlNamespaceManager nsmgr = new XmlNamespaceManager(dom.NameTable);
      nsmgr.AddNamespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");
      XmlNode node = dom.SelectSingleNode("//soap:Body", nsmgr);
      // Select the contents within the method defined in the SOAP body
      node = node.FirstChild.FirstChild;
      // Check if there are any nodes selected
      while( node != null )
      {
        if( node.InnerXml.Length > 0 )
        {
          // Zip the data
          byte[] outData = Zip(node.InnerXml);
          // Convert it to Base64 for transfer over the internet
          node.InnerXml = Convert.ToBase64String(outData) ;
        }
        // Move to the next parameter
        node = node.NextSibling ; 
      }
      MemoryStream ms = new MemoryStream();
      // Save the updated data
      dom.Save(ms);
      ms.Position = 0;

      return ms;
    }

    // Unzip the SOAP Body
    private void Unzip()
    {
      MemoryStream unzipedStream = new MemoryStream();

      TextReader reader = new StreamReader(oldStream);
      TextWriter writer = new StreamWriter(unzipedStream);
      writer.WriteLine(reader.ReadToEnd());
      writer.Flush();
      // Unzip the SOAP Body
      unzipedStream = UnzipSoap(unzipedStream);
      // Copy the streams
      Copy(unzipedStream, newStream);

      newStream.Position = 0;
    }

    // Actual Unzip logic
    private byte[] Unzip(string stringToUnzip)
    {
      // Decode the Base64 encoding
      byte[] inputByteArray = Convert.FromBase64String( stringToUnzip ) ;
      MemoryStream ms = new MemoryStream(inputByteArray) ;
      MemoryStream ret = new MemoryStream();

      // Refer to #ziplib documentation for more info on this
      ZipInputStream zipIn = new ZipInputStream(ms);
      ZipEntry theEntry = zipIn.GetNextEntry();
      Byte[] buffer = new Byte[2048] ;
      int size = 2048;
      while (true)
      {
        size = zipIn.Read(buffer, 0, buffer.Length);
        if (size > 0)
        {
          ret.Write(buffer, 0, size);
        }
        else
        {
          break;
        }
      }
      return ret.ToArray();
    }

    // Unzip the SOAP Body
    public MemoryStream UnzipSoap(Stream streamToUnzip)
    {
      streamToUnzip.Position = 0;
      // Load a XmlReader
      XmlTextReader reader = new XmlTextReader(streamToUnzip);
      XmlDocument dom = new XmlDocument();
      dom.Load(reader);

      XmlNamespaceManager nsmgr = new XmlNamespaceManager(dom.NameTable);
      nsmgr.AddNamespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");
      // Select the SOAP Body node 
      XmlNode node = dom.SelectSingleNode("//soap:Body", nsmgr);
      node = node.FirstChild.FirstChild;

      // Check if node exists
      while( node != null )
      {
        if( node.InnerXml.Length > 0 )
        {
          // Send the node's contents to be unziped
          byte[] outData = Unzip(node.InnerXml);
          string sTmp = Encoding.UTF8.GetString(outData);
          node.InnerXml = sTmp;
        }
        // Move to the next parameter
        node = node.NextSibling ; 
      }

      MemoryStream ms = new MemoryStream();

      dom.Save(ms);
      ms.Position = 0;

      return ms;
    }

  }
}

CompressionExtensionAttribute
This class defines an Attribute which will be used to mark-up Web Methods that need to be compressed. This class inherits the SoapExtensionAttribute class and overrides its ExtensionType and Priority properties.

using System;
using System.Web.Services;
using System.Web.Services.Protocols;

namespace MasterCSharp.WebServices
{

  /// <summary>
  /// Summary description for CompressionExtensionAttribute.
  /// </summary>
  // Make the Attribute only Applicable to Methods
  [AttributeUsage(AttributeTargets.Method)]
  public class CompressionExtensionAttribute : SoapExtensionAttribute
  {

    private int priority;

    // Override the base class properties
    public override Type ExtensionType
    {
      get { return typeof(CompressionExtension); }
    }

    public override int Priority
    {
      get
      {
        return priority;
      }
      set
      {
        priority = value;
      }
    }

  }
}

Code Compilation
Once you have the CompressionExtension.cs and CompressionExtensionAttribute.cs files ready, its time to compile them and convert them into a library. If you use VS.NET then you need to Add Reference to the System.Web.Services, System.Xml and SharpZipLib assemblies and compile the project. If you are manually compiling the files, then following compilation string:
csc /t:library /out:CompressionExtension.dll /r:SharpZipLib.dll CompressionExtension.cs CompressionExtensionAttribute.cs

This will produce the CompressionExtension.dll library. Now you can use this library in your clients and Web Services to enable compression extension. Note: Remember the clients and servers will also need to install the SharpZibLib.dll library also for this extension to work!

Sample Web Service
Our Compression Extension is ready for usage. I will create a simple Web Service which will demonstrate the usage of the Compression Extension, note the goal here is show the usage of the Compression Extension and not to teach how to create and deploy Web Services. If you want to learn more about Web Service basics please read this article.

I am using the Pubs sample database which installs with MSDE 2000 (SQL Server), just because I want to return a lot of data for testing purpose. But the Compression Extension is in no way tied to the database, it can be applied on any Web Method. As a matter of fact the Compression Extension does not even dictate there method parameter and  return parameter data type. Hence you can apply this extension on any Web Method no matter what parameters / return types it uses.
In the listing below, I have defined a basic Web Service that returns a string containing the XML representation of the Authors, Titles and Publishers tables from the Pubs database.
There are only two changes; Firstly, I have referenced the MasterCSharp.WebServices namespace that contains the Compression Extension. Secondly, I have applied the Compression Extension on the GetDetails Web Method. That's all you  need to do!!! It just can't get any simpler than this :).
While deploying the Web Service, you should ensure the SharpZipLib assembly has been registered in the GAC, alternatively you can also copy the library into the BIN directory of the Virtual Directory hosting this Web Service. Also don't forget to copy the CompressionExtension.dll library into BIN directory of the Virtual Root hosting this Web Service.

One important point to be noted is that we have applied a SOAP Extension, and this SOAP Extension is called only when a SOAP call is made to a Web Method. Hence if we are making HTTP GET / POST calls the Compression Extension is NOT in effect. Hence when we check the Web Service in Internet Explorer using the ASP.NET rendered test interface, you cannot see any compressed output.

<%@ WebService Language="C#" Class="PubsService" %>

using System.Web.Services;
using MasterCSharp.WebServices ;
using System.Data ;
using System.Data.SqlClient ;

public class PubsService {

  [WebMethod]
  [CompressionExtension]
  public string GetDetails() {

    // Replace with the connection string to connect to your database
    SqlConnection myCon = new SqlConnection("server=(local)\\NetSDK;database=pubs;" ;
				+Trusted_Connection=yes");
    SqlDataAdapter myCommand1 = new SqlDataAdapter("SELECT * FROM Authors", myCon);
    SqlDataAdapter myCommand2 = new SqlDataAdapter("SELECT * FROM Titles", myCon);
    SqlDataAdapter myCommand3 = new SqlDataAdapter("SELECT * FROM Publishers", myCon);

    DataSet ds = new DataSet();
    myCommand1.Fill(ds, "Authors");
    myCommand2.Fill(ds, "Titles");
    myCommand3.Fill(ds, "Publishers");

    return ds.GetXml() ;
  }

}

Test Web Service Client
In order to consume our PubsService I create a console based test client. In VS.NET you can create a new Console Project and then Add Web Reference to the PubsService and Add Reference to the CompressionExtension.dll library. Remember even method parameters sent from the client are compressed, hence you need to reference the Compression Extension library even at the client side.
In case of manual class creation, use the WSDL tool to generate a Proxy Class for the PubsService (replace the URL to the Web Service as per your setup):
wsdl http://localhost/PubsService/PubsService.asmx?WSDL

This creates a PubsService.cs Proxy Class file. Now let's create the Pubs Test Application using this proxy class. The listing below shows the code for the PubsTestApp. In this test application, I just make an instance of the Proxy class and call a Web Method. The value returned by the Web Service is saved to a file (so I can measure the bytes returned) and also printed on the console screen. Compile this application using the following compilation string:
csc /out:PubsTestApp.exe PubsService.cs PubsTestApp.cs

using System;
using System.IO ;

public class PubsTestApp
{
  public static void Main()
  {
    // Create an instance of the Proxy Class
    PubsService pws = new PubsService();
    // Open a StreamReader to a local file
    // This file will be used to log the return value
    // of the SOAP call
    StreamWriter sw = new StreamWriter( "log.txt" );
    // Call the Web Method and write the return value to file
    sw.Write( pws.GetDetails() ) ;
    sw.Close();
    // Call the Web Method again and write value to Console
    Console.WriteLine( pws.GetDetails() ) ;

    Console.WriteLine();
    Console.WriteLine("Hit Enter to Close Window!");
    Console.ReadLine();
  }
}

Once the application is compiled we call the PubsTestApp, it runs and shows some garbage characters, this output is even saved to the log.txt file. In reality what's shown is not garbage, but its the Base 64 encoding of the zipped data. Open Windows Explorer and observe the file size of the file log.txt, its approximately 4,864 bytes on my computer. This is the compressed data that's been sent to the client.

Next, you need to apply the the Compression Extension to the Proxy Class, so that you can receive normal output (unzipped data) from the Web Service. This is the step you would normally be taking right after generating the Proxy Class. Open the PubsService.cs proxy class file and add the [CompressionExtension] attribute above the public string GetDetails() method definition. Also reference the MasterCSharp.WebServices namespace which contains the Compression Extension. Once you apply the attribute, re-compile the PubsTestApp this time using the following compilation string: 
csc /out:PubsTestApp.exe /r:CompressionExtension.dll PubsService.cs PubsTestApp.cs

Note: The CompressionExtension.dll library should be in the same directory we are compiling the application and the SharpZipLib.dll library should also be appropriately installed on the client.

Now run the PubsTestApp again, this time correct XML output is shown on the console screen, indicating the extension works on both ends seamlessly. Observe the size of the log.txt file again and this time its around 15,908 bytes, this is the amount of data that would have been passed if we did not use compression. Hence the Compression Extension has achieved almost 30% size reduction in the SOAP Messages used to communicate between the client and the server. Although, this compression ratio will vary on compression algorithm and the type of data you are compressing.

Conclusion
This article provides a practical SOAP Extension that can be used in any kind of Web Services, to utilize network resources better. In this article I not only highlighted the creation and deployment of a SOAP Extension in .NET, but I also silently displayed how to pass binary data using Web Service i.e. using Base 64 Encoding. If you are wondering if this method would still let your Web Service interop with other platforms and programming languages, well I haven't worked with other toolkits, but I am sure, they would definitely allow you to manipulate the SOAP Message. And since we are using standard ZIP algorithms, I don't see a huge barrier to the usage of this Web Service across different platforms and programming languages. If some of you get a chance to test this, please do let me know!!

Comments

Add Comment