Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Extending WCF - Part II

4.96/5 (39 votes)
21 Jan 2010CPOL4 min read 118.3K   2.5K  
An article on using compression/decompression of WCF data

Introduction

This article is a continuation of my first article Extending WCF Part I. In this article, I'm going to show you how to compress WCF data just before sending at service end and decompressing that data at client end just after receiving by extending WCF Framework. Compression/decompression is very much useful when bulk amount of data is returned from a service, also when client is under low band width internet connection. Under such circumstances, you may find out that your service operation is taking less time than the time taken to transport data. At the end service client needs to wait longer for the result. IIS provides the HTTP Compression technique to solve the issue. But there is a high chance that you face problems with other sites hosted in the IIS after turning on the HTTP compression. I found the solution first at MSDN. I used the codes here with few changes and fixes.

Background

Encoding is the process of transforming a message into a sequence of bytes. Decoding is the reverse process. Windows Communication Foundation (WCF) includes three types of encoding for SOAP messages: Text, Binary and Message Transmission Optimization Mechanism (MTOM). For more information, see at MSDN.

Figure 1

Image 1

Figure 1 shows the place where we're going to extend in WCF architecture.

A typical wsHttpBinding may look like the following:

XML
<bindings>
  <wsHttpBinding>
    <binding name="MyHttpBinding" openTimeout="00:05:00" sendTimeout="00:05:00"
          messageEncoding="Mtom">
    <readerQuotas maxDepth="999999" 
		maxArrayLength="999999" maxBytesPerRead="999999"
            maxNameTableCharCount="999999" />
    </binding>
  </wsHttpBinding>
</bindings>    

In the above binding sample, I've provided messageEncoding option to "Mtom". The default option is "Text".

Now let's have a look into the structure of binding elements in WCF Framework:

Figure 2

Image 2

The element below the red dot line is our extended classes. For WCF framework provided bindings, we can only use the framework provided 3 encoding options. To use custom encoding, we need to write custom binding using configuration. At ExtendedMessageEncodingBindingElement overridden method CreateMessageEncoderFactory(), we're returning the ExtendedMessageEncoderFactory class.

C#
//Main entry point into the encoder binding element.
//Called by WCF to get the factory that will create the
//message encoder
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
    return new ExtendedMessageEncoderFactory
            	(innerBindingElement.CreateMessageEncoderFactory(), _encoderType);
}    

At the constructor of the ExtendedMessageEncoderFactory, we're constructing the custom message encoder.

C#
//The GZip encoder wraps an inner encoder
//We require a factory to be passed in that will create this inner encoder
public ExtendedMessageEncoderFactory
           (MessageEncoderFactory messageEncoderFactory, string encoderType)
{
    if (messageEncoderFactory == null)
               throw new ArgumentNullException("messageEncoderFactory",
                   "A valid message encoder factory must be passed to the GZipEncoder");
    var messageEncoderType = Type.GetType(encoderType);
    encoder = (MessageEncoder)Activator.CreateInstance
               (messageEncoderType, messageEncoderFactory.Encoder);
}

WcfExtensions.MessageEncodingBindingElementExtension is our extended class which provides us the option to tie our custom message encoders into the system and configure it properly. The GZipMessageEncoder class uses System.IO.Compression.GZipStream has been used to compress and decompress data.

C#
//Helper method to compress an array of bytes
static ArraySegment<byte> CompressBuffer(ArraySegment<byte> buffer, 
BufferManager bufferManager, int messageOffset)
{
    //Display the Compressed and uncompressed sizes
    Console.WriteLine("Original message is {0} bytes", buffer.Count);
    
    var memoryStream = new MemoryStream();
    memoryStream.Write(buffer.Array, 0, messageOffset);
    
    using (var gzStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
    {
        gzStream.Write(buffer.Array, messageOffset, buffer.Count);
    }
    
    var compressedBytes = memoryStream.ToArray();
    var bufferedBytes = bufferManager.TakeBuffer(compressedBytes.Length);
    
    Array.Copy(compressedBytes, 0, bufferedBytes, 0, compressedBytes.Length);
    
    bufferManager.ReturnBuffer(buffer.Array);
    var byteArray = new ArraySegment<byte>
    (bufferedBytes, messageOffset, bufferedBytes.Length - messageOffset);
    
    Console.WriteLine("GZipCompressed message is {0} bytes", byteArray.Count);
    
    return byteArray;
}

//Helper method to decompress an array of bytes
static ArraySegment<byte> DecompressBuffer
(ArraySegment<byte> buffer, BufferManager bufferManager)
{
    Console.WriteLine(string.Format
    	("Compressed buffer size is {0} bytes", buffer.Count));
    var memoryStream = new MemoryStream
    	(buffer.Array, buffer.Offset, buffer.Count - buffer.Offset);
    var decompressedStream = new MemoryStream();
    var totalRead = 0;
    var blockSize = 1024;
    var tempBuffer = bufferManager.TakeBuffer(blockSize);
    using (var gzStream = new GZipStream(memoryStream, CompressionMode.Decompress))
    {
        while (true)
        {
            var bytesRead = gzStream.Read(tempBuffer, 0, blockSize);
            if (bytesRead == 0)
                break;
            decompressedStream.Write(tempBuffer, 0, bytesRead);
            totalRead += bytesRead;
        }
    }
    bufferManager.ReturnBuffer(tempBuffer);
    
    var decompressedBytes = decompressedStream.ToArray();
    var bufferManagerBuffer = bufferManager.TakeBuffer
    	(decompressedBytes.Length + buffer.Offset);
    Array.Copy(buffer.Array, 0, bufferManagerBuffer, 0, buffer.Offset);
    Array.Copy(decompressedBytes, 0, bufferManagerBuffer, 
    	buffer.Offset, decompressedBytes.Length);
    
    var byteArray = new ArraySegment<byte>
    	(bufferManagerBuffer, buffer.Offset, decompressedBytes.Length);
    bufferManager.ReturnBuffer(buffer.Array);
    Console.WriteLine(string.Format
    	("Decompressed buffer size is {0} bytes", byteArray.Count));
    return byteArray;
}    

Using the Code

For simplicity, I've hosted the WCF service at console. To run the sample code and see it working, follow the steps below:

  1. After opening the "ExtendingWCFPartII" solution with VS, right click on the ConsoleServices project, then click Debug -- > Start new instance. Doing so, you have ensured that the service is running at console host.
  2. Then run the WpfClient project under debug mode.
    WcfExtentions.dll is the reusable component which is going to be used at both WCF client and WCF service.

Using the Code at WCF Service

At first, let me show you how we can use this component to host our WCF services. You can host the WCF services at IIS or at Windows Service or at console, no matter where you host it, add the following section at your app.config or web.config ServiceModel section. Add a reference to the WcfExtentions.dll to your service host application.

XML
<system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="ExtendedServiceBehavior" 
	type="WcfExtensions.ServiceBehaviorExtension, WcfExtensions, 
	Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
      <bindingElementExtensions>
        <add name="customMessageEncoding" 
	type="WcfExtensions.MessageEncodingBindingElementExtension, 
	WcfExtensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </bindingElementExtensions>
    </extensions>
</system.serviceModel>

WcfExtentions.ServiceBehaviorExtension has been used to tell WCF framework to use ExtendedServiceBehavior at its CreateBehavior() method. And WcfExtensions.MessageEncodingBindingElementExtension has been used to tell WCF framework to use WcfExtentions.ExtendedMessageEncodingBindingElement for message encoding and decoding.

XML
<system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ExtendedServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
          <serviceThrottling maxConcurrentCalls="16" 
          	maxConcurrentSessions="16" />
          <dataContractSerializer maxItemsInObjectGraph="999999"/>
          <ExtendedServiceBehavior />
        </behavior>
      </serviceBehaviors>
    </behaviors>
</system.serviceModel>

Now create a service behavior like the example above. <ExtendedServiceBehavior /> ensures that this behavior configuration will use the ExtendedServiceBehavior.

XML
<system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="ZipBinding" closeTimeout="00:10:00" 
	openTimeout="00:10:00" receiveTimeout="00:10:00" sendTimeout="00:10:00">
          <customMessageEncoding innerMessageEncoding="mtomMessageEncoding" 
		messageEncoderType="WcfExtensions.GZipMessageEncoder, WcfExtensions">
            <readerQuotas maxDepth="999999999" maxStringContentLength="999999999" 
		maxArrayLength="999999999" maxBytesPerRead="999999999" 
		maxNameTableCharCount="999999999">
            </readerQuotas>
          </customMessageEncoding>
          <httpTransport maxBufferSize="999999999" 
		maxReceivedMessageSize="999999999" authenticationScheme="Anonymous" 
		proxyAuthenticationScheme="Anonymous" useDefaultWebProxy="true"/>
        </binding>
      </customBinding>
    </bindings>
</system.serviceModel>        

The binding element extension "customMessageEncoding" has been used here in this custom binding.

XML
<system.serviceModel>
    <services>
      <service behaviorConfiguration="ExtendedServiceBehavior" 
	name="ConsoleServices.FileService">
        <endpoint address="FileService" binding="customBinding" 
	bindingConfiguration="ZipBinding" contract="Common.Services.IFileService" />
        <endpoint address="FileService/mex" binding="mexHttpBinding" 
	contract="IMetadataExchange"/>
      </service>
    </services>
</system.serviceModel>        

That's all you need to do to host your WCF service which will have the compression functionality explained in the introduction.

Using the Code at WCF Client

Add a reference to the WcfExtentions.dll in your client application. Now create the external configuration file. It may look like the following:

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="ExtendedServiceBehavior"
        type="WcfExtensions.ServiceBehaviorExtension, WcfExtensions,
        Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
      <bindingElementExtensions>
        <add name="customMessageEncoding"
        type="WcfExtensions.MessageEncodingBindingElementExtension,
        WcfExtensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </bindingElementExtensions>
    </extensions>
    <bindings>
        .. tips: provide the same binding which has been used at service
    </bindings>
    <behaviors>
      <endpointBehaviors>
        <behavior name="CustomServiceEndpointBehavior">
          <ExtendedServiceBehavior />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <client>
      <endpoint address="http://localhost:8036/Services/CustomerService"
          binding="customBinding" bindingConfiguration="ZipBinding"
          behaviorConfiguration="CustomServiceEndpointBehavior"
          contract="Common.Services.IFileService" name="FileServiceEndPoint" />
    </client>
  </system.serviceModel>
</configuration>        

Now save this external configuration file at any location of your hard drive with .xml or .config extension, e.g. "C:\Temp\MyAppServices.xml".
I've provided a very simple interface WcfExtentions.WcfClientHelper to get the instance of proxy.

C#
public static T GetProxy<T>(string externalConfigPath)
{
    var channelFactory = new ExtendedChannelFactory<T>(externalConfigPath);
    channelFactory.Open();
    return channelFactory.CreateChannel();
}

At client code, use this interface to call your service method:

C#
var externalConfigPath = @"C:\Temp\MyAppServices.xml";
var proxy = WcfClientHelper.GetProxy<IMyWCFService>(externalConfigPath);
proxy.CallServiceMethod();        

Points of Interest

There are options to use your own implemented MessageEncoder. At first, write your own extended message encoder and provide that type in binding configuration like below. I achieved this by only introducing an extra configuration property "MessageEncoderType" in the WcfExtensions.MessageEncodingBindingElementExtension class.

XML
<system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="ZipBinding" closeTimeout="00:10:00" 
	openTimeout="00:10:00" receiveTimeout="00:10:00" 
		sendTimeout="00:10:00">
          <customMessageEncoding innerMessageEncoding="mtomMessageEncoding" 
		messageEncoderType="YourAssemblyName.YourMessageEncoder, 
		WcfExtensions">
            <readerQuotas maxDepth="999999999" 
            	maxStringContentLength="999999999" 
		maxArrayLength="999999999" maxBytesPerRead="999999999" 
		maxNameTableCharCount="999999999">
            </readerQuotas>
          </customMessageEncoding>
          <httpTransport maxBufferSize="999999999" 
          	maxReceivedMessageSize="999999999" 
		authenticationScheme="Anonymous" 
		proxyAuthenticationScheme="Anonymous" useDefaultWebProxy="true"/>
        </binding>
      </customBinding>
    </bindings>
</system.serviceModel>    

History

  • 20th January, 2010: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)