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

 


Creating Custom SOAP Extensions - Compression Extension

Add Comment
 

 
<div align="center"> <table cellpadding="1" cellspacing="2" width="75%"class="outline"> <tr> <td width="70%" class="outline"><b>Download</b></td> <td width="30%" class="outline"><b>SDK</b></td> </tr> <tr> <td width="70%" class="outline"> <a class="wbox" href="../../file/compressionextensionproject.zip"> compressionextensionproject.zip</a> (65KB)</td> <td width="30%" class="outline">v1.0.3705</td> </tr> </table> </div> <p> <span class=wboxheado>Introduction</span><br> 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.<br> 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. </p> <p><span class=wboxheado>SOAP Extensions</span><br> The .NET Web Service API uses <i>SOAP Extensions</i> 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.<br> 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&nbsp; (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.<br> 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.<br> You can learn more about the <a class="wbox" target="_blank" href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconanatomyofsoapwebservicelifetime.asp"> Anatomy of an XML Web Services Lifetime</a> in the MSDN documentation.<p> <span class=wboxheado>SOAP Message Compression</span><br> 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 <b>#ziplip</b> library from <a class="wbox" target="_blank" href="http://icsharpcode.net/OpenSource/SharpZipLib/default.asp"> ICSharpCode</a> which is a <a class="wbox" target="_blank" href="http://www.gnu.org">GPL</a> based implementation for the standard ZIP, GZIP, TAR and BZip2 algorithms in C# (with source code) by <a class="wbox" href="mailto:mike@icsharpcode.net">Mike Krueger</a>. A special thanks to Mike Krueger for developing such a wonderful library in C#!<p>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 <i>SOAP Headers</i>, 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 <i>contents</i> 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.<br> <br> Please note that you will have to download and install the <i>#ziplib</i> 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.<p><span class=wboxheado>Let's Dive into some code.</span><br> In order to implement a SOAP Extension we need to define 2 classes, the first class extends the <i>SoapExtention</i> class and it contains all the processing logic for the extension. The class extends the <i>SoapExtensionAttribute</i> class which is the attribute used to apply on the Web Methods that need to support the SOAP Extension.<br> <br> <span class=wboxhead>CompressionExtension</span><br> In the <i>CompressionExtension</i> class I check for the two stages, <i>AfterSerialize</i> and <i>BeforeDeserialize</i>. In the <i>AfterSerialize</i> 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 <i>BeforeDeserialize</i> 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.<br> 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.<br> &nbsp;<br> <b>Update:</b> As per <b>Tobias Gansen</b>'s suggestion, the source code has been updated. Now I use <i>InnerXml</i> instead of <i>InnetText</i> to retrieve the contents of the SOAP Body, so complex obhjects like DataSet's are correctly handled.<br> 25 October 2002 - As per <b>Don Bunt</b>'s suggestion the code has been updated. Now all the WebMethod parameters get compressed correctly.<br> &nbsp;<table cellpadding="1" cellspacing="2" width="100%" class="code"> <tr> <td width="100%"> <pre>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 { <span class=cmt>/// &lt;summary&gt; /// Summary description for ConpressionExtension. /// &lt;/summary&gt;</span> public class CompressionExtension : System.Web.Services.Protocols.SoapExtension { Stream oldStream; Stream newStream; public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute) { return attribute; } <span class=cmt>// Get the Type</span> public override object GetInitializer(Type t) { return typeof(CompressionExtension); } <span class=cmt>// Get the CompressionExtensionAttribute</span> public override void Initialize(object initializer) { CompressionExtensionAttribute attribute = (CompressionExtensionAttribute) initializer; return; } <span class=cmt>// Process the SOAP Message</span> public override void ProcessMessage(SoapMessage message) { <span class=cmt>// Check for the various SOAP Message Stages </span> switch (message.Stage) { case SoapMessageStage.BeforeSerialize: break; case SoapMessageStage.AfterSerialize: <span class=cmt>// ZIP the contents of the SOAP Body after it has // been serialized</span> Zip(); break; case SoapMessageStage.BeforeDeserialize: <span class=cmt>// Unzip the contents of the SOAP Body before it is // deserialized</span> Unzip(); break; case SoapMessageStage.AfterDeserialize: break; default: throw new Exception(&quot;invalid stage&quot;); } } <span class=cmt>// Gives us the ability to get hold of the RAW SOAP message</span> public override Stream ChainStream( Stream stream ) { oldStream = stream; newStream = new MemoryStream(); return newStream; } <span class=cmt>// Utility method to copy streams</span> void Copy(Stream from, Stream to) { TextReader reader = new StreamReader(from); TextWriter writer = new StreamWriter(to); writer.WriteLine(reader.ReadToEnd()); writer.Flush(); } <span class=cmt>// Zip the SOAP Body</span> private void Zip() { newStream.Position = 0; <span class=cmt>// Zip the SOAP Body</span> newStream = ZipSoap(newStream); <span class=cmt>// Copy the streams</span> Copy(newStream, oldStream); } <span class=cmt>// The actual ZIP method</span> private byte[] Zip(string stringToZip) { byte[] inputByteArray = Encoding.UTF8.GetBytes(stringToZip); MemoryStream ms = new MemoryStream(); <span class=cmt>// Check the #ziplib docs for more information</span> ZipOutputStream zipOut = new ZipOutputStream( ms ) ; ZipEntry ZipEntry = new ZipEntry(&quot;ZippedFile&quot;); zipOut.PutNextEntry(ZipEntry); zipOut.SetLevel(9); zipOut.Write(inputByteArray, 0 , inputByteArray.Length ) ; zipOut.Finish(); zipOut.Close(); <span class=cmt>// Return the zipped contents</span> return ms.ToArray(); } // Select and Zip the appropriate parts of the SOAP message public MemoryStream ZipSoap(Stream streamToZip) { streamToZip.Position = 0; <span class=cmt>// Load a XML Reader</span> XmlTextReader reader = new XmlTextReader(streamToZip); XmlDocument dom = new XmlDocument(); dom.Load(reader); <span class=cmt>// Load a NamespaceManager to enable XPath selection</span> XmlNamespaceManager nsmgr = new XmlNamespaceManager(dom.NameTable); nsmgr.AddNamespace(&quot;soap&quot;, &quot;http://schemas.xmlsoap.org/soap/envelope/&quot;); XmlNode node = dom.SelectSingleNode(&quot;//soap:Body&quot;, nsmgr); <span class=cmt>// Select the contents within the method defined in the SOAP body</span> node = node.FirstChild.FirstChild; <span class=cmt>// Check if there are any nodes selected</span> while( node != null ) { if( node.InnerXml.Length &gt; 0 ) { <span class=cmt> // Zip the data</span> byte[] outData = Zip(node.InnerXml); <span class=cmt>// Convert it to Base64 for transfer over the internet</span> node.InnerXml = Convert.ToBase64String(outData) ; } <span class=cmt>// Move to the next parameter</span> node = node.NextSibling ; } MemoryStream ms = new MemoryStream(); <span class=cmt>// Save the updated data</span> dom.Save(ms); ms.Position = 0; return ms; } <span class=cmt>// Unzip the SOAP Body</span> private void Unzip() { MemoryStream unzipedStream = new MemoryStream(); TextReader reader = new StreamReader(oldStream); TextWriter writer = new StreamWriter(unzipedStream); writer.WriteLine(reader.ReadToEnd()); writer.Flush(); <span class=cmt>// Unzip the SOAP Body</span> unzipedStream = UnzipSoap(unzipedStream); <span class=cmt>// Copy the streams</span> Copy(unzipedStream, newStream); newStream.Position = 0; } <span class=cmt>// Actual Unzip logic</span> private byte[] Unzip(string stringToUnzip) { <span class=cmt>// Decode the Base64 encoding</span> byte[] inputByteArray = Convert.FromBase64String( stringToUnzip ) ; MemoryStream ms = new MemoryStream(inputByteArray) ; MemoryStream ret = new MemoryStream(); <span class=cmt>// Refer to #ziplib documentation for more info on this</span> 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 &gt; 0) { ret.Write(buffer, 0, size); } else { break; } } return ret.ToArray(); } <span class=cmt>// Unzip the SOAP Body</span> public MemoryStream UnzipSoap(Stream streamToUnzip) { streamToUnzip.Position = 0; <span class=cmt>// Load a XmlReader</span> XmlTextReader reader = new XmlTextReader(streamToUnzip); XmlDocument dom = new XmlDocument(); dom.Load(reader); XmlNamespaceManager nsmgr = new XmlNamespaceManager(dom.NameTable); nsmgr.AddNamespace(&quot;soap&quot;, &quot;http://schemas.xmlsoap.org/soap/envelope/&quot;); <span class=cmt>// Select the SOAP Body node </span> XmlNode node = dom.SelectSingleNode(&quot;//soap:Body&quot;, nsmgr); node = node.FirstChild.FirstChild; <span class=cmt> // Check if node exists</span> while( node != null ) { if( node.InnerXml.Length &gt; 0 ) { <span class=cmt> // Send the node's contents to be unziped</span> byte[] outData = Unzip(node.InnerXml); string sTmp = Encoding.UTF8.GetString(outData); node.InnerXml = sTmp; } <span class=cmt>// Move to the next parameter</span> node = node.NextSibling ; } MemoryStream ms = new MemoryStream(); dom.Save(ms); ms.Position = 0; return ms; } } }</pre> </td> </tr> </table> <p><span class=wboxhead>CompressionExtensionAttribute</span><br> This class defines an Attribute which will be used to mark-up Web Methods that need to be compressed. This class inherits the <i>SoapExtensionAttribute</i> class and overrides its <i>ExtensionType</i> and <i>Priority</i> properties.</p> <table cellpadding="1" cellspacing="2" width="100%" class="Code"> <tr> <td width="100%"> <pre>using System; using System.Web.Services; using System.Web.Services.Protocols; namespace MasterCSharp.WebServices { <span class=cmt>/// &lt;summary&gt; /// Summary description for CompressionExtensionAttribute. /// &lt;/summary&gt;</span></pre> <pre><span class=cmt> // Make the Attribute only Applicable to Methods</span> [AttributeUsage(AttributeTargets.Method)] public class CompressionExtensionAttribute : SoapExtensionAttribute { private int priority; <span class=cmt>// Override the base class properties</span> public override Type ExtensionType { get { return typeof(CompressionExtension); } } public override int Priority { get { return priority; } set { priority = value; } } } }</pre> </td> </tr> </table> <p><span class=wboxheado>Code Compilation</span><br> Once you have the <b>CompressionExtension.cs</b> and <b>CompressionExtensionAttribute.cs</b> files ready, its time to compile them and convert them into a library. If you use VS.NET then you need to <i>Add Reference</i> to the <i>System.Web.Services</i>, <i>System.Xml</i> and <i>SharpZipLib</i> assemblies and compile the project. If you are manually compiling the files, then following compilation string:<br> <b>csc /t:library /out:CompressionExtension.dll /r:SharpZipLib.dll CompressionExtension.cs CompressionExtensionAttribute.cs</b></p> <p>This will produce the <b>CompressionExtension.dll</b> 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 <i>SharpZibLib.dll</i> library also for this extension to work!</p> <p><span class=wboxheado>Sample Web Service</span><br> 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 <a class="wbox" target="_blank" href="http://www.mastercsharp.com/article.aspx?ArticleID=52&&TopicID=7"> this article</a>.</p> <p>I am using the <b>Pubs</b> sample database which installs with <i>MSDE 2000 (SQL Server)</i>, 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&nbsp; return parameter data type. Hence you can apply this extension on any Web Method no matter what parameters / return types it uses. <br> In the listing below, I have defined a basic Web Service that returns a string containing the XML representation of the <b>Authors</b>, <b>Titles</b> and <b>Publishers</b> tables from the <b>Pubs</b> database. <br> There are only two changes; Firstly, I have referenced the <b>MasterCSharp.WebServices</b> namespace that contains the Compression Extension. Secondly, I have applied the Compression Extension on the <b>GetDetails</b> Web Method. That's all you&nbsp; need to do!!! It just can't get any simpler than this :). <br> While deploying the Web Service, you should ensure the <i>SharpZipLib</i> assembly has been registered in the GAC, alternatively you can also copy the library into the <b>BIN</b> directory of the <i>Virtual Directory</i> hosting this Web Service. Also don't forget to copy the <b>CompressionExtension.dll</b> library into <b>BIN</b> directory of the Virtual Root hosting this Web Service.</p> <p>One important point to be noted is that we have applied a SOAP Extension, and this SOAP Extension is called only when a <i>SOAP call</i> 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.</p> <table cellpadding="1" cellspacing="2" width="100%" class="code"> <tr> <td width="100%"> <pre>&lt;%@ WebService Language=&quot;C#&quot; Class=&quot;PubsService&quot; %&gt; using System.Web.Services; using MasterCSharp.WebServices ; using System.Data ; using System.Data.SqlClient ; public class PubsService { [WebMethod] [CompressionExtension] public string GetDetails() { <span class=cmt>// Replace with the connection string to connect to your database</span> SqlConnection myCon = new SqlConnection(&quot;server=(local)\\NetSDK;database=pubs;&quot; ; +Trusted_Connection=yes&quot;); SqlDataAdapter myCommand1 = new SqlDataAdapter(&quot;SELECT * FROM Authors&quot;, myCon); SqlDataAdapter myCommand2 = new SqlDataAdapter(&quot;SELECT * FROM Titles&quot;, myCon); SqlDataAdapter myCommand3 = new SqlDataAdapter(&quot;SELECT * FROM Publishers&quot;, myCon); DataSet ds = new DataSet(); myCommand1.Fill(ds, &quot;Authors&quot;); myCommand2.Fill(ds, &quot;Titles&quot;); myCommand3.Fill(ds, &quot;Publishers&quot;); return ds.GetXml() ; } }</pre> </td> </tr> </table> <p><span class=wboxheado>Test Web Service Client</span><br> In order to consume our <i>PubsService</i> I create a console based test client. In VS.NET you can create a new <i>Console Project</i> and then <i>Add Web Reference</i> to the <i>PubsService</i> and <i>Add Reference</i> to the <i>CompressionExtension.dll</i> 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.<br> In case of manual class creation, use the <b>WSDL</b> tool to generate a Proxy Class for the PubsService (replace the URL to the Web Service as per your setup): <br> <b>wsdl http://localhost/PubsService/PubsService.asmx?WSDL</b></p> <p>This creates a <i>PubsService.cs</i> Proxy Class file. Now let's create the Pubs Test Application using this proxy class. The listing below shows the code for the <i>PubsTestApp</i>. 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: <br> <b>csc /out:PubsTestApp.exe PubsService.cs PubsTestApp.cs</b></p> <table cellpadding="1" cellspacing="2" width="100%" class="code"> <tr> <td width="100%"> <pre>using System; using System.IO ; public class PubsTestApp { public static void Main() { <span class=cmt>// Create an instance of the Proxy Class</span> PubsService pws = new PubsService(); <span class=cmt>// Open a StreamReader to a local file // This file will be used to log the return value // of the SOAP call</span> StreamWriter sw = new StreamWriter( &quot;log.txt&quot; ); <span class=cmt>// Call the Web Method and write the return value to file</span> sw.Write( pws.GetDetails() ) ; sw.Close(); <span class=cmt>// Call the Web Method again and write value to Console</span> Console.WriteLine( pws.GetDetails() ) ; Console.WriteLine(); Console.WriteLine(&quot;Hit Enter to Close Window!&quot;); Console.ReadLine(); } }</pre> </td> </tr> </table> <p>Once the application is compiled we call the<i> PubsTestApp</i>, it runs and shows some garbage characters, this output is even saved to the <b>log.txt</b> file. In reality what's shown is not garbage, but its the Base 64 encoding of the zipped data. Open <i>Windows Explorer</i> 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.</p> <p>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 <i>PubsService.cs</i> proxy class file and add the <b>[CompressionExtension]</b> attribute above the <b>public string GetDetails()</b> method definition. Also reference the <b>MasterCSharp.WebServices</b> namespace which contains the Compression Extension. Once you apply the attribute, re-compile the <i>PubsTestApp</i> this time using the following compilation string:&nbsp; <br> <b>csc /out:PubsTestApp.exe /r:CompressionExtension.dll PubsService.cs PubsTestApp.cs</b></p> <p>Note: The <i>CompressionExtension.dll</i> library should be in the same directory we are compiling the application and the <i>SharpZipLib.dll</i> library should also be appropriately installed on the client.</p> <p>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 <i>log.txt</i> 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 <i>30%</i> 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.</p> <p><span class=wboxheado>Conclusion</span><br> 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!!</p>

Comments

Add Comment