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
Figure 1 shows the place where we're going to extend in WCF architecture.
A typical wsHttpBinding
may look like the following:
<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
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.
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new ExtendedMessageEncoderFactory
(innerBindingElement.CreateMessageEncoderFactory(), _encoderType);
}
At the constructor of the ExtendedMessageEncoderFactory
, we're constructing the custom message 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.
static ArraySegment<byte> CompressBuffer(ArraySegment<byte> buffer,
BufferManager bufferManager, int messageOffset)
{
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;
}
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:
- 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. - 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.
<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.
<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
.
<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.
<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:
="1.0"="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.
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:
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.
<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