Custom Message Encoder: Compression Encoder

The Compression sample demonstrates how to implement a custom encoder using the Windows Communication Foundation (WCF) platform.

Sample Details

This sample consists of a client console program (.exe), a self-hosted service console program (.exe) and a compression message encoder library (.dll). The service implements a contract that defines a request-reply communication pattern. The contract is defined by the ISampleServer interface, which exposes basic string echoing operations (Echo and BigEcho). The client makes synchronous requests to a given operation and the service replies by repeating the message back to the client. Client and service activity is visible in the console windows. The intent of this sample is to show how to write a custom encoder and demonstrate the impact of compression of a message on the wire. You can add instrumentation to the compression message encoder to calculate message size, processing time, or both.

Note

In the .NET Framework 4, automatic decompression has been enabled on a WCF client if the server is sending a compressed response (created with an algorithm such as GZip or Deflate). If the service is Web-hosted in Internet Information Server (IIS), then IIS can be configured for the service to send a compressed response. This sample can be used if the requirement is to do compression and decompression on both the client and the service or if the service is self-hosted.

The sample demonstrates how to build and integrate a custom message encoder into a WCF application. The library GZipEncoder.dll is deployed with both the client and the service. This sample also demonstrates the impact of compressing messages. The code in GZipEncoder.dll demonstrates the following:

  • Building a custom encoder and encoder factory.

  • Developing a binding element for a custom encoder.

  • Using the custom binding configuration for integrating custom binding elements.

  • Developing a custom configuration handler to allow file configuration of a custom binding element.

As indicated previously, there are several layers that are implemented in a custom encoder. To better illustrate the relationship between each of these layers, a simplified order of events for service start-up is in the following list:

  1. The server starts.

  2. The configuration information is read.

    1. The service configuration registers the custom configuration handler.

    2. The service host is created and opened.

    3. The custom configuration element creates and returns the custom binding element.

    4. The custom binding element creates and returns a message encoder factory.

  3. A message is received.

  4. The message encoder factory returns a message encoder for reading in the message and writing out the response.

  5. The encoder layer is implemented as a class factory. Only the encoder class factory must be publicly exposed for the custom encoder. The factory object is returned by the binding element when the ServiceHost or ChannelFactory<TChannel> object is created. Message encoders can operate in a buffered or streaming mode. This sample demonstrates both buffered mode and streaming mode.

For each mode there is an accompanying ReadMessage and WriteMessage method on the abstract MessageEncoder class. A majority of the encoding work takes place in these methods. The sample wraps the existing text and binary message encoders. This allows the sample to delegate the reading and writing of the wire representation of messages to the inner encoder and allows the compression encoder to compress or decompress the results. Because there is no pipeline for message encoding, this is the only model for using multiple encoders in WCF. Once the message has been decompressed, the resulting message is passed up the stack for the channel stack to handle. During compression, the resulting compressed message is written directly to the stream provided.

This sample uses helper methods (CompressBuffer and DecompressBuffer) to perform conversion from buffers to streams to use the GZipStream class.

The buffered ReadMessage and WriteMessage classes make use of the BufferManager class. The encoder is accessible only through the encoder factory. The abstract MessageEncoderFactory class provides a property named Encoder for accessing the current encoder and a method named CreateSessionEncoder for creating an encoder that supports sessions. Such an encoder can be used in the scenario where the channel supports sessions, is ordered and is reliable. This scenario allows for optimization in each session of the data written to the wire. If this is not desired, the base method should not be overloaded. The Encoder property provides a mechanism for accessing the session-less encoder and the default implementation of the CreateSessionEncoder method returns the value of the property. Because the sample wraps an existing encoder to provide compression, the MessageEncoderFactory implementation accepts a MessageEncoderFactory that represents the inner encoder factory.

Now that the encoder and encoder factory are defined, they can be used with a WCF client and service. However, these encoders must be added to the channel stack. You can derive classes from the ServiceHost and ChannelFactory<TChannel> classes and override the OnInitialize methods to add this encoder factory manually. You can also expose the encoder factory through a custom binding element.

To create a new custom binding element, derive a class from the BindingElement class. There are, however, several types of binding elements. To ensure that the custom binding element is recognized as a message encoding binding element, you also must implement the MessageEncodingBindingElement. The MessageEncodingBindingElement exposes a method for creating a new message encoder factory (CreateMessageEncoderFactory), which is implemented to return an instance of the matching message encoder factory. Additionally, the MessageEncodingBindingElement has a property to indicate the addressing version. Because this sample wraps the existing encoders, the sample implementation also wraps the existing encoder binding elements and takes an inner encoder binding element as a parameter to the constructor and exposes it through a property. The following sample code shows the implementation of the GZipMessageEncodingBindingElement class.

public sealed class GZipMessageEncodingBindingElement
                        : MessageEncodingBindingElement //BindingElement
                        , IPolicyExportExtension
{

    //We use an inner binding element to store information
    //required for the inner encoder.
    MessageEncodingBindingElement innerBindingElement;

        //By default, use the default text encoder as the inner encoder.
        public GZipMessageEncodingBindingElement()
            : this(new TextMessageEncodingBindingElement()) { }

    public GZipMessageEncodingBindingElement(MessageEncodingBindingElement messageEncoderBindingElement)
    {
        this.innerBindingElement = messageEncoderBindingElement;
    }

    public MessageEncodingBindingElement InnerMessageEncodingBindingElement
    {
        get { return innerBindingElement; }
        set { innerBindingElement = value; }
    }

    //Main entry point into the encoder binding element.
    // Called by WCF to get the factory that creates the
    //message encoder.
    public override MessageEncoderFactory CreateMessageEncoderFactory()
    {
        return new
GZipMessageEncoderFactory(innerBindingElement.CreateMessageEncoderFactory());
    }

    public override MessageVersion MessageVersion
    {
        get { return innerBindingElement.MessageVersion; }
        set { innerBindingElement.MessageVersion = value; }
    }

    public override BindingElement Clone()
    {
        return new
        GZipMessageEncodingBindingElement(this.innerBindingElement);
    }

    public override T GetProperty<T>(BindingContext context)
    {
        if (typeof(T) == typeof(XmlDictionaryReaderQuotas))
        {
            return innerBindingElement.GetProperty<T>(context);
        }
        else
        {
            return base.GetProperty<T>(context);
        }
    }

    public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        context.BindingParameters.Add(this);
        return context.BuildInnerChannelFactory<TChannel>();
    }

    public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        context.BindingParameters.Add(this);
        return context.BuildInnerChannelListener<TChannel>();
    }

    public override bool CanBuildChannelListener<TChannel>(BindingContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        context.BindingParameters.Add(this);
        return context.CanBuildInnerChannelListener<TChannel>();
    }

    void IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext policyContext)
    {
        if (policyContext == null)
        {
            throw new ArgumentNullException("policyContext");
        }
       XmlDocument document = new XmlDocument();
       policyContext.GetBindingAssertions().Add(document.CreateElement(
            GZipMessageEncodingPolicyConstants.GZipEncodingPrefix,
            GZipMessageEncodingPolicyConstants.GZipEncodingName,
            GZipMessageEncodingPolicyConstants.GZipEncodingNamespace));
    }
}

Note that GZipMessageEncodingBindingElement class implements the IPolicyExportExtension interface, so that this binding element can be exported as a policy in metadata, as shown in the following example.

<wsp:Policy wsu:Id="BufferedHttpSampleServer_ISampleServer_policy">
    <wsp:ExactlyOne>
      <wsp:All>
        <gzip:text xmlns:gzip=
        "http://schemas.microsoft.com/ws/06/2004/mspolicy/netgzip1" />
       <wsaw:UsingAddressing />
     </wsp:All>
   </wsp:ExactlyOne>
</wsp:Policy>

The GZipMessageEncodingBindingElementImporter class implements the IPolicyImportExtension interface, this class imports policy for GZipMessageEncodingBindingElement. Svcutil.exe tool can be used to import policies to the configuration file, to handle GZipMessageEncodingBindingElement, the following should be added to Svcutil.exe.config.

<configuration>
  <system.serviceModel>
    <extensions>
      <bindingElementExtensions>
        <add name="gzipMessageEncoding"
          type=
            "Microsoft.ServiceModel.Samples.GZipMessageEncodingElement, GZipEncoder, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      </bindingElementExtensions>
    </extensions>
    <client>
      <metadata>
        <policyImporters>
          <remove type=
"System.ServiceModel.Channels.MessageEncodingBindingElementImporter, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
          <extension type=
"Microsoft.ServiceModel.Samples.GZipMessageEncodingBindingElementImporter, GZipEncoder, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        </policyImporters>
      </metadata>
    </client>
  </system.serviceModel>
</configuration>

Now that there is a matching binding element for the compression encoder, it can be programmatically hooked into the service or client by constructing a new custom binding object and adding the custom binding element to it, as shown in the following sample code.

ICollection<BindingElement> bindingElements = new List<BindingElement>();
HttpTransportBindingElement httpBindingElement = new HttpTransportBindingElement();
GZipMessageEncodingBindingElement compBindingElement = new GZipMessageEncodingBindingElement ();
bindingElements.Add(compBindingElement);
bindingElements.Add(httpBindingElement);
CustomBinding binding = new CustomBinding(bindingElements);
binding.Name = "SampleBinding";
binding.Namespace = "http://tempuri.org/bindings";

While this may be sufficient for the majority of user scenarios, supporting a file configuration is critical if a service is to be Web-hosted. To support the Web-hosted scenario, you must develop a custom configuration handler to allow a custom binding element to be configurable in a file.

You can build a configuration handler for the binding element on top of the configuration system. The configuration handler for the binding element must derive from the BindingElementExtensionElement class. The BindingElementExtensionElement.BindingElementType informs the configuration system of the type of binding element to create for this section. All aspects of the BindingElement that can be set should be exposed as properties in the BindingElementExtensionElement derived class. The ConfigurationPropertyAttribute assists in mapping the configuration element attributes to the properties and setting default values if attributes are missing. After the values from configuration are loaded and applied to the properties, the BindingElementExtensionElement.CreateBindingElement method is called, which converts the properties into a concrete instance of a binding element. The BindingElementExtensionElement.ApplyConfiguration method is used to convert the properties on the BindingElementExtensionElement derived class into the values to be set on the newly created binding element.

The following sample code shows the implementation of the GZipMessageEncodingElement.

public class GZipMessageEncodingElement : BindingElementExtensionElement
{
    public GZipMessageEncodingElement()
    {
    }

//Called by the WCF to discover the type of binding element this
//config section enables
    public override Type BindingElementType
    {
        get { return typeof(GZipMessageEncodingBindingElement); }
    }

    //The only property we need to configure for our binding element is
    //the type of inner encoder to use. Here, we support text and
    //binary.
    [ConfigurationProperty("innerMessageEncoding",
                         DefaultValue = "textMessageEncoding")]
    public string InnerMessageEncoding
    {
        get { return (string)base["innerMessageEncoding"]; }
        set { base["innerMessageEncoding"] = value; }
    }

    //Called by the WCF to apply the configuration settings (the
    //property above) to the binding element
    public override void ApplyConfiguration(BindingElement bindingElement)
    {
        GZipMessageEncodingBindingElement binding =
                (GZipMessageEncodingBindingElement)bindingElement;
        PropertyInformationCollection propertyInfo =
                    this.ElementInformation.Properties;
        if (propertyInfo["innerMessageEncoding"].ValueOrigin !=
                                     PropertyValueOrigin.Default)
        {
            switch (this.InnerMessageEncoding)
            {
                case "textMessageEncoding":
                    binding.InnerMessageEncodingBindingElement =
                      new TextMessageEncodingBindingElement();
                    break;
                case "binaryMessageEncoding":
                    binding.InnerMessageEncodingBindingElement =
                         new BinaryMessageEncodingBindingElement();
                    break;
            }
        }
    }

    //Called by the WCF to create the binding element
    protected override BindingElement CreateBindingElement()
    {
        GZipMessageEncodingBindingElement bindingElement =
                new GZipMessageEncodingBindingElement();
        this.ApplyConfiguration(bindingElement);
        return bindingElement;
    }
}

This configuration handler maps to the following representation in the App.config or Web.config for the service or client.

<gzipMessageEncoding innerMessageEncoding="textMessageEncoding" />

To use this configuration handler, it must be registered within the <system.serviceModel> element, as shown in the following sample configuration.

<extensions>
    <bindingElementExtensions>
       <add
           name="gzipMessageEncoding"
           type=
           "Microsoft.ServiceModel.Samples.GZipMessageEncodingElement,
           GZipEncoder, Version=1.0.0.0, Culture=neutral,
           PublicKeyToken=null" />
      </bindingElementExtensions>
</extensions>

When you run the server, the operation requests and responses are displayed in the console window. Press ENTER in the window to shut down the server.

Press Enter key to Exit.

        Server Echo(string input) called:
        Client message: Simple hello

        Server BigEcho(string[] input) called:
        64 client messages

When you run the client, the operation requests and responses are displayed in the console window. Press ENTER in the client window to shut down the client.

Calling Echo(string):
Server responds: Simple hello Simple hello

Calling BigEcho(string[]):
Server responds: Hello 0

Press <ENTER> to terminate client.

To set up, build, and run the sample

  1. Install ASP.NET 4.0 using the following command:

    %windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
    
  2. Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples.

  3. To build the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  4. To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.