Search code examples
c#xmlwcf

WCF client, XML namespace prefix results in null object


I have created a WCF service (.NET 4.6.2) and a third party has created the client.

I have no control over the client, or the ability to get the third party to change anything, so any changes are going to be on the server side only.

I have minimised and anonymised the code to the bare minimum to hopefully demonstrate the problem in as little code as possible. If I made any mistakes or you need any further detail, I will alter the details accordingly.


Summary

The client is able to call the service, and the parser is able to correctly identify the method in code from the SOAP Action, however any objects passed in are always null. I am able to catch the requests with a breakpoint and see that the objects are null at runtime.

I have identified that the problem is being caused mostly my an XML namespace prefix that the client is passing.

I am able to intercept the incoming raw messages and I can see exactly what the client is sending.

I am able to manipulate these incoming messages, for experimentation, and then post the modified version of the message to the service to test the results using a generic browser based client.


Example Data Contract:-

[DataContract(Namespace = "http://mycompanyname.co.uk")]
public class SomeObject
{
    [DataMember]
    public string SomeField { get; set; }
}


Example Service Contract:-

[ServiceContract(Namespace = "http://mycompanyname.co.uk")]
public interface IIncoming
{
    [OperationContract]
    XmlElement MyAction(SomeObject someObject);
}


Example WCF service implementing the previously defined Service Contract:-

[ServiceBehavior(Namespace = "http://mycompanyname.co.uk")]
public class Incoming : IIncoming
{
    public XmlElement MyAction(SomeObject someObject)
    {
        XmlDocument response = new XmlDocument();
        response.LoadXml("<Response>OK</Response>");
        return response.DocumentElement;
    }
}


This is what the 3rd party posts to the service:-

/*3rd party original*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Body>
        <ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
            <someObject xmlns="">
                <SomeField>Blah</SomeField>
            </someObject>
        </ns1:MyAction>
    </soapenv:Body>
</soapenv:Envelope>

Breakpointing the code, I can see that the above results in MyAction being called, but MyObject is null


I modified the SOAP message to remove the blank xmlns, but someObject is still null

/*blank xmlns removed*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Body>
        <ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
            <someObject>
                <SomeField>Blah</SomeField>
            </someObject>
        </ns1:MyAction>
    </soapenv:Body>
</soapenv:Envelope>


I modified the SOAP message to remove the ns1 prefix, but someObject is still null

/*ns1 removed*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Body>
        <MyAction xmlns="http://mycompanyname.co.uk">
            <someObject xmlns="">
                <SomeField>Blah</SomeField>
            </someObject>
        </MyAction>
    </soapenv:Body>
</soapenv:Envelope>


Now if I both remove the ns1 prefix AND the blank xmlns, then someObject is correctly populated as expected. This resolves everything. The only problem is, I can't get the client to make this change.

/*both removed - works*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Body>
        <MyAction xmlns="http://mycompanyname.co.uk">
            <someObject>
                <SomeField>Blah</SomeField>
            </someObject>
        </MyAction>
    </soapenv:Body>
</soapenv:Envelope>


So firstly, why does this happen? as I understand it in XML, the prefix is effectively irrelevant in determining an object, so <ns1:Something> should be the same object as <ns2:Something> or <Something> The WCF parser is however determining that <ns1:Something> is not a <Something>


How can I resolve this problem server side, preferably from within the WCF service? I was thinking if there is a way to capture the message before it is parsed in the WCF service and strip the ns1: and blank xmlns out beforehand?

Many thanks

EDIT

Here is an example that you can copy/paste to reproduce the problem exactly:-

Create a WCF Service Application (.NET Framework 4.6.2)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Xml;

namespace GenericIncoming
{
    [ServiceContract(Namespace = "http://mycompanyname.co.uk")]
    public interface IIncoming
    {
        [OperationContract]
        XmlElement MyAction(SomeObject someObject);
    }

    [DataContract(Namespace ="")]
    public class SomeObject
    {
        [DataMember]
        public string SomeField { get; set; }
    }
}



using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Xml;

namespace GenericIncoming
{
    [ServiceBehavior(Namespace = "http://mycompanyname.co.uk")]
    public class Incoming : IIncoming
    {
        public XmlElement MyAction(SomeObject someObject)
        {
            XmlDocument response = new XmlDocument();
            if (someObject != null)
            {
                response.LoadXml("<Response>OK</Response>");
            }
            else
            {
                response.LoadXml("<Response>NULL</Response>");
            }
            return response.DocumentElement;
        }
    }
}

Run your WCF service and POST this message to the service, and you will get an "OK" response

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Body>
        <MyAction xmlns="http://mycompanyname.co.uk">
            <someObject>
                <SomeField>Blah</SomeField>
            </someObject>
        </MyAction>
    </s:Body>
</s:Envelope>

Now if you add the ns1 prefix, just like the 3rd party sends to me, then the response is "NULL" this means that the object in the WCF is null because the parser was unable to deal with the XML

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Body>
        <ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
            <someObject>
                <SomeField>Blah</SomeField>
            </someObject>
        </ns1:MyAction>
    </s:Body>
</s:Envelope>

This is the first problem that I am unable to resolve


Solution

  • I've invested a huge amount of time looking for a simple solution, but one doesn't seem to exist.

    In order to resolve this problem, I ended up researching MessageInspectors, which allow the viewing and editing of the raw SOAP messages before they are processed by the WCF service.

    Create a class that implements IDispatchMessageInspector. This is where the actual filtering will occur

    public class CorrectorInspector : IDispatchMessageInspector
    {
        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            request = FilterMessage(request);
            return null;
        }
    
        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            return;
        }
    
        private Message FilterMessage(Message originalMessage)
        {
            MemoryStream memoryStream = new MemoryStream();
            XmlWriter xmlWriter = XmlWriter.Create(memoryStream);
            originalMessage.WriteMessage(xmlWriter);
            xmlWriter.Flush();
            string body = Encoding.UTF8.GetString(memoryStream.ToArray());
            xmlWriter.Close();
    
            //Remove the ns1 prefix
            body = body.Replace("ns1:", "");
            body = body.Replace(":ns1", "");
            //remove the blank namespace 
            body = body.Replace(" xmlns=\"\"","");
    
            memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(body));
            XmlDictionaryReader xmlDictionaryReader = XmlDictionaryReader.CreateTextReader(memoryStream, new XmlDictionaryReaderQuotas());
            Message newMessage = Message.CreateMessage(xmlDictionaryReader, int.MaxValue, originalMessage.Version);
            newMessage.Properties.CopyProperties(originalMessage.Properties);
            return newMessage;
        }
    }
    

    Create a class that implements IEndpointBehavior

    public class CorrectorBehavior : IEndpointBehavior
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            return;
        }
    
        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            return;
        }
    
        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            CorrectorInspector inspector = new CorrectorInspector();
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
        }
    
        public void Validate(ServiceEndpoint endpoint)
        {
            return;
        }
    }
    

    Create a class that implements BehaviorExtensionElement

    public class CorrectorBehaviourExtensionElement : BehaviorExtensionElement
    {
        public override Type BehaviorType
        {
            get
            {
                return typeof(CorrectorBehavior);
            }
        }
    
        protected override object CreateBehavior()
        {
            return new CorrectorBehavior();
        }
    }
    

    In the web.config, we need to define the behaviorExtension, the serviceBehavior, and then we need to specify our newly created behaviour against the service binding

     <system.serviceModel>
        <services>
          <service name="GenericIncoming.Incoming">
            <endpoint address="" binding="basicHttpBinding" contract="GenericIncoming.IIncoming" behaviorConfiguration="correctorBehavior" />
          </service>
        </services>    
        <behaviors>
          <serviceBehaviors>
            <behavior>
              <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
              <serviceDebug includeExceptionDetailInFaults="false"/>
            </behavior>
          </serviceBehaviors>
          <endpointBehaviors>
            <behavior name="correctorBehavior">
              <serviceFilter />
            </behavior>
          </endpointBehaviors>      
        </behaviors>
        <extensions>
          <behaviorExtensions>
            <add name="serviceFilter" type="GenericIncoming.CorrectorBehaviourExtensionElement, GenericIncoming, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          </behaviorExtensions>
        </extensions>
        <protocolMapping>
            <add binding="basicHttpsBinding" scheme="https" />
        </protocolMapping>    
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
      </system.serviceModel>
    

    This is a fairly heavyweight solution, but it does indeed work perfectly, and learning this functionality will surely be useful for future projects.