3. How It Works
BizTalk includes
out-of-the-box functionality for processing messages within receive and
send pipelines. However, situations may arise where custom processing of
messages is required. For example, you may need custom message
validation, processing of flat files, or formatting of messages to HTML
that BizTalk does not natively support. Custom pipeline components offer
incredible flexibility for preprocessing or postprocessing messages via
send and receive pipelines. Additionally, custom pipeline components
can coexist with native BizTalk pipeline components in the same
pipeline.
In BizTalk solutions
where message content or context determines routing, you may need to
route messages based on the lack of existence of a property. BizTalk
does not have the ability to route on the lack of property existence.
However, you can use a custom pipeline component to create a property
that is always part of a message. Rather than check for the existence of
the property, you can check whether the property contains an empty
string value. The solution that accompanies this recipe creates a custom
pipeline component that creates a context property for every message
that is processed by the component.
Creating custom pipeline
components may seem like an extensive task due to the code required for
hosting the component in a BizTalk pipeline, as well as setting the
design-time properties. In actuality, there is a single method that
contains the processing logic, and the remainder of the code supports
the component design-time properties.
Your first task in creating a
custom pipeline component is to create a project reference to the Microsoft.BizTalk.Pipeline.dll file contained in the main BizTalk installation
folder. After you have added the appropriate project reference, you must
consider the implementation of the custom pipeline component. There are
three logical areas to a custom pipeline component to consider:
Attributes and class declaration
Design-time properties
Implementation of the four pipeline
interfaces: IBaseComponent, IComponentUI, IPersistPropertyBag,
and IComponent
The following sections describe
each of these areas in more detail.
4. Attributes and
Class Declaration
Consider how you plan on using
the custom component, such as in a send or receive pipeline.
Additionally, consider the pipeline stage in which you plan on
implementing the custom component. The attributes and class declaration
indicate to Visual Studio that the assembly is a custom pipeline
component. If you do not properly identify that you are creating a
BizTalk custom pipeline component, you will not be able to add the
component to the Visual Studio toolbox or be able to add the component
to a pipeline stage. Here is the header section from the sample code
shown previously in Listing 1:
[ComponentCategory(CategoryTypes.CATID_PipelineComponent)]
[ComponentCategory(CategoryTypes.CATID_Any)]
[System.Runtime.InteropServices.Guid("63ed4b26-63cd-4d29-9661-f584c94cf858")]
public class PropertyPromotionPipelineExample :
Microsoft.BizTalk.Component.Interop.IBaseComponent
, Microsoft.BizTalk.Component.Interop.IComponent
, Microsoft.BizTalk.Component.Interop.IComponentUI
, Microsoft.BizTalk.Component.Interop.IPersistPropertyBag
The sample custom component is identified, via the ComponentCategory attributes, as a pipeline component for use
in any pipeline stage. Additionally, the class declaration specifies
that four interfaces (IBaseComponent, IComponent, IComponentUI,
and IPersistPropertyBag) will be
implemented within the class. The GUID is required for use in COM
interop with unmanaged code.
5. Design-Time
Properties
Custom component
design-time properties are exposed via public declarations and
appropriate get/set methods.
The following is the section of Listing 1 that demonstrates how two design-time properties are exposed.
#region Public Properties
// Display the following public properties for design time
public string CustomContextPropertyName
{
get { return m_propname;}
set { m_propname = value;}
}
// Display the following public properties for design time
public string CustomContextPropertyNamespace
{
get { return m_propnamespace;}
set { m_propnamespace = value;}
}
#endregion
6. Implementation
of the Four Pipeline Interfaces
The final logical area
required in the creation of a custom pipeline component is the
implementation of four interfaces, as summarized in Table 1. The four interfaces provide the design-time and
runtime implementations for the custom component and serve as a guide.
Not all of the interfaces must contain code, and the IComponent interface contains the function that performs
the message processing.
NOTE
If the custom
component is to reside in either the Assemble or Disassemble pipeline
stages, the component must also implement the appropriate
assemble/disassemble interface.
Table 1.
Interface Implementation
Interface | Description | Implementation
Notes |
---|
IBaseComponent | Properties related to the basic information about the
custom component. | Contains three properties that enable the engine
to retrieve the component name, version, and description. |
IComponentUI | Defines
the properties for displaying the custom component in the design-time
user interface as well as the validation rules for design-time
properties. | Contains two methods: one that allows the
validation of the component's configuration and another that provides a
pointer reference to the icon that will be displayed in the development
tool set for the custom component. If null is set for the pointer, the
default icon will be used. |
IPersistPropertyBag | Custom
pipeline properties that are displayed at runtime when using the custom
assembly in a pipeline. | Enables a component to store and
receive its configuration information. |
IComponent | Defines the method used by all pipeline components.
Assembler/disassembler custom components implement their own interface. | Contains
the main function that performs the heavy lifting and processing of the
inbound/outbound message. This method takes as parameters the pipeline
context and incoming message. |
6.1. IBaseComponent
IBaseComponent
contains three read-only properties that return the description,
version, and name of the component to the design-time environment and
other tools interested in basic component information. Implementing the IBaseComponent is straightforward and requires
implementing only the three read-only properties. Here is the section of
the code in Listing 1 that demonstrates the implementation of
the three properties.
#region IBaseComponent members defines Description, Name, and Version
public string Description
{
get
{
return "Sample Custom Pipeline Component";
}
}
public string Name
{
get
{
return "Sample Custom Pipeline Component";
}
}
public string Version
{
get
{
return "1.0";
}
}
#endregion
6.2. IComponentUI
IComponentUI
serves to present the component icon in the design-time tool set. The
two methods implemented in the IComponentUI are Icon
and Validate. The Icon
method provides a pointer to the graphic icon displayed in the
design-time user interface. If no icon is specified, Visual Studio will
display the default icon in the BizTalk Pipeline Components section of
the Toolbox. The Validate method allows
processing of any design-time properties. For example, if you have a
custom design-time property that requires information, you can include
validation rules within the Validate
method to verify the design-time information entered.
The following portion of Listing 1 shows both the Validate and Icon methods. In the solution example included with
this recipe, the default Visual Studio icon will be used, and no
special validation rules are required for the specified user data.
#region IComponentUI members contains design time information
// Include a validate method used by BizTalk
public IEnumerator Validate(object obj)
{
IEnumerator enumerator = null;
// Return a null
return enumerator;
}
// We do not have an icon for this custom component
[Browsable(false)]
public System.IntPtr Icon
{
get
{
// No icon associated with this pipeline component
return IntPtr.Zero;
}
}
#endregion
6.3.
IPersistPropertyBag
The purpose of the IPersistPropertyBag
interface is to provide access to your object to unmanaged code. If you
are familiar with .NET, then you may have used property bags in other
projects. IPersistPropertyBag also
allows access to design-time configuration values. There are four public
methods that exist in the IPersistPropertyBag interface: GetClassID,
initNew, Load, and Save. The GetClassID function must return a unique ID that represents
the component. The initNew function can
be used to establish structures (data, caching, and memory) used by the
other IPersistPropertyBag methods.
The final functions facilitate the loading and saving of property
values. In the solution example accompanying this recipe, two additional
methods were created to wrap the actual read/write functions of the
property bag; however, the read and write functions could also be called
directly from the Load and Save functions.
The following portion of the
code from Listing 1 demonstrates the implementation of the four IPersistPropertyBag
functions as well as the two helper functions.
#region IPersistPropertyBag members contains placeholders
public void GetClassID(out Guid classid)
{
// Return class ID of this component for usage from unmanaged code.
classid = new System.Guid("63ed4b26-63cd-4d29-9661-f584c94cf858");
}
public void InitNew()
{
// Initialization not implemented
}
public void Load(IPropertyBag propertyBag, Int32 errorlog)
{
// Load configuration property for component.
string val = (string)ReadPropertyBag(propertyBag,
m_propbagkey_customproprop);
if (val != null)
m_propname = val;
val = (string)ReadPropertyBag(propertyBag,
m_propbagkey_custompropropnamespace);
if (val != null)
m_propnamespace = val;
}
public void Save(IPropertyBag propertyBag
, Boolean clearDirty, Boolean saveAllProperties)
{
// Saves the current component configuration into the property bag.
object val = (object)m_propname;
WritePropertyBag(propertyBag,
m_propbagkey_customproprop, val);
val = (object)m_propnamespace;
WritePropertyBag(propertyBag,
m_propbagkey_custompropropnamespace, val);
}
private static object ReadPropertyBag(IPropertyBag propertyBag
, string propertyName)
{
// Reads property value from property bag.
object val = null;
try
{
propertyBag.Read(propertyName, out val, 0);
}
catch(ArgumentException)
{
return val;
}
catch(Exception ex)
{
throw new ApplicationException(ex.Message);
}
return val;
}
private static void WritePropertyBag(IPropertyBag propertyBag
, string propertyName, object val)
{
// Writes property values into a property bag.
try
{
propertyBag.Write(propertyName, ref val);
}
catch(Exception ex)
{
throw new ApplicationException(ex.Message);
}
}
#endregion
6.4. IComponent
IComponent is the
most important interface in the component, as it contains the processing
logic for messages. This interface contains a single method, Execute,
which takes two parameters. BizTalk calls the Execute method to process the message, and then passes
the message and the context of the message as the two parameters. The
following outlines the Execute method
declaration and the two required parameters in Listing 1.
#region IComponent members contains the main implementation
public IBaseMessage Execute(IPipelineContext pContext
, IBaseMessage pInMsg)
{
// Create custom context property on message
pInMsg.Context.Promote(m_propname, m_propnamespace, string.Empty);
return pInMsg;
}
#endregion
The IPipelineContext
parameter refers to the environment in which the component is running.
For example, the IPipelineContext object
contains pipeline property information, including the pipeline stage in
which the component is running. The IPipelineContext object also contains a resource tracker, which
cleans up objects. The IBaseMessage
object contains the inbound message. The main purpose of the custom
component is to perform some level of processing on the inbound message
object.
The Execute method returns the IBaseMessage object, which represents all parts of the
processed message (such as the message content and context). You may
perform any type of message or context processing if you return the IBaseMessage object at the end of the function. When BizTalk
processes messages through pipelines, it streams the messages, rather
than passing the whole messages. Additionally, the message passed
through the pipeline is a read-only data object. The solution example
accompanying this recipe demonstrates only adding a context property and
does not demonstrate updating the content of the message. You must
perform the following steps if you plan to alter the message content:
Create
a memory stream object to hold the contents on the updated message or a
copy of the inbound message. Remember that an inbound message is
read-only, and you need a new container to perform updates to the
inbound message. The new memory stream object is a container for the
updated message.
Process the inbound message
stream. The easiest way of processing the inbound message is to copy
the stream to a string and load the message into an XMLDocument.
However, using an XMLDocument object does
not perform well and is not recommended for a production-type solution.
A better method involves using a stream reader to manipulate the
inbound stream. Consider the following approaches for manipulating the
inbound memory stream:
Use Stream.Read() as the primary mechanism for dealing with
message content.
Use XMLReader.Read() as the secondary mechanism for dealing with
message content.
Use
the XML message in the DOM as a last resort option due to the
performance hit. Specifically, if you're dealing with large messages, do
not load the entire message into the DOM for processing.
Set
the message body part. After processing the message, you must return
the updated message. If you used a memory stream object, you can set the
return IBaseMessage.Data object to
the memory stream object. Remember to rewind the updated memory stream
object so you are passing the whole message and not the end of the
memory stream. The pipeline processor will not attempt to rewind the
stream, and you will receive a pipeline if the stream is not rewound.
Add
the memory stream to the resource tracker. If you used a new memory
stream object, make sure to add the memory stream object to the IPipelineContext.Resource
tracker for cleanup.
Pipelines are the first line of
processing before a message is received by the BizTalk MessageBox or
before the message is received by a target system. Out-of-the-box
functionality supports the ability to perform straightforward processing
of messages. There may be situations that require more complex
processing, data validation, or interaction with .NET objects. In those
situations, implementing a custom pipeline component offers the
flexibility of adding processing logic within the BizTalk framework.
The main function
required for implementing a custom pipeline component is the Execute
function in the IComponent interface.
The other interfaces serve for design-time and component interactions
with the runtime engine.
Manipulation of the inbound
message requires making a copy of the inbound message stream, as the
inbound message stream is read-only. Before a memory stream can be
returned to the IBaseMessage.Data object,
it must be rewound, as the BizTalk pipeline engine does not perform this
function. You should also clean up memory stream objects by adding the
object to the pipeline resource tracker.
NOTE
There are
several pipeline component creation tools available on the Internet that
may be used to reduce overall coding effort.