다음을 통해 공유


Exposing Custom WCF Headers through WCF Behaviors

Since the WCF Publishing Wizard in BizTalk does not support adding custom headers defined at the server, we need to programmatically modify what gets created by the wizard to add custom headers.  However, from the client you have the option to pass in header values at will.  If you are passing in headers generated at the client BizTalk will take them and map them to the context.  However, they show up as an XML fragment and not as individual data items.  It becomes annoying to constantly parse the fragment each and every time you want to get to the data.

What we are really interested in is the ability to expose the end point with the header values already defined, accept the header values from the client and either promote or write the values to the context and lastly, be able to create a behavior that you can attach to your WCF endpoint that exposes the properties through configuration to let you dynamically, per end point, set the header items and what you want to do with them as they are submitted.  This will be a three part posting with a post covering each of these features.

For this first post, we will focus on the ability to expose the end point with the header values already defined.  What makes this even more interesting is that there is no WSDL file as this gets generated dynamically when you access the SVC file.  If you wish you can create a static WSDL file and then use the externalMetadataLocation attribute of the element in the Web.config file that the wizard generates to specify the location of the WSDL file.  Then the static WSDL file will be sent to the user in response to WSDL and metadata exchange (MEX) requests instead of the auto-generated WSDL. 

In our solution, we did not want to have to create WSDL files for each of our endpoints, nor did we want to maintain them.  We needed a way to hook in to the dynamic WSDL creation process.

There are a number of posts out there that talk about this but after reviewing them I found that none of them gave the whole picture.  They were all very good and they provided enough information to fill in many missing pieces but there was enough missing that I though it warranted looking at the whole picture.

We are going to start by creating our own EndPointBehavior.  The EndPointBehavior allows us to inject custom functionality in the WCF execution pipeline. 

To create the EndPointBehavior we need to create a solution that references System.ServiceModel.dll and includes a class that derives from BehaviorExtensionElement, IWsdlExportExtension and IEndpointBehavior.  We need the functionality of the BehaviorExtensionElement to implement the configuration of the behavior, the functionality of the IWsdlExportExtension to change the generated WSDL and the functionality of the IEndPointBehavior to define the endpoint and its behavior.

Lets add a class file to our solution called SoapHeaderEndpointBehavior.  After we create the class and inherit from our objects we need to add the following line of code to the ExportEndPoint method

SoapHeaderWsdlExport.ExportEndpoint(exporter,context);

and we need to add the following two lines of code to the ApplyDispatchBehavior method.

SoapHeaderMessageInspector headerInspector = new SoapHeaderMessageInspector();
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(headerInspector);

Our code should look like this:

using System;
using System.Collections.Generic;
using System.Collections;
using System.Configuration;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Web.Services;
using System.Web.Services.Description;
using WsdlDescription = System.Web.Services.Description.ServiceDescription;

namespace Services.WCF.ServiceBehavior
{
public class SoapHeaderEndpointBehavior : BehaviorExtensionElement, IWsdlExportExtension, IEndpointBehavior
    {
#region BehaviorExtensionElement Members

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

protected override object CreateBehavior()
            {
return new SoapHeaderEndpointBehavior();
            }
        #endregion

#region IWsdlExportExtension Members

public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
            {
//throw new NotImplementedException();
            }

public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
            {
SoapHeaderWsdlExport.ExportEndpoint(exporter,context);
            }

        #endregion

#region IEndpointBehavior Members

public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
            {
//throw new NotImplementedException();
            }

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

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
            {
SoapHeaderMessageInspector headerInspector = new SoapHeaderMessageInspector();
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(headerInspector);
            }

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

        #endregion
    }
}

When we added code to the ExportEndpoint method, we utilized a custom object.  Let's add another class to our solution to implement the SoapHeaderWsdlExport.  This class will add a header schema and its namespace, create and add a header message description and finally add the header to the operation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Schema;
using System.Reflection;
using System.Web.Services.Description;
using System.Xml;
using System.ServiceModel.Description;
using WsdlDescription = System.Web.Services.Description.ServiceDescription;

namespace Services.WCF.ServiceBehavior
{
class SoapHeaderWsdlExport
    {
public static void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
        {
// Read the schema of the custom header message
XmlSchema customSoapHeaderSchema = XmlSchema.Read(Assembly.GetExecutingAssembly().GetManifestResourceStream("Services.WCF.ServiceBehavior.SoapHeader.xsd"),
new ValidationEventHandler (SoapHeaderWsdlExport.ValidationCallBack));

// Create the HeaderMessage to add to wsdl:message AND to refer to from wsdl:operation
System.Web.Services.Description.Message headerMessage = CreateHeaderMessage();

foreach (WsdlDescription wsdl in exporter.GeneratedWsdlDocuments)
            {
// Add the schema of the CustomSoapHeader to the types AND add the namespace to the list of namespaces
wsdl.Types.Schemas.Add(customSoapHeaderSchema);
wsdl.Namespaces.Add("sh", SoapHeaderNames.SoapHeaderNamespace);

// The actual adding of the message to the list of messages
wsdl.Messages.Add(headerMessage);
            }

addHeaderToOperations(headerMessage, context);
        }

private static System.Web.Services.Description.Message CreateHeaderMessage()
        {
// Create Message
System.Web.Services.Description.Message headerMessage = new System.Web.Services.Description.Message();

// Set the name of the header message
headerMessage.Name = SoapHeaderNames.SoapHeaderName;

// Create the messagepart and add to the header message
MessagePart part = new MessagePart();
part.Name = "Header";
part.Element = new XmlQualifiedName(SoapHeaderNames.SoapHeaderName, SoapHeaderNames.SoapHeaderNamespace);
headerMessage.Parts.Add(part);

return headerMessage;
        }

private static void addHeaderToOperations(System.Web.Services.Description.Message headerMessage, WsdlEndpointConversionContext context)
        {
// Create a XmlQualifiedName based on the header message, this will be used for binding the header message and the SoapHeaderBinding
XmlQualifiedName header = new XmlQualifiedName(headerMessage.Name, headerMessage.ServiceDescription.TargetNamespace);

foreach (OperationBinding operation in context.WsdlBinding.Operations)
            {
// Add the SoapHeaderBinding to the MessageBinding
ExportMessageHeaderBinding(operation.Input, context, header, false);
ExportMessageHeaderBinding(operation.Output, context, header, false);
            }
        }

private static void ExportMessageHeaderBinding(MessageBinding messageBinding, WsdlEndpointConversionContext context, XmlQualifiedName header, bool isEncoded)
        {
// For brevity, assume Soap12HeaderBinding for Soap 1.2
SoapHeaderBinding binding = new Soap12HeaderBinding();
binding.Part = "Header";
binding.Message = header;
binding.Use = isEncoded ? SoapBindingUse.Encoded : SoapBindingUse.Literal;

messageBinding.Extensions.Add(binding);
        }

private static void ValidationCallBack(object sender, ValidationEventArgs args)
        {
if (args.Severity == XmlSeverityType.Warning)
Console.WriteLine("\tWarning: Matching schema not found. No validation occurred." + args.Message);
else
Console.WriteLine("\tValidation error: " + args.Message);

}

    }
}

In the addHeaderToOperations method there are two calls to the ExportMessageHeaderBinding method.  The second call passes the operation.Output parameter which will pass the header back to the calling application with the response message.  This also also means that the method signature at the client will be to pass in the header object by Ref.  Since we needed the client to pass the header data into BizTalk we didn't need to echo the header back to the client so we deleted this line.  If you want to echo it back then keep this line (as shown in the code above).

Also, in the code above, in the ExportEndpoint and CreateHeaderMessage methods there was another custom class called SoapHeaderNames.  This class contained the values that we wanted to place in the custom header.  By creating a class for this data we could limit the location of this information to one location.  The code for the SoapHeaderNames class looks like this:

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

namespace Services.WCF.ServiceBehavior
{
public static class SoapHeaderNames
    {
public const String SoapHeaderName = "SoapHeader";

public const String AppName = "App";
public const String UserName = "User";

public const String SoapHeaderNamespace = https://servicebehavior.mycompany.com;
    }
}

Way back at the top, in the ApplyDispatchBehavior method of the SoapHeaderEndpointBehavior class we have a custom object called SoapHeaderMessageInspector.  Therefore, let's add another class for the SoapHeaderMessageInspector.  There are two methods that we must implement on the IDispatchMessageInspector and they are the AfterReceiveRequest and the BeforeReceiveRequest.  Since we are interested in applying the headers in the dynamic WSDL we will only need code in the BeforeReceiveRequest.  The code will look like this:

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

namespace Services.WCF.ServiceBehavior
{
class SoapHeaderMessageInspector: IDispatchMessageInspector
    {

#region IDispatchMessageInspector Members

public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
            {
return null;
            }

public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
            {
// Look for my custom header in the request
Int32 headerPosition = OperationContext.Current.IncomingMessageHeaders.FindHeader(SoapHeaderNames.SoapHeaderName, SoapHeaderNames.SoapHeaderNamespace);

// Get an XmlDictionaryReader to read the header content
XmlDictionaryReader reader = OperationContext.Current.IncomingMessageHeaders.GetReaderAtHeader(headerPosition);

// Read through its static method ReadHeader
SoapHeader header = SoapHeader.ReadHeader(reader);

if (header != null)
                {
// Add the header from the request
reply.Headers.Add(header);
                }
            }

        #endregion
    }
}

This code grabs the header section and will inject the header elements.  In the BeforeSendReply method you will see we are using a SoapHeader object.  This object contains the properties and methods to deal with the elements that we will be reading and writing to the header.

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

namespace Services.WCF.ServiceBehavior
{
    [Serializable]
public class SoapHeader: MessageHeader
    {
private string _app;
private string _user;

public string App
        {
get
            {
return (this._app);
            }
set
            {
this._app = value;
            }
        }

public string User
        {
get
            {
return (this._user);
            }
set
            {
this._user = value;
            }
        }

public SoapHeader()
        {
        }

public SoapHeader(string app, string user)
        {
this._app = app;
this._user = user;
}

public override string Name
        {
get { return (SoapHeaderNames.SoapHeaderName); }
}

public override string Namespace
        {
get { return (SoapHeaderNames.SoapHeaderNamespace); }
}

protected override void OnWriteHeaderContents(System.Xml.XmlDictionaryWriter writer, MessageVersion messageVersion)
        {
// Write the content of the header directly using the XmlDictionaryWriter
writer.WriteElementString(SoapHeaderNames.AppName, this.App);
writer.WriteElementString(SoapHeaderNames.UserName, this.User);
}

public static SoapHeader ReadHeader(XmlDictionaryReader reader)
        {
String app = null;
String user = null;

// Read the header content (key) using the XmlDictionaryReader
if (reader.ReadToDescendant(SoapHeaderNames.AppName, SoapHeaderNames.SoapHeaderNamespace))
            {
app = reader.ReadElementString();
            }

if (reader.ReadToDescendant(SoapHeaderNames.UserName, SoapHeaderNames.SoapHeaderNamespace))
            {
user = reader.ReadElementString();
            }

if (!String.IsNullOrEmpty(app) && !String.IsNullOrEmpty(user))
            {
return new SoapHeader(app, user);
            }
else
            {
return null;
            }
        }

    }
}

There is also a schema, called SoapHeader.xsd, that needs to be added to the project as well.  This schema defines the message contract for the header and is referenced and used in the ExportEndpoint method of the SoapHeaderWsdlExport class and looks like this:

<?xml version="1.0" encoding="utf-16"?>
<xs:schema xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:b="https://schemas.microsoft.com/BizTalk/2003" xmlns="https://servicebehavior.mycompany.com" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="https://servicebehavior.mycompany.com" xmlns:xs="https://www.w3.org/2001/XMLSchema">
    <xs:element name="SoapHeader">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="App" type="xs:string" />
                <xs:element name="User" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

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. 

There is an easy way to add entries by using the SvcConfigEditor.exe tool.  This tool is part of the Windows SDK and, if installed, can be found in the \Program Files\Microsoft SDKs\Windows\v6.0A\Bin directory. 

Once this utility is open you can click on the File ->Open->Config File menu item.  Open the machine.config file.  At the bottom of the tree view on the left side you will see an Advanced folder.  Expand that node and expand the the Extensions folder.  Click on the the 'behavior element extensions' node.  At the bottom right, click on the new button.  This will bring up the Extension Configuration Element Editor dialog box.  Enter the name you wish to give to your extension and then click on the ellipses next to type.  This will bring up the Type Browser dialog box.  Browse to your component (also note that you can select assemblies already placed in the GAC).  Once selected, your fully qualified assembly name will be entered.  Select Save under the File menu.  You are now ready to start using the new behavior.

Up to this point everything we have done has been specifically WCF functionality.  The next paragraph will outline how we can utilized this behavior in a BizTalk WCF endpoint. 

Create a WCF endpoint in BizTalk (if you need more information on creating a WCF endpoint check out the docs on MSDN (Insert Link)).  One thing to keep in mind is that when you create a WCF end point in BizTalk using one of the standard bindings you will not have the option to specify a behavior.  In order to specify a behavior you need to specify either the WCF-Custom or WCF-CustomIsolated binding.  Once you select the binding type, click on the Configure button and the Transport Properties dialog will appear.  Select the Behavior tab and then right click on the the Endpoint Behavior node.  Once the popup menu appears, select Add extension.  Select your behavior from the Select Behavior Extensions dialog box and click OK.  Enter the rest of the specific information you need for the end point and click OK to save your endpoint.

We have now done everything that is needed to create a custom behavior including the ability to link into the dynamic WSDL creation process at run time, register the behavior and finally to use the behavior.

Now, when you create a client against the end point you will see that there will be two parameters for the web method call.  The first will be the custom header and the second will be the message body.  When we look in the object browser for the header object we will see that the two items appear that we defined in our behavior. 

As I said at the beginning of this post, this will be a three part series.  What we have not covered is the ability to accept these header values from the client and promote or write the values to the context (part 2) and we have not covered the ability to create a behavior that exposes the properties through configuration to let you dynamically, per end point, set the header items (part 3).

Comments