A Base Class for Building Custom Pipeline Components

In most scenarios I’ve been working on with BizTalk Server, I had a need to introduce one or more custom pipeline components. You always have this feature that is not quite available out of the box and often need to rely on some custom code to address it. In this post, I would like to introduce a class that proves useful as a base class for building custom pipeline components.

This class abstracts away virtually all the boilerplate code that you need to write in order to make a pipeline component. It contains extensive support for the serialization of simple and complex design-time properties and provides minimal helper code for logging to the event log.

In essence, this base class adheres to recommended practices for building custom components.

Sidebar

If you’re still reading, you may have wondered why the need for yet another custom pipeline component base class?

Well, one of the goals of this blog is to occasionally introduce some custom pipeline components. Most of the time, you need to write the same boring boilerplate code to setup the class library project as a pipeline component in the first place. In order to avoid repeating this tedious code all over, I figured I would post my version once, so that every one can follow the future samples I will post.

Think of it as kind of like the moral equivalent of Raymond‘s scratch program for pipeline components, if you will.

Besides, this is probably the best implementation you will ever find on the entire Internet, right? Wink smile

The Code

Let’s dive into the code, and I’ll explain key bits along the way:

All this boilerplate !

One of the most tedious part of setting up a basic working pipeline component is to actually implement all kinds of interfaces. This usually requires a fair amount of boilerplate code.

Since the purpose of a base class is to make like simpler for subsequent usage, it makes sense to implement them here:

public class PipelineComponentBase
    : IBaseComponent
    , IComponentUI
    , IPersistPropertyBag {

#region Design-Time Properties

private bool enabled_ = true;
private string eventlog_ = "Application";
private string eventsource_ = "Pipeline Component";

[Browsable(true)]
public bool Enabled
{
  get { return enabled_; }
  set { enabled_ = value; }
}

[Browsable(true)]
public string EventLog
{
  get { return eventlog_; }
  set { eventlog_ = value; }
}

[Browsable(true)]
public string EventSource
{
  get { return eventsource_; }
  set { eventsource_ = value; }
}

#endregion
  ...
}

All the pipeline components that I write can be disabled for diagnostics purposes without incurring a pipeline deployment or a recompile of the project. That’s why I expose an ‘Enabled’ design-time property that defaults to true.

A pipeline component is a server-side component and involves a great deal of planning an debugging so as not to introduce errors in the final solution. Therefore, I opt to use the Windows Event Log to log various informations during the component execution. The properties ‘EventLog’ and ‘EventSource’ store the details of the target Event Log.

IBaseComponent implementation

#region IBaseComponent Members

public string Description
{
  get
  {
    object[] attrs = GetAttrs(typeof(AssemblyDescriptionAttribute));
    if (attrs.Length > 0)
      return (attrs[0] as AssemblyDescriptionAttribute).Description;
    return String.Empty;
  }
}

public string Name
{
  get
  {
    object[] attrs = GetAttrs(typeof(AssemblyTitleAttribute));
    if (attrs.Length > 0)
    {
      AssemblyTitleAttribute attrTitle = (attrs[0] as AssemblyTitleAttribute);
      if (!String.IsNullOrEmpty(attrTitle.Title))
        return attrTitle.Title;

      attrs = GetAttrs(typeof(AssemblyProductAttribute));
      if (attrs.Length > 0)
        return (attrs[0] as AssemblyProductAttribute).Product;
    }

    return String.Empty;
  }
}

public string Version
{
  get
  {
    object[] attrs = GetAttrs(typeof(AssemblyVersionAttribute));
      if (attrs.Length > 0)
        return (attrs[0] as AssemblyVersionAttribute).Version;
      return String.Empty;
  }
}

#endregion

...

private object[] GetAttrs(Type type)
{
  return GetType().Assembly.GetCustomAttributes(type, false);
}

One interesting feature I introduced to my base class is to actually implement IBaseComponent in terms of attributes from the assembly implementing the pipeline component itself.

So, the description, name and version of the pipeline components are taken, respectively, from the AssemblyDescription, AssemblyTitle/AssemblyProduct of AssemblyVersion attributes.

IComponentUI implementation

#region IComponentUI Members

IntPtr IComponentUI.Icon
{
  get
  {
    string ns = GetType().Namespace;
    string res = String.Format("{0}.Resources.MyIcon.ico", ns);
    Assembly assembly = GetType().Assembly;

    Bitmap bitmap;
    using (System.IO.Stream stream = assembly.GetManifestResourceStream(res))
      bitmap = (Bitmap)CloneObject(new Bitmap(stream));
    return bitmap.GetHicon();
  }
}

public virtual IEnumerator Validate(object projectSystem)
{
  ArrayList errors = new ArrayList(0);
  return errors.GetEnumerator();
}

#endregion

...

private static object CloneObject(object obj)
{
  using (MemoryStream memStream = new MemoryStream())
  {
    BinaryFormatter binaryFormatter =
      new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Clone));
      binaryFormatter.Serialize(memStream, obj);
      memStream.Seek(0, SeekOrigin.Begin);
      return binaryFormatter.Deserialize(memStream);
  }
}

The icon for the pipeline component is taken from an embedded icon file in the implementing assembly. This file must be stored inside a project subfolder named ‘Resources’ and have the hardcoded name ‘MyIcon.ico’.

I could not find a way to hand a bitmap to the Visual Studio designer without keeping a live reference to it. So I had to actually make a deep copy of the resulting bitmap resource.

Error validation is a stub in this current implementation. It will be overridden if necessary in derived classes as appropriate.

IPersistPropertyBag implementation

#region IPersistPropertyBag Members

void IPersistPropertyBag.GetClassID(out Guid classID)
{
  object[] attrs = GetAttrs(typeof(GuidAttribute));
  System.Diagnostics.Debug.Assert(attrs.Length == 1);
  System.Diagnostics.Debug.Assert(attrs[0] is System.Runtime.InteropServices.GuidAttribute);
  classID = new Guid((attrs[0] as System.Runtime.InteropServices.GuidAttribute).Value);
}

public virtual void InitNew()
{
}

Each pipeline component must have a globally unique identifier to hand out to the GetClassID callers. This base class relies on the derived class being decorated with a [GuidAttribute].

Should the programmer of a pipeline component forget to add this otherwise unnecessary attribute to its implementing class (I know I do!) a bunch of Assert statements will warn her right away.

public virtual void Load(IPropertyBag propertyBag, int errorLog)
{
  if (propertyBag == null)
    return;

  // only per-instance configuration overwrites default properties

  object enabled = ReadProperty(propertyBag, "Enabled", errorLog, true);
  if (enabled != null)
    Enabled = (bool)enabled;

  object eventlog = ReadProperty(propertyBag, "EventLog", errorLog, "Application");
  if (eventlog != null)
    EventLog = (string)eventlog;

  object eventsource = ReadProperty(propertyBag, "EventSource", errorLog, "Pipeline Component");
  if (eventsource != null)
    EventSource = (string)eventsource;
}

Loading the values for design-time properties did not prove very intuitive, because the code needs to handle retrieving the values for properties that are specified at design-time in the Visual Studio property grid as well as the values for properties that are potentially overwritten in per-instance pipeline configuration.

Here, we are loading the values for the three properties exposed by this base class.

Notice that we are using a ReadProperty helper method. More on this a little further down the article…

public virtual void Save(IPropertyBag propertyBag, bool clearDirty, bool saveAllProperties)
{
  if (saveAllProperties || Enabled != true)
    WriteProperty(propertyBag, "Enabled", Enabled);

  if (saveAllProperties || EventLog != "Application")
    WriteProperty(propertyBag, "EventLog", EventLog);

  if (saveAllProperties || EventSource != "Pipeline Component")
    WriteProperty(propertyBag, "EventSource", EventSource);
}

#endregion

Likewise, writing the values back to the property bag needed to cater for design-time values as well as per-instance pipeline configuration overwritten values.

Here, we are only overwriting properties whose value differ from their defaults.

Again, notice that we are using a WriteProperty helper method. Along with the ReadProperty mentioned above, they simplify reading and writing the values for a pipeline component design-time properties. Here they are:

#region Read/Write Property Helpers

protected virtual object ReadProperty(IPropertyBag propertyBag, string name)
{
  return ReadProperty(propertyBag, name, 0);
}

protected virtual object ReadProperty(IPropertyBag propertyBag, string name, int errorlog)
{
  return ReadProperty(propertyBag, name, errorlog, null);
}

protected virtual object ReadProperty(IPropertyBag propertyBag, string name, int errorlog, object defaultValue)
{
  try
  {
    object value = null;
    propertyBag.Read(name, out value, 0);
    return value;
  }
  catch (System.ArgumentException e)
  {
    if (defaultValue != null)
      return defaultValue;
    throw new ApplicationException(e.Message);
  }
}

protected virtual void WriteProperty(IPropertyBag propertyBag, string name, object value)
{
  propertyBag.Write(name, ref value);
}

#endregion

The ReadProperty helper method is quite useful in my opinion because it handles the initial case where the values for the required design-time properties is not present in the property bag, as well as implement a ‘default value’ semantics, all this in a very simple way.

Not so much gain from a WriteProperty helper here. But for the sake of consistency, I prefer to still use a helper method.

Serialization Helpers

In order to cater for more complex scenarios involving custom design-time properties that are made of custom classes of collections, I implemented a couple of serialization helper methods in the base class itself.

I will show how to use them in a future article. For the sake of completness, the source code is included here nonetheless.

#region Serialization Helpers

protected virtual object Deserialize(string value, Type type)
{
  using (XmlTextReader textReader = new XmlTextReader(new StringReader(value)))
  {
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.IgnoreProcessingInstructions = true;
    using (XmlReader xmlReader = XmlReader.Create(textReader, settings))
    {
      XmlSerializer serializer = new XmlSerializer(type);
      return serializer.Deserialize(xmlReader);
    }
  }
}

protected virtual string Serialize(object obj)
{
  StringBuilder builder = new StringBuilder();

  using (StringWriter textWriter = new StringWriter(builder))
  {
    XmlWriterSettings settings = new XmlWriterSettings();
    settings.OmitXmlDeclaration = true;
    using (XmlWriter xmlWriter = XmlWriter.Create(textWriter, settings))
    {
      XmlSerializer serializer = new XmlSerializer(obj.GetType());
      serializer.Serialize(xmlWriter, obj);
    }
  }

  return builder.ToString();
}

#endregion

Diagnostics and event logging

Finally, a set of simple helper methods to write messages to the specified Event Log.

#region Event Log Helpers

protected virtual void WriteEventLog(EventLogEntryType type, string message, params object[] args)
{
  string msg = message;

  if (args != null && args.Length != 0)
    msg = String.Format(message, args);

  System.Diagnostics.EventLog.WriteEntry(EventSource, msg, type);
}

protected virtual void WriteEventLog(string message, params object[] args)
{
  WriteEventLog(EventLogEntryType.Information, message, args);
}

protected virtual void WriteErrorLog(string message, params object[] args)
{
  WriteEventLog(EventLogEntryType.Error, message, args);
}

protected virtual void WriteWarningLog(string message, params object[] args)
{
  WriteEventLog(EventLogEntryType.Warning, message, args);
}

#endregion

Note that the Event Log and source must be present when the pipeline component executes.

Conclusion

There you have it.

This actually serves as one of our key base class, albeit in a slightly modified form, for all our custom components in the real world. I’ll talk about them in future posts. In particular, I will focus on how to build robust components that follow the streaming approach to acquiring messages that BizTalk Server encourages.

This entry was posted in Pipeline Components. Bookmark the permalink.

2 Responses to A Base Class for Building Custom Pipeline Components

  1. Patrick says:

    This is a very interesting Blog. I see some similarities with the things I do in BizTalk.Like your resubmit scenario, I have done the same for my customers. And i have almost the same aproach. This pipeline stuff sounds very interesting to me. And you are obviously a better programmer than I am. I can learn a lot from your code. Therefore I am very interested in your code samples. Although they are almost complete in in this blog I am very curious about actual implementation details….It\’s probably too much to ask to send me some source code ?.

  2. Maxime says:

    Thanks for your kind words Patrick.You can find the source code for getting started with the sample at the following location:http://cid-d8d9369449d177da.skydrive.live.com/self.aspx/.Public/Code%20Samples/PipelineComponentBase.zipCheers.

Comments are closed.