WSE 2.0 and SetHeaderObject: De-funking code

A customer asked me about how you might make it easier on yourself to set the object for a header in WSE.  There is a very simple way to set the body of a SoapEnvelope using SetBodyObject, so why is there not a SetHeaderObject method?  I decided it would be pretty easy to implement the method and send it as a sample of how this could be achieved.

The SoapEnvelope class derives from XmlDocument.  Falling back on old tricks, my first thought was to use Chris Lovett's XmlNodeWriter to write to the SoapEnvelope.  Hmm... wait a minute, that's a code smell.  WSE doesn't have a reference to XmlNodeWriter, this seems like overkill.  I pulled out Reflector and found the SetBodyObject method.  Pretty easy, actually:  it just calls the XmlSerializerCache to serialize the type.  That confirms that there's a better design for this.

My next thought was to derive from SoapEnvelope and add my new methods to this type.  Easy enough to write, and the code would be pretty easy to add.  The problem is that it would cause the consumer of the code to pass my derived SoapEnvelope type everywhere instead of SoapEnvelope, and that is less than desirable.  Another code smell.

The final design choice was to write to use a simplified Visitor class to add the methods to a SoapEnvelope.  This relieves the requirement of adding a reference to another library, and relieves the amount that a consumer of the code needs to pass around a custom type.  Using this type, it is extremely simple to set the content of a SOAP header using WSE.

   SoapEnvelope env = new SoapEnvelope();

Contoso.Web.Services.Protocols.ContosoInteropHeader header = new Contoso.Web.Services.Protocols.ContosoInteropHeader();
header.CallingService = "urn:com-services-Contoso:Support";
header.ID = 5;

   Contoso.Web.Services.SoapEnvelopeVisitor vis = new SoapEnvelopeVisitor(env);
vis.SetHeaderObject(header,"urn:com-services-Contoso:Interop");

There is one largely obvious caveat to this approach.  Notice below that the header is completely overwritten when you call SetHeaderObject.  This means that any existing headers are blown away.  The above code is pretty safe, though, since the WS-Addressing and WS-Security headers are not added to the SoapEnvelope until the message is serialized. However, be aware that there are circumstances where there may be existing headers, and calling this method would wipe those headers away.  But, it can be very useful for simple Request / Response interactions with no intermediary. 

using System;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Web.Services2;
using Microsoft.Web.Services2.Xml;
using System.Reflection;

namespace Contoso.Web.Services
{

 public class SoapEnvelopeVisitor
{
private SoapEnvelope _envelope;

  public SoapEnvelopeVisitor(SoapEnvelope envelope)
{
_envelope = envelope;
}

  public void SetHeaderObject(Object headerObject)
{
SetHeaderObject(headerObject, "https://tempuri.org/");
}

  public void SetHeaderObject(Object headerObject, string defaultNamespace)
{
if (headerObject == null)
{
throw new ArgumentNullException("headerObject");
}
if (defaultNamespace == null)
{
throw new ArgumentNullException("defaultNamespace");
}
XmlElement header = _envelope.CreateHeader();
SoapContext context = SoapContext.Current;

SoapContext.Current = _envelope.Context;
try
{
if (headerObject is XmlElement)
{
header.RemoveAll();
header.AppendChild(_envelope.ImportNode((XmlNode) headerObject, true));
}
else if (headerObject is IXmlElement)
{
header.RemoveAll();
header.AppendChild(((IXmlElement) headerObject).GetXml(_envelope));
}
else
{
MemoryStream mem = new MemoryStream();
XmlTextWriter writer = new BareXmlTextWriter(mem, Encoding.UTF8);
XmlSerializerCache.GetXmlSerializer(headerObject.GetType(), defaultNamespace).Serialize(writer, headerObject);
mem.Position = 0;
StreamReader reader = new StreamReader(mem);
header.InnerXml = reader.ReadToEnd();
reader.Close();
writer.Close();
}
}
finally
{
SoapContext.Current = context;
}

  }

  public object GetHeaderObject(Type headerType)
{
return this.GetHeaderObject(headerType, "https://tempuri.org/");
}
 

  public object GetHeaderObject(Type headerType, string defaultNamespace)
{
if (headerType == null)
{
throw new ArgumentNullException("headerType");
}
if (_envelope.Body == null)
{
return null;
}
if (_envelope.Body.ChildNodes.Count == 0)
{
return null;
}
if (defaultNamespace == null)
{
throw new ArgumentNullException("defaultNamespace");
}
XmlElement headerObject = null;
for (int i = 0; (i < _envelope.Header.ChildNodes.Count) && (headerObject == null); i++)
{
headerObject = _envelope.Header.ChildNodes[i] as XmlElement;
}
object obj1 = null;
SoapContext context1 = SoapContext.Current;
SoapContext.Current = _envelope.Context;
try
{
if (headerType.Equals(typeof(XmlElement)))
{
return headerObject;
}
if (Utility.ImplementsInterface(headerType, typeof(IXmlElement)))
{
object[] objArray1;
ConstructorInfo info1 = XmlSerializerCache.GetXmlElementConstructor(headerType);
if (info1 != null)
{
objArray1 = new object[1] { headerObject } ;
return info1.Invoke(objArray1);
}
objArray1 = new object[1] { headerType.FullName } ;
throw new InvalidOperationException(string.Format("No suitable constructor could be found to deserialize type {0}.",objArray1));

}
obj1 = XmlSerializerCache.GetXmlSerializer(headerType, defaultNamespace).Deserialize(new XmlNodeReader(headerObject));
}
finally
{
SoapContext.Current = context1;
}
return obj1;
}
}

 internal class Utility
{
public static bool ImplementsInterface(Type type, Type interfaceType)
{
Type[] typeArray1 = type.GetInterfaces();
for (int num1 = 0; num1 < typeArray1.Length; num1++)
{
if (typeArray1[num1].IsAssignableFrom(interfaceType))
{
return true;
}
}
return false;
}
}
}