apiWCF

Changing SOAP message data and namespaces with C#

WCF, you got to love it! I don't, but then again I don't get to always chose which technologies I can work with. I've recently come across a very peculiar and annoying problem while working against a particular web service. I was able to hit the endpoints using SOAP UI but all calls from code were failing miserably. The project required importing a third party WSDL in order to generate the appropriate proxy classes etc. The WSDL had to be added as a Service Refernce instead of Web Reference as the exteranl API did not expose a Web Services Metadata Exchange.

With the proxy in place I assumed that everything should work as expected but this could not be far from truth. In the end, I fired up Fiddler and started comparing the SOAP UI messages to the C# ones. The difference in the payload was quite significant, mainly around the namespace declaration. The API expected all namespaces to be declared at the top, while the proxy was declaring the namespaces against each individual element.

It took quite a bit of google/Bing(ing) to realize that there is no way to change the <envelop> format without performing some serious workarounds. Also, since I had no access to the server running the API, there was no way to use handlers etc to change the message format. And finally, I was not allowed to touch or alter the auto-generated proxy from WSDL! Challenge accepted

The Solution

In the end, I came up with a perfectly satisfactory solution that met all the above requirements and didn't look like a dirty, aweful hack that you try to forget as soon as it goes into production. The solution provided below allows you to change any part of the message on the client side before sending it to the server. This way, you have full control of the generated SOAP XML.

First, we need to to implement a custom IClientMessageInspector implementation. This was my way in and I could access the full SOAP message and change it as needed.

using System;
using System.IO;
using System.Net;
using System.Security.Policy;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Xml;

namespace APIServiceConsumer.Services
{
    public class ClientMessageInspector : IClientMessageInspector
    {
        public string LastRequestXml { get; private set; }
        public string LastResponseXml { get; private set; }
        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            LastResponseXml = reply.ToString();
        }

        public object BeforeSendRequest(ref Message request, System.ServiceModel.IClientChannel channel)
        {
            LastRequestXml = request.ToString();
            var xmlPayload = ChangeMessage();
            var ms = new MemoryStream();
            var writer = new StreamWriter(ms);
            writer.Write(xmlPayload);
            writer.Flush();
            ms.Position = 0;

            var reader = XmlReader.Create(ms);
            request = Message.CreateMessage(reader, int.MaxValue, request.Version);
            var payload = request.ToString();
            return request;
        }

        /// Manipulate the SOAP message
        private string ChangeMessage()
        {
            // LastRequestXML is a string here. You can change it to your heart's content
            // Sample example here
            
            // NOTE - I couldn't load the string into an XDocument or XMLDocument as it was throwing all sort of errors
            // about undefined namespaces. String manipulation to the rescue!
            
            // strip out the envelope and header
            var startIndexOfStringToKeep = LastRequestXml.IndexOf("RequestHeader", StringComparison.CurrentCulture);
            
            // strip out the footer
            var sanitizedRequestXml = LastRequestXml.Substring(startIndexOfStringToKeep - 1);
            var firstPhase = sanitizedRequestXml.Replace(@"</s:Envelope>", string.Empty);
            var secondPhase = test.Replace(@"</s:Body>", string.Empty);
            var thirdPhase = test2.Replace(@"</SomeServiceRequest>", string.Empty);
            
            // wrap the body with the right element declarations sans any namespaces
            var bodyRequestXml = "<SomeServiceRequest>" + thirdPhase + "</SomeServiceRequest>";

            
            var document = new XmlDocument();
            var root = document.CreateElement("s", "Envelope", "http://schemas.xmlsoap.org/soap/envelope/");
            root.SetAttribute("xmlns:s", "http://schemas.xmlsoap.org/soap/envelope/");
            root.SetAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
            root.SetAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
            root.SetAttribute("xmlns", "http://www.randomnamespaceroot.co.uk/SomeServiceIdentification");
            document.AppendChild(root);

            var bodyHeader = document.CreateElement("s", "Body", "http://schemas.xmlsoap.org/soap/envelope/");
            root.AppendChild(bodyHeader);

            var body = document.CreateElement("IdentifyCustomerRequest", "http://www.randomnamespaceroot.co.uk/SomeServiceIdentification");
            bodyHeader.AppendChild(body);

            var replacedString = GetPayloadString(document);
            var removedString = replacedString.Replace(@"<IdentifyCustomerRequest />", bodyRequestXml);

            return removedString;
        }

        /// Helper method to get the XMLDocument format right
        private string GetPayloadString(XmlDocument document)
        {
            var settings = new XmlWriterSettings();
            settings.OmitXmlDeclaration = true;
            using (var stringWriter = new StringWriter())
            using (var xmlTextWriter = XmlWriter.Create(stringWriter, settings))
            {
                document.WriteTo(xmlTextWriter);
                xmlTextWriter.Flush();
                return stringWriter.GetStringBuilder().ToString();
            }
        }
    }
}

View CustomMessageInspector.cs on GitHub

Next, we need to create a custom implementation of a WCF IEndpointBehavior which is a WCF extensibility feature. You can find more details about this interface here. Within the implementation, we will make use of our CustomMessageInspector that we created above. This will allow us to automatically intercept the SOAP message prior to sending it to the API.

using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace APIServiceConsumer.Services
{
    public class CustomInspectorBehavior : IEndpointBehavior
    {
        private readonly ClientMessageInspector clientMessageInspector = new ClientMessageInspector();

        public string LastRequestXml
        {
            get { return clientMessageInspector.LastRequestXml; }
        }

        public string LastResponseXml
        {
            get { return clientMessageInspector.LastRequestXml; }
        }

        public void AddBindingParameters(
            ServiceEndpoint endpoint,
            System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
        }

        public void Validate(ServiceEndpoint endpoint)
        {
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(clientMessageInspector);
        }
    }
}

View Custom IEndpointBehavior.cs on GitHub

Testing our new solution

If you follow some form of TDD or any other form of unit testing, then you can quickly put a test together to validate that your implementation works.

using APIConsumerServices.Services.CustomService;
using FluentAssertions;
using NUnit.Framework;

namespace APIConsumerServices.Services.Tests
{
    [TestFixture]
    public class CallServiceTest
    {
        // CustomClient is the wrapper around the WSDL generated proxy class
        var testservice = new CustomClient(endpointname, endpoint);
        var requestInterceptor = new CustomInspectorBehavior();
        testservice.Endpoint.Behaviors.Add(requestInterceptor);
        
        testservice.Open();

        var channel = testservice.ChannelFactory.CreateChannel(endpoint);
        var request = testservice.CreateCustomRequest();
        var response = channel.SomeAbritraryMethod(request);
                
        response.Should().NotBeNull("Results were returned successfully");
        
        testservice.Close();
    }
}

View WCF CustomMessageHandler.cs on GitHub

The important code in the unit test is shown below:

var testservice = new CustomClient(endpointname, endpoint);
var requestInterceptor = new CustomInspectorBehavior();
testservice.Endpoint.Behaviors.Add(requestInterceptor);

We instantiate a CustomInspectorBehavior and attach it to the Service (WSDL proxy class wrapper).

And simply like this, you can intercept and change any SOAP message without messing with your proxy classes. I hope this helps you in some way.