Customizing the location of XMLNS attributes
A question I’ve seen in the forums and heard in person a few too many times over the past few weeks has been “how do I control the location of XML namespace declarations in my emitted XML?” In other words, folks want to control the Text encoding of a given bit of XML as it goes elsewhere. The same folks aren’t too concerned over the reading side. With that in mind, I decided to see what happens when one takes over for serialization. It’s surprisingly easy to control the ultimate format if you apply your efforts in the right places. My goal here was to simply emit XML namespaces into the root of the envelope and then have a Message serialized the right way. By doing this, I’m limiting myself to text message encoding only—no MTOM or Binary for me here! I’ve also locked down on UTF8 encoding for outgoing messages.
In this post, I’m assuming that you already know how to create a BindingElement and a MessageEncodingBindingElement. The secret sauce is in the MessageEncoder anyhow. What I did to control the serialization a bit more was to emit XML namespace declarations when writing out the SOAP Envelope, then let the Headers and Body do what they will. What I did was wrap a TextMessageEncodingBindingElement with my own type. I let TextMessageEncodingBindingElement handle the responsibilities with reading messages in. My encoder handles writing messages out.
Here’s the EncodingBindingElement:
class MyEncoderBindingElement : MessageEncodingBindingElement
{
private readonly MessageEncodingBindingElement _innerEncoding;
MyEncoderBindingElement(MyEncoderBindingElement clone)
{
_innerEncoding = (MessageEncodingBindingElement)clone._innerEncoding.Clone();
}
public MyEncoderBindingElement ()
{
_innerEncoding = new TextMessageEncodingBindingElement();
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new MyEncoderFactory(_innerEncoding.CreateMessageEncoderFactory());
}
public override MessageVersion MessageVersion
{
get { return _innerEncoding.MessageVersion; }
set { _innerEncoding.MessageVersion = value; }
}
public override BindingElement Clone()
{
return new MyEncoderBindingElement(this);
}
public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
{
return _innerEncoding.CanBuildChannelFactory<TChannel>(context);
}
public override bool CanBuildChannelListener<TChannel>(BindingContext context)
{
return _innerEncoding.CanBuildChannelListener<TChannel>(context);
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
context.BindingParameters.Add(this);
return context.BuildInnerChannelFactory<TChannel>();
}
public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
context.BindingParameters.Add(this);
return context.BuildInnerChannelListener<TChannel>();
}
class MyEncoderFactory : MessageEncoderFactory
{
private readonly MessageEncoderFactory _innerFactory;
MessageEncoder _innerEncoder = null;
public MyEncoderFactory(MessageEncoderFactory innerFactory)
{
_innerFactory = innerFactory;
}
public override MessageEncoder Encoder
{
get
{
if (_innerEncoder == null)
{
_innerEncoder = new MyMessageEncoder(_innerFactory.Encoder);
}
return _innerEncoder;
}
}
public override MessageVersion MessageVersion
{
get { return _innerFactory.MessageVersion; }
}
}
private class MyMessageEncoder : MessageEncoder
{
readonly MessageEncoder _inner;
public MyMessageEncoder(MessageEncoder encoder)
{
_inner = encoder;
}
public override string ContentType
{
get { return _inner.ContentType; }
}
public override string MediaType
{
get { return _inner.MediaType; }
}
public override MessageVersion MessageVersion
{
get { return _inner.MessageVersion; }
}
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
return _inner.ReadMessage(buffer, bufferManager, contentType);
}
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
{
return _inner.ReadMessage(stream, maxSizeOfHeaders, contentType);
}
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
MemoryStream ms = new MemoryStream();
WriteMessage(message, ms);
ArraySegment<byte> retval = new ArraySegment<byte>(bufferManager.TakeBuffer((int)ms.Length));
for (long i = 0; i < ms.Length; ++i)
{
retval.Array
= ms.GetBuffer()
;
}
return retval;
}
string EnvelopeVersion(MessageVersion version)
{
string retval = string.Empty;
if (version.Envelope == System.ServiceModel.EnvelopeVersion.Soap11)
{
retval = "http://schemas.xmlsoap.org/soap/envelope/";
}
else if (version.Envelope== System.ServiceModel.EnvelopeVersion.Soap12)
{
retval = "http://www.w3.org/2003/05/soap-envelope";
}
return retval;
}
public override void WriteMessage(Message message, Stream stream)
{
XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(stream, Encoding.UTF8);
writer.WriteStartElement("soap", "Envelope", EnvelopeVersion(message.Version));
writer.WriteAttributeString("xmlns", "scott", "http://www.w3.org/2000/xmlns/", "http://www.scottseely.com");
writer.WriteAttributeString("xmlns", "addressing", "http://www.w3.org/2000/xmlns/",
"http://www.w3.org/2005/08/addressing");
{
if (message.Headers.Count > 0)
{
writer.WriteStartElement("soap", "Header", EnvelopeVersion(message.Version));
foreach (MessageHeader header in message.Headers)
{
header.WriteHeader(writer, message.Version);
}
writer.WriteEndElement();
}
writer.WriteStartElement("soap", "Body", EnvelopeVersion(message.Version));
{
message.WriteBodyContents(writer);
}
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.Flush();
}
}
}
And, here is some test code to exercise the binding:
MyEncoderBindingElement be = new MyEncoderBindingElement();
MessageEncoderFactory fact = be.CreateMessageEncoderFactory();
MemoryStream ms = new MemoryStream();
fact.Encoder.WriteMessage(Message.CreateMessage(
MessageVersion.Soap11WSAddressing10, "urn:someAction", new MyData()),
ms);
Console.WriteLine(Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length));
Output is then:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:scott="http://www.scottseely.com"
xmlns:addressing="http://www.w3.org/2005/08/addressing">
<soap:Header>
<addressing:Action soap:mustUnderstand="1">urn:someAction</addressing:Action>
</soap:Header>
<soap:Body>
<scott:MyData xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<scott:MyName>Scott</scott:MyName>
</scott:MyData>
</soap:Body>
</soap:Envelope>
If you need more namespaces, you can either hard code or add some stuff to config or elsewhere.