Changing prefixes in XML responses for WCF services

When responding to requests using any of the text/XML bindings (BasicHttpBinding / WSHttpBinding / CustomBinding with a TextMessageEncodingBindingElement), WCF has some specific prefixes which it uses corresponding to a certain namespaces. The SOAP namespace (“https://www.w3.org/2003/05/soap-envelope” for SOAP 1.2, “https://schemas.xmlsoap.org/soap/envelope/” for SOAP 1.1), for example, is always mapped to the prefix “s”. This is usually not a problem, but there are some Web Service stack implementations which require that a certain prefix to be used (there have been a few questions in the WCF forums about this issue.

First a small introduction on why this should not be a problem. The following two documents are essentially equivalent, from a XML Infoset standpoint, for “namespace-aware applications”, according to https://www.w3.org/TR/xml-infoset/#infoitem.element:

 <a:root xmlns:a="https://my.namespace">
  <a:item>value</a:item>
</a:root>
 <b:root xmlns:b="https://my.namespace">
  <b:item>value</b:item>
</b:root>

Here, the only difference between those two documents is the prefix used to represent the namespace “https://my.namespace”. So, I imagine that the WCF developers decided to follow that “rule” and didn’t add any simple way of changing the prefixes used in its SOAP responses – after all, it shouldn’t matter. The problem is that sometimes it does :-). Thankfully, the WCF extensibility model is rich enough that we can overcome this limitation by a number of different ways. Here I’ll show one which uses the message encoding extensibility to trap the message right before it’s encoded into bytes (to be sent over the wire), modify it changing its prefixes when necessary, and then proceed with the encoding.

Download the code

The code for this post can be found in the MSDN Code Gallery at https://code.msdn.microsoft.com/Replacing-XML-prefixes-in-a417c6f2.

The prefix changer

The class PrefixReplacer does most of the work, by using the XmlElement class to change, whenever necessary, the elements / attributes of the XML document which have the prefix which needs to be changed.

  1. public class PrefixReplacer
  2. {
  3.     const string XmlnsNamespace = "https://www.w3.org/2000/xmlns/";
  4.     Dictionary<string, string> namespaceToNewPrefixMapping = new Dictionary<string, string>();
  5.  
  6.     public void AddNamespace(string namespaceUri, string newPrefix)
  7.     {
  8.         this.namespaceToNewPrefixMapping.Add(namespaceUri, newPrefix);
  9.     }
  10.  
  11.     public void ChangePrefixes(XmlDocument doc)
  12.     {
  13.         XmlElement element = doc.DocumentElement;
  14.         XmlElement newElement = ChangePrefixes(doc, element);
  15.         doc.LoadXml(newElement.OuterXml);
  16.     }
  17.  
  18.     private XmlElement ChangePrefixes(XmlDocument doc, XmlElement element)
  19.     {
  20.         string newPrefix;
  21.         if (this.namespaceToNewPrefixMapping.TryGetValue(element.NamespaceURI, out newPrefix))
  22.         {
  23.             XmlElement newElement = doc.CreateElement(newPrefix, element.LocalName, element.NamespaceURI);
  24.             List<XmlNode> children = new List<XmlNode>(element.ChildNodes.Cast<XmlNode>());
  25.             List<XmlAttribute> attributes = new List<XmlAttribute>(element.Attributes.Cast<XmlAttribute>());
  26.             foreach (XmlNode child in children)
  27.             {
  28.                 newElement.AppendChild(child);
  29.             }
  30.  
  31.             foreach (XmlAttribute attr in attributes)
  32.             {
  33.                 newElement.Attributes.Append(attr);
  34.             }
  35.  
  36.             element = newElement;
  37.         }
  38.  
  39.         List<XmlAttribute> newAttributes = new List<XmlAttribute>();
  40.         bool modified = false;
  41.         for (int i = 0; i < element.Attributes.Count; i++)
  42.         {
  43.             XmlAttribute attr = element.Attributes[i];
  44.             if (this.namespaceToNewPrefixMapping.TryGetValue(attr.NamespaceURI, out newPrefix))
  45.             {
  46.                 XmlAttribute newAttr = doc.CreateAttribute(newPrefix, attr.LocalName, attr.NamespaceURI);
  47.                 newAttr.Value = attr.Value;
  48.                 newAttributes.Add(newAttr);
  49.                 modified = true;
  50.             }
  51.             else if (attr.NamespaceURI == XmlnsNamespace && this.namespaceToNewPrefixMapping.TryGetValue(attr.Value, out newPrefix))
  52.             {
  53.                 XmlAttribute newAttr;
  54.                 if (newPrefix != "")
  55.                 {
  56.                     newAttr = doc.CreateAttribute("xmlns", newPrefix, XmlnsNamespace);
  57.                 }
  58.                 else
  59.                 {
  60.                     newAttr = doc.CreateAttribute("xmlns");
  61.                 }
  62.  
  63.                 newAttr.Value = attr.Value;
  64.                 newAttributes.Add(newAttr);
  65.                 modified = true;
  66.             }
  67.             else
  68.             {
  69.                 newAttributes.Add(attr);
  70.             }
  71.         }
  72.  
  73.         if (modified)
  74.         {
  75.             element.Attributes.RemoveAll();
  76.             foreach (var attr in newAttributes)
  77.             {
  78.                 element.Attributes.Append(attr);
  79.             }
  80.         }
  81.  
  82.         List<KeyValuePair<XmlNode, XmlNode>> toReplace = new List<KeyValuePair<XmlNode, XmlNode>>();
  83.         foreach (XmlNode child in element.ChildNodes)
  84.         {
  85.             XmlElement childElement = child as XmlElement;
  86.             if (childElement != null)
  87.             {
  88.                 XmlElement newChildElement = ChangePrefixes(doc, childElement);
  89.                 if (newChildElement != childElement)
  90.                 {
  91.                     toReplace.Add(new KeyValuePair<XmlNode, XmlNode>(childElement, newChildElement));
  92.                 }
  93.             }
  94.         }
  95.  
  96.         if (toReplace.Count > 0)
  97.         {
  98.             for (int i = 0; i < toReplace.Count; i++)
  99.             {
  100.                 element.InsertAfter(toReplace[i].Value, toReplace[i].Key);
  101.                 element.RemoveChild(toReplace[i].Key);
  102.             }
  103.         }
  104.  
  105.         return element;
  106.     }
  107. }

The prefix replacer message encoding binding element

The encoding element follows a pattern similar to the custom message encoder found in the WCF samples at https://msdn.microsoft.com/en-us/library/ms751486.aspx. Essentially, it simply delegates all calls to an inner encoder, except when writing messages to the wire; in this case, it will instead first change the message (to replace the prefixes, then call the inner encoder to convert it to bytes.

  1. public class ReplacePrefixMessageEncodingBindingElement : MessageEncodingBindingElement
  2. {
  3.     MessageEncodingBindingElement inner;
  4.     Dictionary<string, string> namespaceToPrefixMapping = new Dictionary<string, string>();
  5.     public ReplacePrefixMessageEncodingBindingElement(MessageEncodingBindingElement inner)
  6.     {
  7.         this.inner = inner;
  8.     }
  9.  
  10.     private ReplacePrefixMessageEncodingBindingElement(ReplacePrefixMessageEncodingBindingElement other)
  11.     {
  12.         this.inner = other.inner;
  13.         this.namespaceToPrefixMapping = new Dictionary<string, string>(other.namespaceToPrefixMapping);
  14.     }
  15.  
  16.     public void AddNamespaceMapping(string namespaceUri, string newPrefix)
  17.     {
  18.         this.namespaceToPrefixMapping.Add(namespaceUri, newPrefix);
  19.     }
  20.  
  21.     public override MessageEncoderFactory CreateMessageEncoderFactory()
  22.     {
  23.         return new ReplacePrefixMessageEncoderFactory(this.inner.CreateMessageEncoderFactory(), this.namespaceToPrefixMapping);
  24.     }
  25.  
  26.     public override MessageVersion MessageVersion
  27.     {
  28.         get { return this.inner.MessageVersion; }
  29.         set { this.inner.MessageVersion = value; }
  30.     }
  31.  
  32.     public override BindingElement Clone()
  33.     {
  34.         return new ReplacePrefixMessageEncodingBindingElement(this);
  35.     }
  36.  
  37.     public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
  38.     {
  39.         context.BindingParameters.Add(this);
  40.         return context.BuildInnerChannelListener<TChannel>();
  41.     }
  42.  
  43.     public override bool CanBuildChannelListener<TChannel>(BindingContext context)
  44.     {
  45.         return context.CanBuildInnerChannelListener<TChannel>();
  46.     }
  47.  
  48.     public static CustomBinding ReplaceEncodingBindingElement(Binding originalBinding, Dictionary<string, string> namespaceToPrefixMapping)
  49.     {
  50.         CustomBinding custom = originalBinding as CustomBinding;
  51.         if (custom == null)
  52.         {
  53.             custom = new CustomBinding(originalBinding);
  54.         }
  55.  
  56.         for (int i = 0; i < custom.Elements.Count; i++)
  57.         {
  58.             if (custom.Elements[i] is MessageEncodingBindingElement)
  59.             {
  60.                 ReplacePrefixMessageEncodingBindingElement element = new ReplacePrefixMessageEncodingBindingElement((MessageEncodingBindingElement)custom.Elements[i]);
  61.                 foreach (var mapping in namespaceToPrefixMapping)
  62.                 {
  63.                     element.AddNamespaceMapping(mapping.Key, mapping.Value);
  64.                 }
  65.  
  66.                 custom.Elements[i] = element;
  67.                 break;
  68.             }
  69.         }
  70.  
  71.         return custom;
  72.     }
  73.  
  74.     class ReplacePrefixMessageEncoderFactory : MessageEncoderFactory
  75.     {
  76.         private MessageEncoderFactory messageEncoderFactory;
  77.         private Dictionary<string, string> namespaceToNewPrefixMapping;
  78.  
  79.         public ReplacePrefixMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory, Dictionary<string, string> namespaceToNewPrefixMapping)
  80.         {
  81.             this.messageEncoderFactory = messageEncoderFactory;
  82.             this.namespaceToNewPrefixMapping = namespaceToNewPrefixMapping;
  83.         }
  84.  
  85.         public override MessageEncoder Encoder
  86.         {
  87.             get { return new ReplacePrefixMessageEncoder(this.messageEncoderFactory.Encoder, this.namespaceToNewPrefixMapping); }
  88.         }
  89.  
  90.         public override MessageVersion MessageVersion
  91.         {
  92.             get { return this.messageEncoderFactory.MessageVersion; }
  93.         }
  94.  
  95.         public override MessageEncoder CreateSessionEncoder()
  96.         {
  97.             return new ReplacePrefixMessageEncoder(this.messageEncoderFactory.CreateSessionEncoder(), this.namespaceToNewPrefixMapping);
  98.         }
  99.     }
  100.  
  101.     class ReplacePrefixMessageEncoder : MessageEncoder
  102.     {
  103.         private MessageEncoder messageEncoder;
  104.         private Dictionary<string, string> namespaceToNewPrefixMapping;
  105.  
  106.         public ReplacePrefixMessageEncoder(MessageEncoder messageEncoder, Dictionary<string, string> namespaceToNewPrefixMapping)
  107.         {
  108.             this.messageEncoder = messageEncoder;
  109.             this.namespaceToNewPrefixMapping = namespaceToNewPrefixMapping;
  110.         }
  111.  
  112.         public override string ContentType
  113.         {
  114.             get { return this.messageEncoder.ContentType; }
  115.         }
  116.  
  117.         public override string MediaType
  118.         {
  119.             get { return this.messageEncoder.MediaType; }
  120.         }
  121.  
  122.         public override MessageVersion MessageVersion
  123.         {
  124.             get { return this.messageEncoder.MessageVersion; }
  125.         }
  126.  
  127.         public override bool IsContentTypeSupported(string contentType)
  128.         {
  129.             return this.messageEncoder.IsContentTypeSupported(contentType);
  130.         }
  131.  
  132.         public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
  133.         {
  134.             return this.messageEncoder.ReadMessage(buffer, bufferManager, contentType);
  135.         }
  136.  
  137.         public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
  138.         {
  139.             throw new NotSupportedException("Streamed not supported");
  140.         }
  141.  
  142.         public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
  143.         {
  144.             MemoryStream ms = new MemoryStream();
  145.             XmlDictionaryWriter w = XmlDictionaryWriter.CreateBinaryWriter(ms);
  146.             message.WriteMessage(w);
  147.             w.Flush();
  148.             ms.Position = 0;
  149.             XmlDictionaryReader r = XmlDictionaryReader.CreateBinaryReader(ms, XmlDictionaryReaderQuotas.Max);
  150.             XmlDocument doc = new XmlDocument();
  151.             doc.Load(r);
  152.             PrefixReplacer replacer = new PrefixReplacer();
  153.             foreach (var mapping in this.namespaceToNewPrefixMapping)
  154.             {
  155.                 replacer.AddNamespace(mapping.Key, mapping.Value);
  156.             }
  157.  
  158.             replacer.ChangePrefixes(doc);
  159.             ms = new MemoryStream();
  160.             w = XmlDictionaryWriter.CreateBinaryWriter(ms);
  161.             doc.WriteTo(w);
  162.             w.Flush();
  163.             ms.Position = 0;
  164.             r = XmlDictionaryReader.CreateBinaryReader(ms, XmlDictionaryReaderQuotas.Max);
  165.             Message newMessage = Message.CreateMessage(r, maxMessageSize, message.Version);
  166.             newMessage.Properties.CopyProperties(message.Properties);
  167.             return this.messageEncoder.WriteMessage(newMessage, maxMessageSize, bufferManager, messageOffset);
  168.         }
  169.  
  170.         public override void WriteMessage(Message message, Stream stream)
  171.         {
  172.             throw new NotSupportedException("Streamed not supported");
  173.         }
  174.     }
  175. }

An example

This example shows one way to use this new encoding. Notice that to really see the prefixes changed, you’d need to use a network sniffer, such as Fiddler (which is, by the way, one of the most useful tools I use at work).

  1. [ServiceContract(Name = "ITest", Namespace = "https://service.contract.namespace")]
  2. public interface ICalculator
  3. {
  4.     [OperationContract]
  5.     int Add(int x, int y);
  6.  
  7.     [OperationContract]
  8.     int Subtract(int x, int y);
  9.  
  10.     [OperationContract]
  11.     int Multiply(int x, int y);
  12.  
  13.     [OperationContract]
  14.     int Divide(int x, int y);
  15. }
  16.  
  17. public class CalculatorService : ICalculator
  18. {
  19.     public int Add(int x, int y)
  20.     {
  21.         return x + y;
  22.     }
  23.  
  24.     public int Subtract(int x, int y)
  25.     {
  26.         return x - y;
  27.     }
  28.  
  29.     public int Multiply(int x, int y)
  30.     {
  31.         return x * y;
  32.     }
  33.  
  34.     public int Divide(int x, int y)
  35.     {
  36.         return x / y;
  37.     }
  38. }
  39.  
  40. class Program
  41. {
  42.     static void Main(string[] args)
  43.     {
  44.         string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  45.         ServiceHost host = new ServiceHost(typeof(CalculatorService), new Uri(baseAddress));
  46.         Dictionary<string, string> namespaceToPrefixMapping = new Dictionary<string, string>
  47.         {
  48.             { "https://www.w3.org/2003/05/soap-envelope", "SOAP12-ENV" },
  49.             { "https://www.w3.org/2005/08/addressing", "SOAP12-ADDR" },
  50.         };
  51.         Binding binding = ReplacePrefixMessageEncodingBindingElement.ReplaceEncodingBindingElement(
  52.             new WSHttpBinding(SecurityMode.None),
  53.             namespaceToPrefixMapping);
  54.         host.AddServiceEndpoint(typeof(ICalculator), binding, "");
  55.         host.Open();
  56.  
  57.         Binding clientBinding = new WSHttpBinding(SecurityMode.None);
  58.         ChannelFactory<ICalculator> factory = new ChannelFactory<ICalculator>(clientBinding, new EndpointAddress(baseAddress));
  59.         ICalculator proxy = factory.CreateChannel();
  60.  
  61.         Console.WriteLine(proxy.Add(234, 456));
  62.  
  63.         ((IClientChannel)proxy).Close();
  64.         factory.Close();
  65.         host.Close();
  66.     }
  67. }

Notes about message security

Notice that in the example above I set the message security to None in the WSHttpBinding constructor. If you try using a binding which uses message security (such as WSHttpBinding with its default constructor), this prefix changer will not work. The problem is that the default security causes the original message to be signed, and a signature header to be added to the message itself with a (signed) hash of its contents. By changing the prefix of the XML we’d be invalidating the message signature. There are some ways to circumvent this problem, such as implementing the changer as a custom channel which sits on top of the security channel, so that it would change the XML prefixes before the message is signed. Building a custom channel requires a lot more code than a custom encoder, but if you really need it you can definitely do it as shown by the Custom Message Interceptor sample from https://msdn.microsoft.com/en-us/library/ms751495.aspx.