Getting the raw SOAP XML sent via SoapHttpClientProtocol


Suppose you’re using the .NET SoapHttpClientProtocol to invoke a web API.  This is what happens when you use Visual Studio to add a Web Reference and automatically build a proxy for you.  Now suppose you want to programmatically access the raw SOAP XML that you’re sending to the web API.  Sounds straightforward, right?

Turns out, it isn’t straightforward at all.

Looking online for some help, a few solutions have been proposed.  Some people suggest using a network sniffer or HTTP proxy to get the raw SOAP XML.  That can work, but it’s not a good solution for programmatically getting the XML.  It’s also somewhat labor intensive to setup initially and then use on a regular basis.

It’s probably possible to create a SOAP Extension to do this.  But that’s a bit heavyweight for my purposes.

One guy dug into the guts of the .NET assembly in the debugger to find the spot in memory where the XML document can be found.  Impressive, but again, not entirely practical for programmatic access.

After some trial-and-error, I decided upon a strategy that allows us to get what we need pretty reliably.  Hopefully this helps you.

Let’s say your WSDL has a service called HelloService (from the WSDL’s <service> tag).  When you add it as a Web Reference, Visual Studio automatically creates a nice class called HelloService derived from SoapHttpClientProtocol.  What we want to do is this:

HelloService svc = new HelloService();
svc.doSomething();
string rawXml = svc.Xml();

To add the Xml property, we could just add directly to the auto-generated class.  But that’s not ideal because if you get a new WSDL, for example, and re-generate the class, you’ll destroy changes you’ve made.  So let’s create our own subclass of HelloService.  That’s easy enough, something like this should do it:

namespace MyProject
{
   public class MyHelloService : HelloService
   {
      public MyHelloService : base() { }
      public string Xml { get { return null; } }
   }
}

Now  we should change our code to use this new subclass:

MyHelloService svc = new MyHelloService();
svc.doSomething();
string rawXml = svc.Xml();

So far so good, but what now?

Now we need to intercept the XML that gets created during SoapHttpClientProtocol.Invoke().  There’s a convenient point for doing that: GetWriterForMessage().  It’s responsible for returning an XmlWriter that gets used to build the SOAP XML message.

To do that, we’ll need our own XmlWriter that wraps another XmlWriter, the original one returned by the HelloService class.  Our strategy is to intercept all calls to the original XmlWriter and write those to our StringWriter.  It’s an XmlWriterSpy.  Here’s how it looks (some methods omitted for brevity):

namespace MyProject
{
    using System.IO;
    using System.Xml;
    public class XmlWriterSpy : XmlWriter
    {
        private XmlWriter _me;
        private XmlTextWriter _bu; // Buffer to write XML to
        private StringWriter _sw;

        public XmlWriterSpy(XmlWriter implementation)
        {
            _me = implementation;
            _sw = new StringWriter();
            _bu = new XmlTextWriter(_sw);
            _bu.Formatting = Formatting.Indented;
        }
        public override void Flush()
        {
            _me.Flush();
            _bu.Flush();
            _sw.Flush();
        }
        public string Xml { get { return (_sw == null ? null : _sw.ToString()); } }

        public override void Close() { _me.Close(); _bu.Close(); }
        public override string LookupPrefix(string ns) { return _me.LookupPrefix(ns); }
        public override void WriteBase64(byte[] buffer, int index, int count) { _me.WriteBase64(buffer, index, count); _bu.WriteBase64(buffer, index, count); }

        // ...more overrides omitted, you get the idea...

        public override void WriteSurrogateCharEntity(char lowChar, char highChar) { _me.WriteSurrogateCharEntity(lowChar, highChar); _bu.WriteSurrogateCharEntity(lowChar, highChar); }
        public override void WriteWhitespace(string ws) { _me.WriteWhitespace(ws); _bu.WriteWhitespace(ws); }

    }
}

Lastly, we just need to use this new XmlWriterSpy class in our MyHelloService class.  Here’s how:

namespace MyProject
{
   public class MyHelloService : HelloService
   {
      private XmlWriterSpy writer;
      public MyHelloService() : base() { }

      protected override  XmlWriter GetWriterForMessage( SoapClientMessage message,  int bufferSize)
      {
         writer =  new XmlWriterSpy( base.GetWriterForMessage(message, bufferSize));
         return writer;
      }

      public string Xml { get { return (writer == null ? null : writer.Xml); } }
   }
}

There you have it.

Update (April 2, 2010): As Robert pointed out, it would be nice to have the XmlWriterSpy easily downloadable in its entirety.  True!  Here it is.

, , ,