Exposing Custom WCF Headers through WCF Behaviors - Part 3

In part 1, I covered how to create a custom behavior to inject headers into the dynamically created WSDL.  In part 2, I showed how to either promote or write the header data to the BizTalk context. 

What happens if I want different headers for different end points?  What if I don't want to create a custom header component for each end point?  What if I want to set, through configuration, weather I want to promote or write to the context?  What if I want to add a new header item without the need to recompile?

In this part of the series we will look at the ability to create a behavior that exposes the properties through configuration to let you dynamically, per end point, set the header items.  The configuration is not through a configuration file but instead will hook into the end point behavior dialog box that appears in the adapter configuration in BizTalk.

The finished configuration will look like this:

Let's start looking at code. 

First we are going to look at a new class file that will represent the data that we need to set in the configuration section of the dialog box.  The CustomHeader class is where much of the configuration dialog magic happens.  The way you define the properties will define the way they appear in the configuration dialog box.  If you define your property as a boolean or an enum then it will display as a drop down list box.  If you define it as a class then you will get the ellipses. 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Services.WCF.Behavior
{
public class CustomHeader
    {
public string Name { get; set; }

public string Namespace { get; set; }

public bool Required { get; set; }

public ContextAction Action { get; set; }
    }
}

Where the ContextAction type is the enum listed below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Services.WCF.Behavior
{
public enum ContextAction
    {
None = 0,
Write = 1,
Promote = 2
    }
}

The CustomHeader class is accessed and 'bound' to the dialog box through the CustomHeaderEndpointBehavior class.  This class is just a renamed and modified version of the SoapHeaderEndpointBehavior class we saw in the previous two articles.

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Configuration;
using System.Configuration;
using System.ServiceModel;

namespace Services.WCF.Behavior
{
public class CustomHeaderEndpointBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
#region BehaviorExtensionElement Methods

public override Type BehaviorType
        {
get { return typeof(CustomHeaderEndpointBehavior); }
        }

protected override object CreateBehavior()
        {
return new CustomHeaderEndpointBehavior(this.Headers);
        }

//Copies the content of the specified configuration element to this configuration element
public override void CopyFrom(ServiceModelExtensionElement extFrom)
        {
base.CopyFrom(extFrom);
CustomHeaderEndpointBehavior element = extFrom as CustomHeaderEndpointBehavior;
if (element != null)
            {
Headers = element.Headers;
            }
        }

//Both properties are returned as a collection.
protected override ConfigurationPropertyCollection Properties
        {
get
            {
if (_properties == null)
                {
_properties = new ConfigurationPropertyCollection();

                    _properties.Add(new ConfigurationProperty("Headers", typeof(List<CustomHeader>), null,
new SerializationConverter(typeof(List<CustomHeader>)), null, ConfigurationPropertyOptions.None));
base["Headers"] = new List<CustomHeader>();
                }
return _properties;
            }
        }

        #endregion

#region IEndpointBehavior Members

public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {
bindingParameters.Add(this);
        }

public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
        {
//throw new NotImplementedException();
        }

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
        {
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new CustomHeaderMessageInspector());
        }

public void Validate(ServiceEndpoint endpoint)
        {
//throw new NotImplementedException();
        }

        #endregion

#region Class properties
        [ConfigurationProperty("Headers")]
public List<CustomHeader> Headers
        {
get
            {
return (List<CustomHeader>)base["Headers"];
            }
set
            {
base["Headers"] = value;
            }
        }

        #endregion

#region Class Fields
private ConfigurationPropertyCollection _properties;
        #endregion

#region Constructors
public CustomHeaderEndpointBehavior(List<CustomHeader> headers)
        {
this.Headers = headers;
        }
public CustomHeaderEndpointBehavior() : base()
{}

        #endregion
    }
}

First you will notice that there is a number of new methods.  The first is the CopyFrom method.  This method is needed to copy the contents of the configuration data entered in the dialog box so that we can gain access to it within our class.  We then override ConfigurationPropertyCollection since the header properties are returned as a collection.  We will add the properties to our internal collection based on the List<CustomHeader> object.  We also add the class to the AddBindingParamters method.

One thing to note when creating the configuration class (in our case the CustomHeader class) is that the default behavior of the Transport Properties dialog box and underlying code expects that all configuration information will be of type string.  If you are using other types then you need to create your own type converter to convert to a string representation and back.  I have the code for the type converter at the bottom of this post called SerializationConverter.

In the code above, in the CustomHeaderEndpointBehavior class, there were a couple of custom objects.  The first one we will dig into is in the ApplyDispatchBehavior where we add a new CustomHeaderMessageInspector. 

The class implementation for the CustomHeaderMessageInspector looks like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Xml;

namespace Services.WCF.Behavior
{
class CustomHeaderMessageInspector : IDispatchMessageInspector
    {
#region IDispatchMessageInspector Members

public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
        {
List<KeyValuePair<XmlQualifiedName, object>> writeProps = new List<KeyValuePair<XmlQualifiedName, object>>();
List<KeyValuePair<XmlQualifiedName, object>> promoteProps = new List<KeyValuePair<XmlQualifiedName, object>>();
string writeKey = "https://schemas.microsoft.com/BizTalk/2006/01/Adapters/WCF-properties/WriteToContext";
string promoteKey = "https://schemas.microsoft.com/BizTalk/2006/01/Adapters/WCF-properties/Promote";

CustomHeaderEndpointBehavior bhv = null;

if (instanceContext.Host.Description.Endpoints.Find(channel.LocalAddress.Uri) != null)
bhv = instanceContext.Host.Description.Endpoints.Find(channel.LocalAddress.Uri).Behaviors.Find<CustomHeaderEndpointBehavior>();

if (bhv != null)
            {
foreach (CustomHeader hdr in bhv.Headers)
                {
int headerPos = OperationContext.Current.IncomingMessageHeaders.FindHeader(hdr.Name, hdr.Namespace);

if (headerPos < 0)
                    {
if (hdr.Required)
                        {
//Fault Condition
throw new ArgumentNullException(hdr.Name, "Required soap header not found.");
                        }
                    }
else
                    {
if (hdr.Action != ContextAction.None)
                        {
// Get an XmlDictionaryReader to read the header content
XmlDictionaryReader reader = OperationContext.Current.IncomingMessageHeaders.GetReaderAtHeader(headerPos);
XmlDocument d = new XmlDocument();

d.LoadXml(reader.ReadOuterXml());
XmlQualifiedName PropName1 = new XmlQualifiedName(hdr.Name, hdr.Namespace);

if (hdr.Action == ContextAction.Write)
                            {
writeProps.Add(new KeyValuePair<XmlQualifiedName, object>(PropName1, d.DocumentElement.InnerText));
                            }
else if (hdr.Action == ContextAction.Promote)
                            {
promoteProps.Add(new KeyValuePair<XmlQualifiedName, object>(PropName1, d.DocumentElement.InnerText));
                            }
                        }
                    }
                }
            }
else
            {
//Debug.WriteLine("*****AfterReceiveRequest: No Behavior found of type CustomHeaderEndpointBehavior.*****");
            }

if (writeProps.Count > 0)
            {
request.Properties[writeKey] = writeProps;
            }

if (promoteProps.Count > 0)
            {
request.Properties[promoteKey] = promoteProps;
            }

return null;
        }

public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
        }

        #endregion
    }
}

As we look at this code, in the AfterReceiveRequest method we have included both the writeProps and the promoteProps variables as well as both namespaces.  We then look in the instanceContext.Host.Description.Endpoints collection to find, within the behaviors collection, our CustomerHeaderEndPointBehavior object.  Then we loop through each of the headers that was setup through configuration and look for them in the message headers collection.  Finally, check if the header was required and if we need to write or promote the values.

Lastly, here is the code that does our type conversion from the CustomHeader class to a string representation (in our case this will be XML).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Xml.Serialization;
using System.IO;
using System.Xml;
using System.Globalization;

namespace Services.WCF.Behavior
{
public class SerializationConverter : TypeConverter
    {
private Type _type;
public SerializationConverter(Type type)
        {
_type = type;
        }

#region Helper Utilities

private object Deserialize(object value)
        {
if (_type == null) throw new ArgumentNullException("", "Serialization type was not set. Use the SerializationConvertor(Type) constructor.");
return Deserialize(value, _type);
        }

private object Deserialize(object value, Type destinationType)
        {
StringReader strRdr = null;
XmlReader xmlRdr = null;

try
            {
if (!(value is string))
throw new ApplicationException("Expecting parameter 'value' to be of type string. 'Value' is of type " + value.GetType().ToString());

XmlSerializer serializer = new XmlSerializer(destinationType);
strRdr = new StringReader(value.ToString());
xmlRdr = XmlReader.Create(strRdr);

return serializer.Deserialize(xmlRdr);
            }
finally
            {
strRdr.Close();
xmlRdr.Close();
            }
        }

private object Serialize(object value)
        {
StringWriter wtr = null;

try
            {
if (value == null)
throw new ArgumentNullException("value");

StringBuilder sb = new StringBuilder();
XmlSerializer serializer = new XmlSerializer(value.GetType());
wtr = new StringWriter(sb);
serializer.Serialize(wtr, value);

return sb.ToString();
            }
finally
            {
wtr.Close();
            }
        }

        #endregion

#region Collection code.
//Class to string
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
if (value is string)
            {
return Deserialize(value);
            }
else if (value.GetType() == _type)
            {
return Serialize(value);
            }
else
            {
string msg = (_type == null ? "Value is not of type System.string and no type was specified at construction." : "Value is not of type System.string or " + _type.ToString() + ".");
throw new ApplicationException(msg);
            }
        }

//String to class
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
if (value is string)
            {
return Deserialize(value, destinationType);
            }
else if (value.GetType() == _type)
            {
return Serialize(value);
            }
else
            {
string msg = (_type == null ? "Value is not of type System.string and no type was specified at construction." : "Value is not of type System.string or " + _type.ToString() + ".");
throw new ApplicationException(msg);
            }
        }
        #endregion
    }
}

At this point we have all the code that is part of our project.  Once this is compiled, we need to add the assembly to the machine config and add it to the BizTalk WCF endpoint.  The process to implement this remains the same as that described at the bottom of part 1 of this series.

As always, the code in this post and this series are for reference only and are provided as is.  Now that we have that out of the way, I hope that these posts have been helpful and have shown how to deal with header values in the WCF stack and  shown the ability to create a behavior that exposes the properties through configuration to let you dynamically, per end point, set the header items.