Developing Your First Order Pipeline Component

As previously discussed the pipeline system enables sequential workflow processing. It is a COM-based system which has been migrated over the years from the Site Server and previous Commerce Server releases to the current release. Given it's use of COM you will benefit from spending some time to read up on various interoperability issues.

Pipelines themselves are defined in a Pipeline Configuration File (PCF). They hold the order of execution of stages and components. Stages are a grouping or gating of components. They are important to be aware of before jumping into your first component so let's take a moment to review the basic mechanics:

Defining a Stage

Each stage has a GUID, mode, and error level. The GUID of a stage represents a category used for identifying which components are applicable in the particular stage. This isn't strictly enforced, but is more of a convenience feature. When you build a pipeline component you will register it as application to these pre-defined categories (we'll come back to this later). Here's a look at what categories are defined out-of-the-box:

Stage NameGUID
Accept{d82c3490-43c5-11d0-b85d-00c04fd7a0fa}
Add Header{3ceaf920-660a-11d1-9082-00a0c90543b8}
All Stages{d2acd8e0-43c5-11d0-b85d-00c04fd7a0fa}
Application Integration{d3a71c37-ffc7-11d0-b885-00c04fd7a0f9}
Audit{d3a71c34-ffc7-11d0-b885-00c04fd7a0f9}
Compose E-mail{ded09a46-266d-11d2-8ff7-00c04f8ec3b4}
Create Cookies{ded09a45-266d-11d2-8ff7-00c04f8ec3b4}
Decrypt{d3a71c35-ffc7-11d0-b885-00c04fd7a0f9}
Digitally Sign{d3a71c31-ffc7-11d0-b885-00c04fd7a0f9}
Encrypt{d3a71c32-ffc7-11d0-b885-00c04fd7a0f9}
Filter{1ed7c340-0258-11d2-9c3d-00c04fc29cc1}
Filter Recipient{ded09a44-266d-11d2-8ff7-00c04f8ec3b4}
Format{01c93ae5-eea3-11d1-9c34-00c04fc29cc1}
Generate Receipt{3ceaf922-660a-11d1-9082-00a0c90543b8}
Handling{d82c3491-43c5-11d0-b85d-00c04fd7a0fa}
Initial Score{01c93ae7-eea3-11d1-9c34-00c04fc29cc1}
Inventory{d82c3493-43c5-11d0-b85d-00c04fd7a0fa}
Item Adjust Price{57b798e7-02b1-11d1-8d8a-00c04fd7a111}
Item Price{d3a71c3a-ffc7-11d0-b885-00c04fd7a0f9}
Load Context{1ed7c341-0258-11d2-9c3d-00c04fc29cc1}
Map{261cad41-079f-11d1-8d8e-00c04fd7a111}
Map{d3a71c30-ffc7-11d0-b885-00c04fd7a0f9}
Merchant Information{d82c3494-43c5-11d0-b85d-00c04fd7a0fa}
Open Header{3ceaf921-660a-11d1-9082-00a0c90543b8}
Order Adjust Price{57b798ea-02b1-11d1-8d8a-00c04fd7a111}
Order Check{d82c3496-43c5-11d0-b85d-00c04fd7a0fa}
Order Initialization{d82c3497-43c5-11d0-b85d-00c04fd7a0fa}
Order Subtotal{d3a71c3b-ffc7-11d0-b885-00c04fd7a0f9}
Order Total{d82c349d-43c5-11d0-b85d-00c04fd7a0fa}
Payment{d82c3498-43c5-11d0-b85d-00c04fd7a0fa}
Postprocess Recipient{6d8ff873-27d4-11d2-8ffa-00c04f8ec3b4}
Preprocess Request{ded09a42-266d-11d2-8ff7-00c04f8ec3b4}
Product Info{d82c3499-43c5-11d0-b85d-00c04fd7a0fa}
Purchase Check{d3a71c39-ffc7-11d0-b885-00c04fd7a0f9}
Record{01c93ae6-eea3-11d1-9c34-00c04fc29cc1}
Scoring{01c93ae3-eea3-11d1-9c34-00c04fc29cc1}
Select{01c93ae4-eea3-11d1-9c34-00c04fc29cc1}
Send E-mail{ded09a48-266d-11d2-8ff7-00c04f8ec3b4}
Shipping{d82c349a-43c5-11d0-b85d-00c04fd7a0fa}
Shopper Information{d82c349b-43c5-11d0-b85d-00c04fd7a0fa}
Tax{d82c349c-43c5-11d0-b85d-00c04fd7a0fa}
Test{de61c59f-466f-11d1-b69a-0060b03c5535}
Throttle{111c8135-647c-11d2-8253-00c04f8ec3b4}
Transport{d3a71c33-ffc7-11d0-b885-00c04fd7a0f9}
Verify Digital Signature{d3a71c36-ffc7-11d0-b885-00c04fd7a0f9}

The Mode is provided for Site Server compatibility where it identified the stage of a pipeline to be run by the various pipeline execution methods. You should use the default value of 1 to keep things clean because a mode of 0 prevents execution of the pipeline. If you are, for what ever unfortunate reason, still working with Site Server you can dig into what the mode means in the documentation.

The Error Level dictates that execution of this stage can only occur if previous components have return an error level of the value or less. There are three error levels that you can return:

  • Success - 1 (OPPERRORLEV_SUCCESS)
  • Warning - 2 (OPPERRORLEV_WARN)
  • Fail - 3 (OPPERRORLEV_FAIL)

It is important to understand that returning a failure level from your component when your stage is set to accept only warning or better will result in a PipelineExecutionException.

Developing Components

You've planned out your pipeline and identified some tasks that you need to complete. For example, calculating the tax on an order. Pipeline components are COM objects (or .NET classes in a COM Callable Wrapper) that implement several interfaces:

  • IPipelineComponent - Exposes the main execution method for the component.
  • IPipelineComponentAdmin - Enables you to set and retrieve component properties from a Dictionary object. Typically used in conjunction with the ISpecifyPipelineComponentUI or ISpecifyPropertyPages interface.
  • IPipelineComponentDescription - Provides methods to return an object list of items read/written from the order form and context objects passed to the Execute method.
  • IPipelineComponentUI - Allows you to attached a Windows Form dialog box for configuring a pipeline component.
  • ISpecifyPipelineComponentUI - Returns the ProgId that the Pipeline Editor will use to locate the object implementing the IPiplineComponentUI interface.
  • ISpecifyPropertyPages - Allows you to attach additional tabs to the component property page as opposed to a new dialog as you would with ISpecifyPipelineComponentUI/IPipelineComponentUI.

Like any COM component you will need a GUID and ProgId. A basic template for a pipeline component looks like this:

#region References
using Microsoft.CommerceServer.Interop;
using Microsoft.CommerceServer.Runtime;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
#endregion

namespace RockstarGuys.PipelineComponents
{
    /// <summary />
    [ProgId("RockstarGuys.PipelineComponent"), Guid("40F39A18-1476-4e89-B99D-6327A45CBACB"), ComVisible(true)]
    public class PipelineComponent : IPipelineComponent, IPipelineComponentDescription
    {
        #region Constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="T:PipelineComponent"/> class.
        /// </summary>
        public PipelineComponent() { }
        #endregion

        #region Methods
        /// <summary>
        /// Sets the design mode on the component.
        /// </summary>
        /// <remarks>
        /// Use this method to prepare the component for execution in one of two modes:
        /// design mode or execution mode. In design mode, which is useful for running
        /// the Pipeline Editor, errors are more easily tolerated. Execution mode (the
        /// default) is analogous to production mode.
        /// </remarks>
        /// <param name="fEnable">Indicates whether the component should be run in design
        /// mode (True) or execution mode (False).</param>
        public void EnableDesign(int fEnable) { }

        /// <summary>
        /// The entry point at which the pipeline begins executing the component.
        /// </summary>
        /// <param name="pdispOrder">A pointer to the OrderForm dictionary.</param>
        /// <param name="pdispContext">A pointer to the Context dictionary. This is a
        /// Dictionary object that contains pointers to the MessageManager object,
        /// Content object, and language information.</param>
        /// <param name="lFlags">Reserved</param>
        /// <returns></returns>
        public int Execute(object pdispOrder, object pdispContext, int lFlags)
        {
            int returnValue = 1; // OPPERRORLEV_SUCCESS
            IDictionary orderForm = (IDictionary)pdispOrder;
            IDictionary context = (IDictionary)pdispContext;
            orderForm["string_value"] = "foobar";
            return returnValue;
        }

        /// <summary>
        /// Returns a string array that identifies the pipeline context values that
        /// the component reads.
        /// </summary>
        /// <returns></returns>
        public object ContextValuesRead()
        {
            List<object> ReturnValue = new List<object>();
            ReturnValue.TrimExcess();
            return (object[])ReturnValue.ToArray();
        }

        /// <summary>
        /// Returns a string array that identifies the values that the component reads.
        /// </summary>
        /// <returns></returns>
        public object ValuesRead()
        {
            List<object> ReturnValue = new List<object>();
            ReturnValue.TrimExcess();
            return ReturnValue.ToArray();
        }

        /// <summary>
        /// Returns a string array that identifies the values that the component writes.
        /// </summary>
        /// <returns></returns>
        public object ValuesWritten()
        {
            List<object> ReturnValue = new List<object>();
            ReturnValue.Add("string_value");
            ReturnValue.TrimExcess();
            return ReturnValue.ToArray();
        }
        #endregion
    }
}

We'll talk about some non-essential interfaces and methods in a future post. The core method within the pipeline component is the Execute method -- this is where the real work is done. There are three parameters passed into the method:

  • pdispOrder - An IDictionary containing the order form
  • pdispContext - An IDictionary containing the pipeline context.
  • lFlags - Reserved value.

Navigating the context and order form collections is done as a Dictionary collection. Weakly typed properties are accessible using a string indexer property. Since the objects are passed by reference you simply need to access a property and change it's value for it to be stored and passed back to the pipeline and ultimately your application. Some entities in the dictionary, such as items, payments, and shipments, are accessed using a SimpleList. The SimpleList operates in the manner that a List collection would in .NET where objects are accessed with integer index values.

// Accessing an IDictionary
IDictionary orderForm = (IDictionary)pdispOrder;
orderForm["string_value"] = "foobar";
orderForm["_cy_currencyvalue"] = new CurrencyWrapper(1.23M);
orderForm["bool_value"] = false;

// Accessing an ISimpleList
ISimpleList items = (ISimpleList)orderForm["items"];
IDictionary item = (IDictionary)items[0];

When naming properties it is important to be aware that property named prefixed by an "_" will not be persisted to storage. Use of the temporary property prefix is common with properties that are calculated to ensure that while the value is in a basket form that it is always recalculated to ensure it is up-to-date. If you have ever recalled a basket and tried to access the sub-total before calling a basket refresh pipeline you will have had a null value returned because of this lack of persistence.

If you have extended your order class to access any newly created properties you will also need to update the OrderPipelineMapping.xml file that you will bundle with your application. This file instructs the serialization/deserialization process to map the OrderForm (and associated sub-classes) properties to the pipeline dictionaries. When deciding where to place various properties in your appication side classess it is important to note that you cannot add additional properties at the OrderGroup (Basket) level to serialize into the pipeline.

Testing Your Component

Because of the simplicity of types sent to the Execute method you can use your prefered test framework to unit test your component. You will need to instantiate an instance of your class and pass in the three parameters necessary for the execute method.

Debugging Your Component

When debugging your components you can attach to the process that is executing your pipeline and set break points. Because of the transition from .NET to COM if you are hoping to capture any exception information you should be aware that the entire stack trace inside a pipeline component is lost. As such you will want to employ a logging framework such as the Enterprise Library or Log4Net inside your component to capture exception information and rethrow the exception.

During staging and production usage you may not be able to attach to your process. In addition to logging through a framework you have several other options available:

  • Pipeline Logging - In your application configuration file you can set the loggingEnabled attribute to true and the pipeline system will write a pipelog file to the 'log' folder underneath your pipelines folder. Before enabling this logging make sure you have SECURED YOUR LOG FOLDER. IUSR should have a Deny Full Control access control entry. I have found Excel to be the best viewer for these files.
  • Dump Order - Insert a Scriptor component at any point in your pipeline and point it to the DumpOrder.vbs file in the SDK\DumpOrder folder in your Commerce Server installation directory. Using the parameters you can then point it to a file where the contents of the order form will be dumped.
  • XML Tracer - The tracer leverages the Event Tracing framework in the base operating system to dump the contents of the order form at any point. It operates in a similar manner to Dump Order but uses less overhead and dumps XML data.

The above methods are documented further in the Troubleshooting Pipelines section in the documentation.

Deploying Your Component

Once you have developed and tested your component you are ready to add it to a pipeline for further end-to-end pipeline testing. You will need to deploy the component to make it accesible for addition to a pipeline and your application. To do this you will need to perform two steps:

  1. Associate your pipeline component with one or more stages
  2. Register the COM Callable Wrapper

The easiest way to associate your pipeline component with the various stages is to use the Pipeline Component Registration (pipereg) tool located in the Tools folder of your Commerce Server installation folder. It provides a wizard interface to take the type library information from your assembly (either in the DLL or a separate TLB file) and allows you to match the categories/stages that you wish to enable your component to participate in. At the end of the wizard you can save a registry file or import it directly into the registry.

Before you use the component you still need to register the COM Callable Wrapper for the system to locate the assembly. Your assembly can be located in the GAC or anywhere on the disk. The RegAsm tool that ships with the .NET Framework will handle that for you. When registering an assembly to execute in a place other than the GAC (for example, while developing and debugging), remember to use the /codebase option to store the path to the assembly in the registry.

Ready for More?

The Commerce Server team have covered a number of topics regarding pipeline component development worth checking out:

In addition you will want to refer to the MinMaxShipping and OrdersExtensibility samples in the SDK.

Feedback

Did you find this article useful? I would like to know! Please leave a comment and let me know.

Comments Subscribe to Post Comments Feed

PaulTew said:
Was a big help developing my tax pipeline.  I found some more of your code elsewhere providing a sample tax calculation which pretty much made up what I have at the present time. Any news on the tax data table?
Hemant said:

Can i connect a workflow to pipeline?

Or invoke windows workflow after pipeline

Colin said:

Technically you could connect it to Windows Workflow.  We're in the midst of dumping the pipeline system in favour of Windows Workflow.  The debugging of a fully managed system alone makes it easier.  From what I understand we will likely see full WF implementation in the next Commerce Server release.  I suspect that won't see the light of day though for another 24 months.

Have Your Say