Saturday, March 13, 2010

Adding Custom SOAP Headers in WCF

Where I work, all of our internal published web services are proxied using IBM’s DataPower SOA Appliances.  One of the requirements for services that want to leverage the infrastructure is that all requests and responses to the service must contain a custom header which contains parameters like “MessageID” for correlation, “SiteID” for location information, etc.  For SOAP based services, this information must be transmitted in the SOAP header.  For the purposes of this article, let’s say we need to include an object like the following into the SOAP header of every service operation.

[DataContract]
public class CustomHeader
{
    public const string Name = "customHeader";
    public const string Namespace = "http://mycompany.com";
    public const string OperationContextKey = "CustomSoapHeader";
    
    [DataMember]
    public string MessageID { get; set; }
    
    [DataMember]
    public string SiteID { get; set; }
}

Back in the ASMX days adding headers like this to a service method was as easy as adding a SoapHeader attribute to your WebMethod. Simple.  In the WCF world, not so simple.

There are 3 issues to be addressed:

  1. We need some way to inject the custom header into the WSDL so the service clients are aware of it and can generate client proxies accordingly. 
  2. We need some method to read and process the custom header from the request.
  3. We need some way to roundtrip the header back to the client.

In our case, we’d like to specify whether the custom header should be included at the service contract level, so an IContractBehavior seems like a good place to start.  We also know that we’ll be processing the request and reply messages, so we’re going to need an IDispatchMessageInspector too. 

Let’s start with the message inspector.  The responsibility of this class is to extract the header from incoming messages, store it in the OperationContext in case the service needs to use it (i.e. for logging purposes), and roundtrip it into the reply message.

/// <summary>
/// Retrieves and stores the custom header from requests and roundtrips it into responses.
/// </summary>
public class CustomHeaderMessageInspector : IDispatchMessageInspector
{
    object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        // Grab the header from the request
        // TODO: Handle missing or invalid header
        CustomHeader header = header = request.Headers.GetHeader<CustomHeader>(CustomHeader.Name, CustomHeader.Namespace);

        // Stash the header in the OperationContext so the service code can reference it if necessary
        OperationContext.Current.IncomingMessageProperties[CustomHeader.OperationContextKey] = header;

        // The object returned from this method is passed as the "correlationState" parameter to the BeforeSendReply
        // method.  We can use this to "echo" back the header to the caller.
        return header;
    }

    void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState)
    {
        // correlationState should be the CustomHeader (passed to this method from AfterReceiveRequest)
        // If an exception occured before or during the header processing, this may be null.
        if (correlationState != null)
        {
            // Get the custom header object passed in on the request
            var headerObject = (CustomHeader)correlationState;

            // Build the MessageHeader object from our custom header
            var headerMessage = MessageHeader.CreateHeader(CustomHeader.Name, CustomHeader.Namespace, headerObject);

            // Inject our custom header into the reply.
            reply.Headers.Add(headerMessage);
        }
    }
}

Now that we have the inspector, we’ll need to create an attribute and implement IContractBehavior so we can attach this inspector to the desired service contracts.

/// <summary>
/// Apply this attribute to service contracts to implement CustomHeader processing.
/// </summary>
[AttributeUsage(AttributeTargets.Interface)]
public class CustomMessageHeaderAttribute : Attribute, IContractBehavior, IWsdlExportExtension
{
    void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
    {
        // Attach the message inspector to the endpoint.
        var inspector = new CustomHeaderMessageInspector();
        dispatchRuntime.MessageInspectors.Add(inspector);
    }

    void IWsdlExportExtension.ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
    {
        // TODO: Inject the custom header into the WSDL
    }
    
    // All other interface methods are not used and have been omitted for brevity...
}

At this point, the custom header is usable, but with one problem.  It’s not defined in the WSDL, so the service clients might not “know” about it.  To add the header to the wizard, we just need to implement the ExportContract method of the IWsdlExportExtension interface that we left as a “TODO” in the code above.

void IWsdlExportExtension.ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
{
   // Build the custom header description
   var headerDescription = new MessageHeaderDescription(CustomHeader.Name, CustomHeader.Namespace);
   headerDescription.Type = typeof(CustomHeader);

   // Loop through all the operations defined for the contract and add custom SOAP header to the WSDL
   foreach (OperationDescription op in context.Contract.Operations)
   {
       foreach (MessageDescription messageDescription in op.Messages)
       {
           messageDescription.Headers.Add(headerDescription);
       }
   }
}

Now we have a fully defined and working header.  To tell a service contract to implement the custom header, we just add our custom attribute to the contract definition.

[ServiceContract]
[CustomMessageHeader]
public interface IMyServiceContract 
{
    // Service definition here....
}

Of course this was a very basic implementation for a specific need, but this approach could be extended to create a more “generic attribute” that you could specify header type as a parameter or allow for updating the header values to return in the reply (similar to how the “old school” SoapHeader attribute worked).

3 comments:

Anonymous said...

Thank you so much for this post. This is about the best explanation I've found yet for my needs.

Anonymous said...

Thank

Anonymous said...

Thank you very much, you helped me a lot.

Post a Comment