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.
- public class PrefixReplacer
- {
- const string XmlnsNamespace = "https://www.w3.org/2000/xmlns/";
- Dictionary<string, string> namespaceToNewPrefixMapping = new Dictionary<string, string>();
- public void AddNamespace(string namespaceUri, string newPrefix)
- {
- this.namespaceToNewPrefixMapping.Add(namespaceUri, newPrefix);
- }
- public void ChangePrefixes(XmlDocument doc)
- {
- XmlElement element = doc.DocumentElement;
- XmlElement newElement = ChangePrefixes(doc, element);
- doc.LoadXml(newElement.OuterXml);
- }
- private XmlElement ChangePrefixes(XmlDocument doc, XmlElement element)
- {
- string newPrefix;
- if (this.namespaceToNewPrefixMapping.TryGetValue(element.NamespaceURI, out newPrefix))
- {
- XmlElement newElement = doc.CreateElement(newPrefix, element.LocalName, element.NamespaceURI);
- List<XmlNode> children = new List<XmlNode>(element.ChildNodes.Cast<XmlNode>());
- List<XmlAttribute> attributes = new List<XmlAttribute>(element.Attributes.Cast<XmlAttribute>());
- foreach (XmlNode child in children)
- {
- newElement.AppendChild(child);
- }
- foreach (XmlAttribute attr in attributes)
- {
- newElement.Attributes.Append(attr);
- }
- element = newElement;
- }
- List<XmlAttribute> newAttributes = new List<XmlAttribute>();
- bool modified = false;
- for (int i = 0; i < element.Attributes.Count; i++)
- {
- XmlAttribute attr = element.Attributes[i];
- if (this.namespaceToNewPrefixMapping.TryGetValue(attr.NamespaceURI, out newPrefix))
- {
- XmlAttribute newAttr = doc.CreateAttribute(newPrefix, attr.LocalName, attr.NamespaceURI);
- newAttr.Value = attr.Value;
- newAttributes.Add(newAttr);
- modified = true;
- }
- else if (attr.NamespaceURI == XmlnsNamespace && this.namespaceToNewPrefixMapping.TryGetValue(attr.Value, out newPrefix))
- {
- XmlAttribute newAttr;
- if (newPrefix != "")
- {
- newAttr = doc.CreateAttribute("xmlns", newPrefix, XmlnsNamespace);
- }
- else
- {
- newAttr = doc.CreateAttribute("xmlns");
- }
- newAttr.Value = attr.Value;
- newAttributes.Add(newAttr);
- modified = true;
- }
- else
- {
- newAttributes.Add(attr);
- }
- }
- if (modified)
- {
- element.Attributes.RemoveAll();
- foreach (var attr in newAttributes)
- {
- element.Attributes.Append(attr);
- }
- }
- List<KeyValuePair<XmlNode, XmlNode>> toReplace = new List<KeyValuePair<XmlNode, XmlNode>>();
- foreach (XmlNode child in element.ChildNodes)
- {
- XmlElement childElement = child as XmlElement;
- if (childElement != null)
- {
- XmlElement newChildElement = ChangePrefixes(doc, childElement);
- if (newChildElement != childElement)
- {
- toReplace.Add(new KeyValuePair<XmlNode, XmlNode>(childElement, newChildElement));
- }
- }
- }
- if (toReplace.Count > 0)
- {
- for (int i = 0; i < toReplace.Count; i++)
- {
- element.InsertAfter(toReplace[i].Value, toReplace[i].Key);
- element.RemoveChild(toReplace[i].Key);
- }
- }
- return element;
- }
- }
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.
- public class ReplacePrefixMessageEncodingBindingElement : MessageEncodingBindingElement
- {
- MessageEncodingBindingElement inner;
- Dictionary<string, string> namespaceToPrefixMapping = new Dictionary<string, string>();
- public ReplacePrefixMessageEncodingBindingElement(MessageEncodingBindingElement inner)
- {
- this.inner = inner;
- }
- private ReplacePrefixMessageEncodingBindingElement(ReplacePrefixMessageEncodingBindingElement other)
- {
- this.inner = other.inner;
- this.namespaceToPrefixMapping = new Dictionary<string, string>(other.namespaceToPrefixMapping);
- }
- public void AddNamespaceMapping(string namespaceUri, string newPrefix)
- {
- this.namespaceToPrefixMapping.Add(namespaceUri, newPrefix);
- }
- public override MessageEncoderFactory CreateMessageEncoderFactory()
- {
- return new ReplacePrefixMessageEncoderFactory(this.inner.CreateMessageEncoderFactory(), this.namespaceToPrefixMapping);
- }
- public override MessageVersion MessageVersion
- {
- get { return this.inner.MessageVersion; }
- set { this.inner.MessageVersion = value; }
- }
- public override BindingElement Clone()
- {
- return new ReplacePrefixMessageEncodingBindingElement(this);
- }
- public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
- {
- context.BindingParameters.Add(this);
- return context.BuildInnerChannelListener<TChannel>();
- }
- public override bool CanBuildChannelListener<TChannel>(BindingContext context)
- {
- return context.CanBuildInnerChannelListener<TChannel>();
- }
- public static CustomBinding ReplaceEncodingBindingElement(Binding originalBinding, Dictionary<string, string> namespaceToPrefixMapping)
- {
- CustomBinding custom = originalBinding as CustomBinding;
- if (custom == null)
- {
- custom = new CustomBinding(originalBinding);
- }
- for (int i = 0; i < custom.Elements.Count; i++)
- {
- if (custom.Elements[i] is MessageEncodingBindingElement)
- {
- ReplacePrefixMessageEncodingBindingElement element = new ReplacePrefixMessageEncodingBindingElement((MessageEncodingBindingElement)custom.Elements[i]);
- foreach (var mapping in namespaceToPrefixMapping)
- {
- element.AddNamespaceMapping(mapping.Key, mapping.Value);
- }
- custom.Elements[i] = element;
- break;
- }
- }
- return custom;
- }
- class ReplacePrefixMessageEncoderFactory : MessageEncoderFactory
- {
- private MessageEncoderFactory messageEncoderFactory;
- private Dictionary<string, string> namespaceToNewPrefixMapping;
- public ReplacePrefixMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory, Dictionary<string, string> namespaceToNewPrefixMapping)
- {
- this.messageEncoderFactory = messageEncoderFactory;
- this.namespaceToNewPrefixMapping = namespaceToNewPrefixMapping;
- }
- public override MessageEncoder Encoder
- {
- get { return new ReplacePrefixMessageEncoder(this.messageEncoderFactory.Encoder, this.namespaceToNewPrefixMapping); }
- }
- public override MessageVersion MessageVersion
- {
- get { return this.messageEncoderFactory.MessageVersion; }
- }
- public override MessageEncoder CreateSessionEncoder()
- {
- return new ReplacePrefixMessageEncoder(this.messageEncoderFactory.CreateSessionEncoder(), this.namespaceToNewPrefixMapping);
- }
- }
- class ReplacePrefixMessageEncoder : MessageEncoder
- {
- private MessageEncoder messageEncoder;
- private Dictionary<string, string> namespaceToNewPrefixMapping;
- public ReplacePrefixMessageEncoder(MessageEncoder messageEncoder, Dictionary<string, string> namespaceToNewPrefixMapping)
- {
- this.messageEncoder = messageEncoder;
- this.namespaceToNewPrefixMapping = namespaceToNewPrefixMapping;
- }
- public override string ContentType
- {
- get { return this.messageEncoder.ContentType; }
- }
- public override string MediaType
- {
- get { return this.messageEncoder.MediaType; }
- }
- public override MessageVersion MessageVersion
- {
- get { return this.messageEncoder.MessageVersion; }
- }
- public override bool IsContentTypeSupported(string contentType)
- {
- return this.messageEncoder.IsContentTypeSupported(contentType);
- }
- public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
- {
- return this.messageEncoder.ReadMessage(buffer, bufferManager, contentType);
- }
- public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
- {
- throw new NotSupportedException("Streamed not supported");
- }
- public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
- {
- MemoryStream ms = new MemoryStream();
- XmlDictionaryWriter w = XmlDictionaryWriter.CreateBinaryWriter(ms);
- message.WriteMessage(w);
- w.Flush();
- ms.Position = 0;
- XmlDictionaryReader r = XmlDictionaryReader.CreateBinaryReader(ms, XmlDictionaryReaderQuotas.Max);
- XmlDocument doc = new XmlDocument();
- doc.Load(r);
- PrefixReplacer replacer = new PrefixReplacer();
- foreach (var mapping in this.namespaceToNewPrefixMapping)
- {
- replacer.AddNamespace(mapping.Key, mapping.Value);
- }
- replacer.ChangePrefixes(doc);
- ms = new MemoryStream();
- w = XmlDictionaryWriter.CreateBinaryWriter(ms);
- doc.WriteTo(w);
- w.Flush();
- ms.Position = 0;
- r = XmlDictionaryReader.CreateBinaryReader(ms, XmlDictionaryReaderQuotas.Max);
- Message newMessage = Message.CreateMessage(r, maxMessageSize, message.Version);
- newMessage.Properties.CopyProperties(message.Properties);
- return this.messageEncoder.WriteMessage(newMessage, maxMessageSize, bufferManager, messageOffset);
- }
- public override void WriteMessage(Message message, Stream stream)
- {
- throw new NotSupportedException("Streamed not supported");
- }
- }
- }
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).
- [ServiceContract(Name = "ITest", Namespace = "https://service.contract.namespace")]
- public interface ICalculator
- {
- [OperationContract]
- int Add(int x, int y);
- [OperationContract]
- int Subtract(int x, int y);
- [OperationContract]
- int Multiply(int x, int y);
- [OperationContract]
- int Divide(int x, int y);
- }
- public class CalculatorService : ICalculator
- {
- public int Add(int x, int y)
- {
- return x + y;
- }
- public int Subtract(int x, int y)
- {
- return x - y;
- }
- public int Multiply(int x, int y)
- {
- return x * y;
- }
- public int Divide(int x, int y)
- {
- return x / y;
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
- ServiceHost host = new ServiceHost(typeof(CalculatorService), new Uri(baseAddress));
- Dictionary<string, string> namespaceToPrefixMapping = new Dictionary<string, string>
- {
- { "https://www.w3.org/2003/05/soap-envelope", "SOAP12-ENV" },
- { "https://www.w3.org/2005/08/addressing", "SOAP12-ADDR" },
- };
- Binding binding = ReplacePrefixMessageEncodingBindingElement.ReplaceEncodingBindingElement(
- new WSHttpBinding(SecurityMode.None),
- namespaceToPrefixMapping);
- host.AddServiceEndpoint(typeof(ICalculator), binding, "");
- host.Open();
- Binding clientBinding = new WSHttpBinding(SecurityMode.None);
- ChannelFactory<ICalculator> factory = new ChannelFactory<ICalculator>(clientBinding, new EndpointAddress(baseAddress));
- ICalculator proxy = factory.CreateChannel();
- Console.WriteLine(proxy.Add(234, 456));
- ((IClientChannel)proxy).Close();
- factory.Close();
- host.Close();
- }
- }
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.
Comments
- Anonymous
July 20, 2011
Hi sir,I try to use your code but I have a problem. I would like to use Basic http Binding instead of WqHttpBinding, and when i try to do that there is an exeption throwed :"The message version of the outgoing message (Soap11 (schemas.xmlsoap.org/.../envelope) AddressingNone <br/>(schemas.microsoft.com/.../none)) does not match that of the encoder <br/><br/>(Soap12 (ht...)Can you help me ?thx - Anonymous
February 02, 2012
Hi Sylvain. I downloaded the code from the MSDN Gallery (code.msdn.microsoft.com/Replacing-XML-prefixes-in-a417c6f2), replaced the parts in the Program.cs which said "new WSHttpBinding(SecurityMode.None)" with "new BasicHttpBinding()" and it ran fine - no namespaces were changed, though, since the namespace for SOAP 1.1 (used in Basic) is different. So if you also change the namespace to prefix mapping in the program.cs to the code below, you should see it working fine. Dictionary<string, string> namespaceToPrefixMapping = new Dictionary<string, string> { { "schemas.xmlsoap.org/.../", "SOAP11-ENV" }, }; - Anonymous
December 19, 2012
Hi Carlos,This is exactly what I am looking for due to the client being Java. How can I achieve the same using web.config. I do not have self hosted wcf. what should be my web.config setting to add custom binding extension? Do I need any extra class then what you have mentioned here?Thanks, - Anonymous
December 19, 2012
Hi Dhaval,If you define your endpoint via configuration, you can either use a binding element configuration extension for the custom encoder, or you can start defining the endpoint via code using a custom service host factory. For the former, you can find more information at blogs.msdn.com/.../wcf-extensibility-binding-and-binding-element-configuration-extensions.aspx, and for the latter at blogs.msdn.com/.../wcf-extensibility-servicehostfactory.aspx. - Anonymous
April 25, 2013
The comment has been removed - Anonymous
April 27, 2013
Hi Francisco, the code linked in this post should help you do that; just feed the namespace replacer with the appropriate namespaces and prefixes you want to use. - Anonymous
January 17, 2014
I think the custom encoder is a bit too late in the wcf pipeline. Message security won't work and also the MessageInspectors will work with the wrong message (not with the one that is received by the client at the same level).I used a MessageFormatter to alter the message envelope, namespaces and prefixes:vanacosmin.ro/.../WCFEnvelopeNamespacePrefix