CONTENT
 PRODUCTS
 ARTICLES
 DOWNLOAD
 SOURCE CODE
 BOOK
 ABOUT
 HOME

MVP

ADS




Two-Way .NET CF Web Service Compression
We'll look at how SOAP Extensions can be used with compressed XML Web Service calls to make Smart Device (Pocket PC and Smartphone) applications use less bandwidth, and thereby save communication costs. This is interesting as mobile devices tend to use bandwidth that is more expensive (like GPRS).

As already covered in the previous article on this subject, Web Service Compression with .NET CF, there are many business benefits of compressing calls to XML Web Services. In that article I covered the use of standard (HTTP 1.1) compression provided by the web server, and that is a very useful solution in most scenarios where data is mainly transported from the server to the device. However, if data sent from the device to the server needs to be compressed, another solution must be used.

SOAP Extension Compression Sample
SOAP Extension Compression Sample
Such a solution is the focus of this article where the technology SOAP Extension is used to enhance both the XML Web Service implementation on the server as well as the Web Reference (SOAP proxy) on the client. Let's have a look at a sample.

Get the sample source code!

SOAP Extension Compression Sample
The sample show how to call XML Web Services that use SOAP Extensions to do compression in a Pocket PC application. This sample application is built for .NET CF with Visual Studio (the download include source for VS2003/CF1/WM2003 and VS2005/CF2/WM5) and it looks like this:

SOAP Extension Compression Sample
Figure 1. SOAP Extension Compression Sample

The sample allows a customer table (from the Northwind sample database) to be checked out (downloaded to the device) and later checked in (uploaded to the server) again. This is a common business scenario where a part of the data in a database is used exclusively by a client for a limited period of time until it is returned to the server again.

Code Walkthrough
To start on the server side, the following is the code for the ASP.NET Web Service method to check out the customers:
  [WebMethod]
  public DataSet CheckOutCustomers()
  {
    DataSet ds = new DataSet();
    using(SqlConnection cn = new SqlConnection(connectionString))
    {
      cn.Open();
      ds = SqlHelper.ExecuteDataset(cn, CommandType.Text,
        "SELECT * FROM Customers", "Customers");
    }
    return ds;
  }	
In a real-world application, this method would also mark the customers as checked out somehow. Note that the above code uses a slightly enhanced second version of the Data Access Application Block from Microsoft. The code on the client that calls the above method looks like this:
  WebServices.DataService ws = new WebServices.DataService();
  DataSet ds = ws.CheckOutCustomers();
  xmlTextBox.Text = ds.GetXml();
  MessageBox.Show("Checked out!", "Customers");
The customer check-in code looks like this:
  [WebMethod]
  public void CheckInCustomers(DataSet clientDataSet)
  {
    using(SqlConnection cn = new SqlConnection(connectionString))
    {
      cn.Open();
      DataSet serverDataSet = SqlHelper.ExecuteDataset(cn,
        CommandType.Text, "SELECT * FROM Customers", "Customers");
      foreach(DataRow dr in clientDataSet.Tables["Customers"].Rows)
      {
        DataRow[] drs = serverDataSet.Tables["Customers"].Select(
          "CustomerID='" + dr["CustomerID"].ToString() + "'");
        if(drs.Length > 0) // Update
          for(int i = 0; i < dr.ItemArray.Length; i++)
            drs[0][i] = dr[i];
        else // Insert
          serverDataSet.Tables["Customers"].Rows.Add(dr.ItemArray);
      } // Delete
      foreach(DataRow dr in serverDataSet.Tables["Customers"].Rows)
        if(clientDataSet.Tables["Customers"].Select("CustomerID='" +
           dr["CustomerID"].ToString() + "'").Length < 1)
          dr.Delete();
      SqlHelper.UpdateDataset(cn, serverDataSet, "Customers");
    }
  }
The server database is updated with the data passed in the DataSet from the client (clientDataSet). This code is called by the client like this:
  WebServices.DataService ws = new WebServices.DataService();
  ws.CheckInCustomers(ds);
  xmlTextBox.Text = string.Empty;
  MessageBox.Show("Checked in!", "Customers");
This is all well and works as a standard scenario. However, now we want to compress the data sent both to and from the server, and we do that using SOAP Extensions that allow the possibility to intercept the SOAP message in various points of the serialization process. That way the compression/decompression of the messages can be separated from the implementation of our application logic. For details on how this works, see the section Altering the SOAP Message Using SOAP Extensions in the .NET Framework Developer's Guide.

To be able to mark each of the methods in the Web reference, a class (CompressionSoapExtensionAttribute) that inherits from the class SoapExtensionAttribute (in the System.Web.Services.Protocols namespace) is created like this:
  [AttributeUsage(AttributeTargets.Method)]
  public class CompressionSoapExtensionAttribute : SoapExtensionAttribute
  {
    private int priority;

    public override Type ExtensionType 
    {
      get { return typeof(CompressionSoapExtension); }
    }

    public override int Priority 
    {
      get { return priority; }
      set { priority = value; }
    }
  }
The ExtensionType property returns the type that implements the logic of the extension (CompressionSoapExtension, see below for details). The .NET Compact Framework will retrieve this property to know what type to instantiate. The Priority property indicates the order of processing when there are several extensions simultaneously. The implementation of the actual extension logic is a class that inherits from the class SoapExtension (in the System.Web.Services.Protocols namespace) and looks like this:
  public class CompressionSoapExtension : SoapExtension
  {
    Stream oldStream;
    Stream newStream;
  
    public override Stream ChainStream( Stream stream ) 
    {
      oldStream = stream;
      newStream = new MemoryStream();
      return newStream;
    }
  
    public override object GetInitializer(LogicalMethodInfo methodInfo,
      SoapExtensionAttribute attribute) 
    {
      return attribute;
    }
  
    public override object GetInitializer(Type type) 
    {
      return typeof(CompressionSoapExtension);
    }
    
    public override void Initialize(object initializer) 
    {
      CompressionSoapExtensionAttribute attribute =
        (CompressionSoapExtensionAttribute)initializer;
    }
  
    public override void ProcessMessage(SoapMessage message) 
    {
      Byte[] buffer = new Byte[2048];
      int size;
  
      switch(message.Stage) 
      {
        case SoapMessageStage.AfterSerialize:
          newStream.Seek(0, SeekOrigin.Begin);
          GZipOutputStream zipOutputStream =
            new GZipOutputStream(oldStream);
          size = 2048;
          while(true) 
          {
            size = newStream.Read(buffer, 0, buffer.Length);
            if (size > 0) 
              zipOutputStream.Write(buffer, 0, size);
            else 
              break;
          }
          zipOutputStream.Flush();
          zipOutputStream.Close();		
          break;
  
        case SoapMessageStage.BeforeDeserialize:
          GZipInputStream zipInputStream =
            new GZipInputStream(oldStream);		
          size = 2048;
          while(true) 
          {
            size = zipInputStream.Read(buffer, 0, buffer.Length);
            if (size > 0) 
              newStream.Write(buffer, 0, size);
            else 
              break;
          }
          newStream.Flush();
          newStream.Seek(0, SeekOrigin.Begin);
          break;
      }
    }
  }
First, the ChainStream method is called with the stream that contains the data and allowing the opportunity to return a new stream for the data after the custom processing. The input stream is stored in memory and a new stream is returned that will be use to store the result of the compression and decompression. Then, the main method, ProcessMessage, is called on each stage of the processing of the SOAP message. In our case we are interested only in the AfterSerialize and BeforeDeserialize stages. The AfterSerialize stage indicates that a message has been serialized and is ready to be sent, and this is where the serialized data need to be compressed. The BeforeDeserialize stage indicates that a message has arrived and is about to be de-serialized, and this is where the yet not serialized data need to be uncompressed. The valuable SharpZipLib library is used to do the actual compression and decompression.

With the above two classes in place, each method in the XML Web Service implementation (on the server) can be marked with an attribute like this:
  [WebMethod]
  [CompressionSoapExtension]
  public DataSet CheckOutCustomers()
  { ... }

  [WebMethod]
  [CompressionSoapExtension]
  public void CheckInCustomers(DataSet clientDataSet)
  { ... }
Each method also needs to be marked in the same way in the Web reference (on the client) like this:
  [SoapDocumentMethodAttribute...]
  [CompressionSoapExtension]
  public DataSet CheckOutCustomers()
  { ... }

  [SoapDocumentMethodAttribute...]
  [CompressionSoapExtension]
  public void CheckInCustomers(DataSet clientDataSet)
  { ... }
An advantage of this approach is that you can select which methods you want to compress. If the method has a very small payload, the compression may only create overhead, and such methods can then be left uncompressed.

An interesting alternative to the above described solution is the Plumbwork Orange project that has a more extensive implementation of what could be very similar to a future WS-Compression specification.

For a more complete example, see the SOAP Extension Compression source code.

Conclusion
Using the standard extendibility of SOAP, a lot of bandwidth can be saved along with significant reduction of communication costs. As the impact on new or existing code is minimal, there are really no good reasons preventing you from compressing your XML Web Service calls where appropriate (on medium and large payloads).

Any comments?


©2001-2009 Christian Forsberg & Andreas Sjöström