Kirk Evans Blog

.NET From a Markup Perspective

Modify Message Content With WCF

Modify Message Content With WCF

Rate This
  • Comments 5

Plenty of resources talk about extensibility in WCF, and mention IClientMessageInspector and IDispatchMessageInspector as ways to look at the content of a message, usually dumping to some sort of log.  In this post, I'll highlight their obvious strength:  the capability to change the content of a message.

This post really should be titled something like, "SoapExtensions in ASMX Are Too Complex, and How WCF Fixes This."  You see, I have written a lot of SoapExtensions over the years, and I became increasingly bitter about the programming model each time I needed to use it.  SoapExtensions allow you to inspect and change message content within the ASMX framework.  For an example of how complex they can become, see an example of a SoapExtension I wrote to programmatically insert SOAP headers into a message.  SoapExtensions were really somewhat of an unapproachable black art, using the odd ChainStream model (see Steve Maine's excellent rant on ChainStream and why it is just unnatural).

WCF changes all of that.

I wrote before on a bit of the WCF extensibility model, showing how easy it is to inspect messages on the server and dump the message out to Visual Studio's output window.  Now, let's see how easy it is to inspect and modify messages on the client.

First, let's picture a scenario where this is at all useful.  Amazon Web Services sometimes return data that does not conform to the WSDL describing the service.  I find it comical that the thread referenced in that link spends so much time saying "it's not a bug" and then devolves into a REST versus SOAP debate, because there clearly is a problem that exists for either REST or SOAP services: you don't get what you expect to get, and it won't pass validation.  For instance, there is a type called DecimalWithUnits with the following schema definition:

<xs:complexType name="DecimalWithUnits">
    <xs:simpleContent>
        <xs:extension base="xs:decimal">
            <xs:attribute name="Units" type="xs:string" use="required" /> 
        </xs:extension>
    </xs:simpleContent>
 </xs:complexType>

Valid XML for this definition would look like the following:

<GetDataResult Units="Ounces">10.5</Book>

However, what is sometimes returned from the service is:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IService1/GetDataResponse</Action>
  </s:Header>
  <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <GetDataResponse xmlns="http://tempuri.org/">
      <GetDataResult Units="unknown-value">10.5 ounces</GetDataResult>
    </GetDataResponse>
  </s:Body>
</s:Envelope>

Notice that the "ounces" is appended to the value here.  That's not a decimal, that's a string, but the WSDL says we should expect a decimal.  The generated proxy expects a decimal and maps to a decimal CLR type, because that's what we were told to expect.  If we use svcutil.exe or Visual Studio's  Add Service Reference dialog to generate a proxy, it will generate a class that has a decimal type (according to the WSDL).  However, when we call the service, we get a nice exception:

System.FormatException occurred
  Message="Input string was not in a correct format."
  Source="mscorlib"
 

Hopefully you can see the obvious issue:  the WSDL describes the content of the element as a decimal, but the service sometimes returns string content that cannot be converted to a decimal (violating its own contract).  Without rewriting their WSDL to reflect what it actually returns and re-generating our proxy, we can still get around this by modifying the contents of the message before it is de-serialized into an object.

We will use an IClientMessageInspector to inspect the contents of the message after the reply is received.

using System;
using System.Xml;
using System.IO;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;

namespace DPE.Samples
{
    public class MyInspector : IClientMessageInspector
    {
        public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
            MemoryStream memStream = new MemoryStream();
            XmlDictionaryWriter xdw = XmlDictionaryWriter.CreateBinaryWriter(memStream);

            xdw.WriteStartElement("GetDataResponse", "http://tempuri.org/");
            xdw.WriteStartElement("GetDataResult", "http://tempuri.org/");
            xdw.WriteAttributeString("Units", "ounces");
            xdw.WriteString("10.5");
            xdw.WriteEndElement();
            xdw.WriteEndElement();
            xdw.Flush(); 
            memStream.Position = 0;

            XmlDictionaryReaderQuotas quotas = new XmlDictionaryReaderQuotas();
            XmlDictionaryReader xdr = XmlDictionaryReader.CreateBinaryReader(memStream, quotas);

            Message replacedMessage = Message.CreateMessage(reply.Version, null, xdr);
            replacedMessage.Headers.CopyHeadersFrom(reply.Headers);
            replacedMessage.Properties.CopyProperties(reply.Properties);
            reply = replacedMessage;
        }

        public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
        {
            return null;            
        }     
    }
}

This is a pretty simple bit of code once you look at it.  We create a binary XmlDictionaryWriter to write whatever we want as the body of the message.  We can add new elements, change elements to attributes, whatever we want... we have total control here.  Once we have the writer's contents all squared away, we create an XmlDictionaryReader to read the contents back into a Message type.  I could have used XSLT or some other transformation component to do this instead of hard-coding (maybe even Windows Workflow's rules engine), I leave that as an exercise to the reader.  The last thing we do is copy some stuff from the original Message into our new one, and then overwrite the old Message. 

To use this message inspector, we can use a behavior to add the inspector into the pipeline. This is a really simple little bit of code to write.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

namespace DPE.Samples
{
    public class MyBehavior : IEndpointBehavior
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            //no-op
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            MyInspector inspector = new MyInspector();
            clientRuntime.MessageInspectors.Add(inspector);
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            //no-op
        }

        public void Validate(ServiceEndpoint endpoint)
        {
            //no-op
        }
    }    
}

The last things we need to do is consume our behavior.  There are several ways to do this (take a look at some of the resources for more flexible options), we will simply add the behavior in by coding it into our proxy.

using System;
using ConsoleApplication2.ServiceReference1;

namespace DPE.Samples
{
    class Program
    {
        static void Main(string[] args)
        {
            Service1Client proxy = new Service1Client();
            proxy.Endpoint.Behaviors.Add(new MyBehavior());
            DecimalWithUnits aws = proxy.GetData("hello");
            Console.WriteLine(aws);
        }
    }
}

OK, so we change the data, but what does that look like when I try to use the "aws" type?  What does it contain?  It contains exactly what we specified in the IClientMessageInspector.

 

That's it!  Look at how much simpler and cleaner this approach is than the old SoapExtension way of doing things.  We have clear visibility into where this action is occurring, because IClientMessageInspector only happens at the request side of things, on the "client".  IDispatchMessageInspector is the interface you would use for server-side inspection and manipulation, just like I showed in the referenced article.

For more information on behaviors, inspectors, and extensibility in WCF:

 

Page 1 of 1 (5 items)
Leave a Comment
  • Please add 5 and 2 and type the answer here:
  • Post
Translate This Page
Search
Archive
Archives