Handling custom SOAP headers via WCF Behaviors
A few days ago a customer of mine asked me how to define a WCF behavior to add a custom SOAP Header to sent/received messages.
The solution is not so far from what I've shown in the previous "Writing a WCF Message Inspector" post. In fact one way of working is to define a custom message inspector that writes/reads the custom SOAP Header.
So first of all we need a SOAP Header. Here is the code to define a custom header to handle a random key (as a Guid) injected in every request sent from the consumer to the service:
public class CustomHeader : MessageHeader
{
private String _key;
public String Key
{
get
{
return (this._key);
}
}
public CustomHeader(String key)
{
this._key = key;
}
public override string Name
{
get { return (CustomHeaderNames.CustomHeaderName); }
}
public override string Namespace
{
get { return (CustomHeaderNames.CustomHeaderNamespace); }
}
protected override void OnWriteHeaderContents(System.Xml.XmlDictionaryWriter writer, MessageVersion messageVersion)
{
// Write the content of the header directly using the XmlDictionaryWriter
writer.WriteElementString(CustomHeaderNames.KeyName, this.Key);
}
public static CustomHeader ReadHeader(XmlDictionaryReader reader)
{
// Read the header content (key) using the XmlDictionaryReader
if (reader.ReadToDescendant(CustomHeaderNames.KeyName, CustomHeaderNames.CustomHeaderNamespace))
{
String key = reader.ReadElementString();
return (new CustomHeader(key));
}
else
{
return null;
}
}
}
public static class CustomHeaderNames
{
public const String CustomHeaderName = "CustomHeader";
public const String KeyName = "Key";
public const String CustomHeaderNamespace = "http://schemas.devleap.com/CustomHeader";
}
As you can see it is a type inheriting from MessageHeader class. Notice the OnWriteHeaderContents override, which is invoked by WCF infrastructure to serialize the SOAP Header, and the ReadHeader static method that we will use later.
Such a SOAP Header need to be added by the consumer and read by the service. To do this we need a MessageInspector like the following one:
public class CustomMessageInspector : IDispatchMessageInspector, IClientMessageInspector
{
#region Message Inspector of the Service
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
// Look for my custom header in the request
Int32 headerPosition = request.Headers.FindHeader(CustomHeaderNames.CustomHeaderName, CustomHeaderNames.CustomHeaderNamespace);
// Get an XmlDictionaryReader to read the header content
XmlDictionaryReader reader = request.Headers.GetReaderAtHeader(headerPosition);
// Read it through its static method ReadHeader
CustomHeader header = CustomHeader.ReadHeader(reader);
// Add the content of the header to the IncomingMessageProperties dictionary
OperationContext.Current.IncomingMessageProperties.Add("key", header.Key);
return null;
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
}
#endregion
#region Message Inspector of the Consumer
public void AfterReceiveReply(ref Message reply, object correlationState)
{
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
// Prepare the request message copy to be modified
MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
request = buffer.CreateMessage();
// Simulate to have a random Key generation process
request.Headers.Add(new CustomHeader(Guid.NewGuid().ToString()));
return null;
}
#endregion
}
As you can see from the code sample above, we use the IClientMessageInspector implementation to handle the addition of the header in the consumer-side code, while we use the IDispatchMessageInspector on the service side, to extract the header. It is interesting the FindHeader method of the MessageHeaders collection, as well as the method GetReaderAtHeader, provided by the same collection of SOAP Headers. The result of this last method is an XmlDictionaryReader that we use to read our custom header content, through the ReadHeader static method we've already introduced.
The service will be able to read the Key provided throught the custom SOAP header simply querying the IncomingMessageProperties dictionary:
OperationContext.Current.IncomingMessageProperties["key"]
Of course this custom MessageInspector needs to be plugged into the WCF pipeline using a custom behavior like the following one:
[AttributeUsage(AttributeTargets.Class)]
public class CustomBehavior : Attribute, IEndpointBehavior
{
#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
CustomMessageInspector inspector = new CustomMessageInspector();
clientRuntime.MessageInspectors.Add(inspector);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
ChannelDispatcher channelDispatcher = endpointDispatcher.ChannelDispatcher;
if (channelDispatcher != null)
{
foreach (EndpointDispatcher ed in channelDispatcher.Endpoints)
{
CustomMessageInspector inspector = new CustomMessageInspector();
ed.DispatchRuntime.MessageInspectors.Add(inspector);
}
}
}
public void Validate(ServiceEndpoint endpoint)
{
}
#endregion
}
We also need an ExtensionElement to configure the behavior:
public class CustomBehaviorExtensionElement : BehaviorExtensionElement
{
protected override object CreateBehavior()
{
return new CustomBehavior();
}
public override Type BehaviorType
{
get
{
return typeof(CustomBehavior);
}
}
}
At last we can configure in the .config file of our service and consumer the behavior. Here is the service side configuration:
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="customBehavior" type="DevLeap.WCF.Behaviors.Extensions.CustomBehaviorExtensionElement, DevLeap.WCF.Behaviors.Extensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<services>
<service name="DevLeap.WCF.Behaviors.Services.ServiceOne" behaviorConfiguration="serviceBehavior">
<endpoint
address="net.tcp://localhost:35001/ServiceOne/"
binding="netTcpBinding"
contract="DevLeap.WCF.Behaviors.Contracts.IServiceOne"
behaviorConfiguration="endpointBehavior" />
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="endpointBehavior">
<customBehavior />
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
And here is the configuration of the consumer-side:
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="customBehavior" type="DevLeap.WCF.Behaviors.Extensions.CustomBehaviorExtensionElement, DevLeap.WCF.Behaviors.Extensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<client>
<endpoint name="serviceOneEndpoint"
address="net.tcp://localhost:35001/ServiceOne/"
binding="netTcpBinding"
contract="DevLeap.WCF.Behaviors.Contracts.IServiceOne"
behaviorConfiguration="serviceOneBehavior" />
</client>
<behaviors>
<endpointBehaviors>
<behavior name="serviceOneBehavior">
<customBehavior />
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
That's all! Enjoy your custom SOAP header defining infrastructural protocols, but don't forget to check the wide range of WS-* specifications before inventing your own :-) ...