Implementing a Custom BizTalk Adapter as a Custom WCF Channel Part 1 – Send

This is a walkthrough of the steps needed to create a custom WCF channel that can be used from BizTalk.

As you probably know, you can also create a “native” BizTalk adapter using the BizTalk adapter framework (and wizard), but with the WCF interoperability in BizTalk Server 2006 R2 and BizTalk Server 2009, you can implement a WCF channel instead, which makes the adapter available for other .NET applications.

Also, there is a WCF LOB Adapter SDK (documentation) that is more appropriate if you want to integrate with a line of business system. The custom channel method described here is more appropriate if you want to support transport protocols not natively supported by BizTalk.

The transport implemented in this example is a fairly easy one, namely file. (The send channel writes to files and the receive channel reads files.) In order to make this work, I have studied the Transport- UDP sample and even stolen some code from it. I have also used a lot of trial and error.

Create the Project

First, create a new Windows class library project. Call it e.g. WCFFileTransport. Then, add references to System.ServiceModel and System.Configuration.

image

image

Implement the channel factory and the send channel

The send side is fairly straight forward. You need two classes, one that inherits from ChannelFactoryBase and one that inherits form ChannelBase.

Create a class called FileChannelFactory that inherits from ChannelFactoryBase<IRequestChannel>. (I have read that you cannot use IOutputChannel with BizTalk, but I haven’t verified that.) The FileChannelFactory constructor, which is called by FileBindingElement (which is described later), has a filename parameter. The framework will first call OnOpen and then OnCreateChannel. OnCreateChannel creates the channel passing the filename as a parameter.

class FileChannelFactory : ChannelFactoryBase<IRequestChannel>
//class FileChannelFactory : ChannelFactoryBase<IOutputChannel>
{
    private string fileName;
    private bool useXmlWrapper;

    public FileChannelFactory(string fileName, bool useXmlWrapper, BindingContext context)
        : base(context.Binding)
    {
        this.fileName = fileName;
        this.useXmlWrapper = useXmlWrapper;
    }

    protected override IRequestChannel OnCreateChannel(System.ServiceModel.EndpointAddress address, Uri via)
    //protected override IOutputChannel OnCreateChannel(System.ServiceModel.EndpointAddress address, Uri via)
    {
        Debug.WriteLine("OnCreateChannel called", this.GetType().FullName);
        return new FileSendChannel(this, fileName, useXmlWrapper);
    }

    protected override void OnOpen(TimeSpan timeout)
    {
        Debug.WriteLine("OnOpen called with timeout " + timeout.ToString(), this.GetType().FullName);
    }

    protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
    {
        Debug.WriteLine("OnBeginOpen called", this.GetType().FullName);
        throw new NotImplementedException();
    }

    protected override void OnEndOpen(IAsyncResult result)
    {
        Debug.WriteLine("OnEndOpen called", this.GetType().FullName);
        throw new NotImplementedException();
    }
}

Now, create a class implementing the actual send channel. The following methods are called in the following sequence: OnOpen, BeginRequest, EndRequest, OnClose.

class FileSendChannel : ChannelBase, IRequestChannel
//class FileSendChannel : ChannelBase, IOutputChannel
{
    private string fileName;
    private bool useXmlWrapper;

    delegate Message RequestAsyncDelegate(Message message);
    private RequestAsyncDelegate requestAsyncDelegate;

    public FileSendChannel(ChannelManagerBase channelManager, string fileName, bool useXmlWrapper)
        : base(channelManager)
    {
        this.fileName = fileName;
        this.useXmlWrapper = useXmlWrapper;
        this.requestAsyncDelegate = new RequestAsyncDelegate(Request);
    }

    #region IRequestChannel Members

    public System.ServiceModel.EndpointAddress RemoteAddress
    {
        get { throw new NotImplementedException(); }
    }

    public Uri Via
    {
        get { throw new NotImplementedException(); }
    }

    public Message Request(Message message)
    {
        // Replace special tokens (macros) in the file name.
        string fileName = this.fileName.Replace("%datetime%", DateTime.Now.ToString("yyyy-MM-dd hhmmss.ffffff"));
        // Create the file and write to it.
        FileStream stream = new FileStream(fileName, FileMode.Create);
        if (useXmlWrapper)
        {
            byte[] buffer = message.GetBody<byte[]>();
            stream.Write(buffer, 0, buffer.Length);
            stream.Close();
        }
        else
        {
            XmlDictionaryWriter xdw = XmlDictionaryWriter.CreateTextWriter(stream);
            //XmlDictionaryWriter xdw = XmlDictionaryWriter.CreateBinaryWriter(stream);
            message.WriteBodyContents(xdw);
            xdw.Close();
        }
        // Return an empty message
        return Message.CreateMessage(MessageVersion.Default, "Dummy");
    }

    public Message Request(Message message, TimeSpan timeout)
    {
        // This implementation does not handle timeout.
        return Request(message);
    }

    public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state)
    {
        Debug.WriteLine("BeginRequest called with timeout " + timeout.ToString(), this.GetType().FullName);
        // This implementation does not handle timeout.
        return BeginRequest(message, callback, state);
    }

    public IAsyncResult BeginRequest(Message message, AsyncCallback callback, object state)
    {
        Debug.WriteLine("BeginRequest called", this.GetType().FullName);
        return requestAsyncDelegate.BeginInvoke(message, callback, state);
    }

    public Message EndRequest(IAsyncResult result)
    {
        Debug.WriteLine("EndRequest called", this.GetType().FullName);
        return requestAsyncDelegate.EndInvoke(result);
    }

    #endregion

    #region ChannelBase abstract members

    protected override void OnOpen(TimeSpan timeout)
    {
        Debug.WriteLine("OnOpen called with timeout " + timeout.ToString(), this.GetType().FullName);
    }

    protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
    {
        Debug.WriteLine("OnBeingOpen called with timeout " + timeout.ToString(), this.GetType().FullName);
        throw new NotImplementedException();
    }

    protected override void OnEndOpen(IAsyncResult result)
    {
        Debug.WriteLine("OnEndOpen called", this.GetType().FullName);
    }

    protected override void OnClose(TimeSpan timeout)
    {
        Debug.WriteLine("OnClose called", this.GetType().FullName);
    }

    protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
    {
        Debug.WriteLine("OnBeginClose called", this.GetType().FullName);
        throw new NotImplementedException();
    }

    protected override void OnEndClose(IAsyncResult result)
    {
        Debug.WriteLine("OnEndClose called", this.GetType().FullName);
    }

    protected override void OnAbort()
    {
        Debug.WriteLine("OnAbort called", this.GetType().FullName);
    }

    #endregion

Implement Configuration Classes

The first class to create is FileBindingElement, which inherits from TransportBindingElement. FileBindingElement contains configuration properties, in this case FileName and UseXmlWrapper (more on that later) , and builds a channel factory (BuildChannelFactory), which in turns creates the send channel.

public class FileBindingElement : TransportBindingElement
{
    public string FileName { get; set; }
    public bool UseXmlWrapper { get; set; }

    public FileBindingElement()
    {
        this.FileName = FileDefaults.FileName;
        this.UseXmlWrapper = FileDefaults.UseXmlWrapper;
    }

    public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
    {
        Debug.WriteLine("CanBuildChannelFactory: " + typeof(TChannel).FullName, this.GetType().FullName);
        return typeof(TChannel) == typeof(IRequestChannel);
        //return typeof(TChannel) == typeof(IOutputChannel);
    }

    public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
    {
        Debug.WriteLine("BuildChannelFactory called", this.GetType().FullName);
        return (IChannelFactory<TChannel>)new FileChannelFactory(FileName, UseXmlWrapper, context);
    }

    public override bool CanBuildChannelListener<TChannel>(BindingContext context)
    {
        Debug.WriteLine("CanBuildChannelListener called. Type of channel = " + typeof(TChannel).Name, this.GetType().FullName);
        //return typeof(TChannel) == typeof(IReplyChannel);
        return typeof(TChannel) == typeof(IInputChannel);
    }

    public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
    {
        Debug.WriteLine("BuildChannelListener called.", this.GetType().FullName);
        return (IChannelListener<TChannel>)new FileChannelListener(this, context);
    }

    public override string Scheme
    {
        get { return "net.file"; }
    }

    public override BindingElement Clone()
    {
        return new FileBindingElement(this);
    }

    private FileBindingElement(FileBindingElement elementToBeCloned)
        : base(elementToBeCloned)
    {
        this.FileName = elementToBeCloned.FileName;
        this.UseXmlWrapper = elementToBeCloned.UseXmlWrapper;
    }

CanBuildChannelFactory is called multiple times, but only returns true for the type of channel our channel factory (FileChannelFactory) can build, i.e. a channel that implements IRequestChannel.

BuildChannelFactory creates a new FileChannelFactory with the configured filename as a parameter.

Now, create a signing key and build the project.

To create the rest of the configuration classes, use ConfigurationCodeGenerator (under <samples installation folder>WCFTools):

samplesWCFWFCardSpaceWCFToolsConfigurationCodeGeneratorCSbinConfigurationCodeGenerator.exe /be:WCFFileTransport.FileBindingElement /dll:bindebugWCFFileTransport.dll

Add the output files (FileElement.cs, FileDefaults.cs, FileConfigurationSrings.cs, sampleConfig.xml) to the project and make the following modifications:

  • Rename FileElement to the better describing FileBindingElementExtensionElement. Don’t forget to change sampleConfig.xml!
  • This class inherits from a non-existing class BindingElementExtensionSection – this should be changed to BindingElementExtensionElement.
  • The type of the parameter of CopyFrom should be ServiceModelExtensionElement, not ServiceModelExtensionSection.
  • You can get rid of the ManualAddressing, MaxBufferPoolSize and MaxReceivedMessageSize properties.
  • In FileDefaults, set a sensible default for DefaultFileName and DefaultUseXmlWrapper. In FileBindingElement, use this default.

Rebuild and add the assembly to the GAC.

Configure BizTalk

Copy the bindingElementExtension from the generated sampleConfig.xml and paste it into machine.config (normally located in C:WindowsMicrosoft.NETFrameworkv2.0.xCONFIG). (I changed the name from “file” to “WCFFileAdapter” here.) If you had started BizTalk Administration already, you might need to close and restart it now.

Create a new send port, and choose transport type WCF-Custom. Press the Configure button. On the General tab, type an address, e.g. net.file://localhost/test and an action (any will do). If you forget to state an action you will get this error later:

System.ArgumentNullException: Value cannot be null.
Parameter name: key

On the Binding tab, choose customBinding. Then remove existing binding elements and insert WCFFileAdapter. Specify the output filename.

image

Create a receive port and file receive location, and a filter on the send port so that you can drop test messages that are sent to the new WCF channel.

Test with a simple XML message. It should work. You should get something similar to this in the debug log:

WCFFileTransport.FileBindingElement: CanBuildChannelFactory: System.ServiceModel.Channels.IDuplexChannel
WCFFileTransport.FileBindingElement: CanBuildChannelFactory: System.ServiceModel.Channels.IRequestChannel
WCFFileTransport.FileBindingElement: BuildChannelFactory called
WCFFileTransport.FileChannelFactory: OnOpen called with timeout 00:01:00
WCFFileTransport.FileChannelFactory: OnCreateChannel called
WCFFileTransport.FileSendChannel: OnOpen called with timeout 00:01:00
WCFFileTransport.FileSendChannel: BeginRequest called
WCFFileTransport.FileSendChannel: EndRequest called
WCFFileTransport.FileSendChannel: OnClose called

If you test with a binary file, or an ordinary text file, you must set useXmlWrapper to True as shown above. Otherwise you will get the following error:

A message sent to adapter "WCF-Custom" on send port "net.file" with URI "net.file://localhost/test" is suspended.
Error details: System.Xml.XmlException: Data at the root level is invalid. Line 1, position 1.

In other words, WCF expects that everything is XML. But you probably want to be able to use the file adapter with arbitrary files. That is why I have the useXmlWrapper configuration property. If it set to true, the file contents is wrapped in a binary XML element like this:

<base64Binary xmlns="http://schemas.microsoft.com/2003/10/Serialization/">…</base64Binary>

References

Extending the Channel Layer in Windows Communication Foundation

Advertisements

One thought on “Implementing a Custom BizTalk Adapter as a Custom WCF Channel Part 1 – Send”

  1. Use a passthrough pipeline and you can pass any type of file with no validation or requirements. If you want to add validation for types other than XML you have to make a custom validation component for your a custom pipeline.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s