Building an Order Pipeline Component

As I spoke about in an earlier post, Commerce Server 2007 leverages a COM-based pipeline system to perform a series of operations on an order. The logic in the pipeline is controled, for the most part, by individual pipeline components.  Each component pefroms a specific operation on the order form and then returns a status to control flow through the rest of the pipeline.

If you have used previous releases of Commerce Server you will soon realize that the out-of-the-box sample tax component is missing.  I suspect that the team decided not to include it to entice people to write their own tax component which deals with regional tax complexities.  In this post we will build a sample tax component which will calculate and store the tax total as part of a pipeline component library starter kit.

Before we dig into the code let's look at the core business logic in psuedo code:

    OrderForm.TaxTotal = 0
    For Each LineItem in OrderForm.LineItems
        TaxRate = RetrieveTaxRate(OrderAddresses.RegionCode, OrderAddresses.CountryCode)
        LineItem.TaxTotal += LineItem.ExtendedPrice * TaxRate
        OrderForm.TaxTotal += LineItem.TaxTotal
    Next

Seems pretty simple?  That's because it is!  Now the implementation isn't as straightforward because we're left with an untyped list and dictionary object collection which contains all of the data for our order. To make manipulation easier our first step is to build a few wrapper classes for those lists and the objects which they contain. In the attached starter kit you will find a few key classes:

  • SimpleList - A wrapper for the ISimpleList-based lists which exposes a series of methods that are familiar to users of the List<T> generic list class.
  • SimpleListContainer - A wrapper for the ISimpleList-based lists which can be used to expose other strongly-typed wrappers of the objects inside the list.
  • Dictionary - A wrapper for the IDictionary-based collections which exposes a series of methods that are familiar to users of the Dictionary classes.
  • DictionaryContainer - A wrapper for the IDictionary-based collections which can be used to expose other strongly-typed wrappers of the objects inside the list.

Using these containers we are able to wrap all IDictionary and ISimpleList containers quite easily (and even expose nullable types!):

public SimpleList<string> BasketErrors
{
    get
    {
        if (this.basketErrors == null)
        {
            if (this[OrderForm.KeyNames.BasketErrors] == null)
            {
                this[OrderForm.KeyNames.BasketErrors] = new SimpleListClass();
            }
            this.basketErrors = new SimpleList<string>((ISimpleList)this[OrderForm.KeyNames.BasketErrors], StringComparer.InvariantCultureIgnoreCase);
        }
        return this.basketErrors;
    }
}
 
public DictionaryContainer<OrderAddress, IDictionary> Addresses
{
    get
    {
        if (this.addresses == null)
        {
            if (this[OrderForm.KeyNames.Addresses] == null)
            {
                this[OrderForm.KeyNames.Addresses] = new DictionaryClass();
            }
            this.addresses = new DictionaryContainer<OrderAddress, IDictionary>((IDictionary)this[OrderForm.KeyNames.Addresses]);
        }
        return this.addresses;
    }
}
 
public SimpleListContainer<LineItem, IDictionary> LineItems
{
    get
    {
        if (this.lineItems == null)
        {
            if (this[OrderForm.KeyNames.LineItems] == null)
            {
                this[OrderForm.KeyNames.LineItems] = new SimpleListClass();
            }
            this.lineItems = new SimpleListContainer<LineItem, IDictionary>((ISimpleList)this[OrderForm.KeyNames.LineItems]);
        }
        return this.lineItems;
    }
}
 
public Guid OrderGroupId
{
    get { return GetGuidFromStringValue(OrderForm.KeyNames.OrderGroupId).Value; }
    set { this[OrderForm.KeyNames.OrderGroupId] = value.ToString(); }
}
 
public int CurrencyDecimalPlaces
{
    get { return GetValue<int>(OrderForm.KeyNames.CurrencyDecimalPlaces); }
    set { this[OrderForm.KeyNames.CurrencyDecimalPlaces] = value; }
}
 
public string TrackingNumber
{
    get { return this[OrderForm.KeyNames.TrackingNumber] as string; }
    set { this[OrderForm.KeyNames.TrackingNumber] = value; }
}
 
public decimal? SubTotal
{
    get { return GetValue<decimal?>(OrderForm.KeyNames.SubTotal); }
    set { this[OrderForm.KeyNames.SubTotal] = (value.HasValue ? new CurrencyWrapper(value.Value) : null); }
}

There is an opportunity here to use the OrderPipelineMapping.xml and a code generator to build these automatically, but that is out of scope for this post. I have included the series of objects as defined by the Starter Site to give you a full array of property type examples.

With a foundation in place it's time to write our Execute method:

ComponentErrorLevel componentErrorLevel = ComponentErrorLevel.Success;
Dictionary<string, object> logContext = new Dictionary<string, object>();
OrderForm orderForm = new OrderForm((IDictionary)pdispOrder);
PipelineContext pipelineContext = new PipelineContext((IDictionary)pdispContext);
 
try
{
    logContext["OrderGroupId"] = orderForm.OrderGroupId;
    logContext["SoldToId"] = orderForm.SoldToId;
 
    orderForm.TaxIncluded = decimal.Zero;
    orderForm.TaxTotal = decimal.Zero;
 
    foreach (LineItem lineItem in orderForm.LineItems)
    {
        logContext["LineItemId"] = lineItem.LineItemId;
 
        OrderAddress shippingAddress = orderForm.Addresses[lineItem.ShippingAddressId];
 
        decimal taxRate = RetrieveTaxRate(pipelineContext.CommerceResources.TransactionConfigSqlConnectionString,
                                          shippingAddress.CountryCode,
                                          shippingAddress.RegionCode);
 
        logContext["CountryCode"] = shippingAddress.CountryCode;
        logContext["RegionCode"] = shippingAddress.RegionCode;
        logContext["TaxRate"] = taxRate;
        logContext["ExtendedPrice"] = lineItem.ExtendedPrice;
 
        lineItem.TaxIncluded = decimal.Zero;
        lineItem.TaxTotal = Helper.Round(orderForm.CurrencyDecimalPlaces,
                                         lineItem.ExtendedPrice.GetValueOrDefault() * taxRate);
 
        logContext["TaxTotal"] = lineItem.TaxTotal;
 
        orderForm.TaxTotal += lineItem.TaxTotal;
 
        Helper.Log(EventIdentifiers.CalculateTaxApplyingLineItemTax,
                   TraceEventType.Verbose,
                   logContext,
                   CalculateTax.ComponentCategory,
                   StringResources.ApplyingLineItemTax);
 
        logContext.Remove("TaxTotal");
        logContext.Remove("ExtendedPrice");
        logContext.Remove("TaxRate");
        logContext.Remove("RegionCode");
        logContext.Remove("CountryCode");
        logContext.Remove("LineItemId");
    }
 
    foreach (Shipment shipment in orderForm.Shipments)
    {
        logContext["ShipmentId"] = shipment.ShipmentId;
 
        OrderAddress shippingAddress = orderForm.Addresses[shipment.ShippingAddressId];
        decimal taxRate = RetrieveTaxRate(pipelineContext.CommerceResources.TransactionConfigSqlConnectionString,
                                          shippingAddress.CountryCode,
                                          shippingAddress.RegionCode);
 
        logContext["CountryCode"] = shippingAddress.CountryCode;
        logContext["RegionCode"] = shippingAddress.RegionCode;
        logContext["TaxRate"] = taxRate;
        logContext["ShippingTotal"] = shipment.ShippingTotal;
 
        shipment.TaxIncluded = decimal.Zero;
        shipment.TaxTotal = Helper.Round(orderForm.CurrencyDecimalPlaces,
                                         shipment.ShippingTotal * taxRate);
 
        logContext["TaxTotal"] = shipment.TaxTotal;
 
        orderForm.TaxTotal += shipment.TaxTotal;
 
        Helper.Log(EventIdentifiers.CalculateTaxApplyingShipmentTax,
                   TraceEventType.Verbose,
                   logContext,
                   CalculateTax.ComponentCategory,
                   StringResources.ApplyingShipmentTax);
 
        logContext.Remove("TaxTotal");
        logContext.Remove("ShippingTotal");
        logContext.Remove("TaxRate");
        logContext.Remove("RegionCode");
        logContext.Remove("CountryCode");
        logContext.Remove("ShipmentId");
    }
}
catch (Exception exception)
{
    Helper.Log(EventIdentifiers.CalculateTaxUnhandledException,
               TraceEventType.Error,
               logContext,
               CalculateTax.ComponentCategory,
               exception);
    throw;
}
 
return (int)componentErrorLevel;

There are a few practices to note here:

  1. Resetting values - We always start by setting the existing total to zero.  Even though the property isn't a persistent property at the basket level (designated by the "_" prefix), this will help us ensure we aren't adding any unnecessary charges.
  2. Catch and log general exceptions - While you typically don't want to catch a general exception, because the pipeline system structure data goes from .NET to COM to .NET you lose certain properties in exceptions that do occur (e.g. call stack).  Instead of losing that data we'll log it and rethrow it. For those who are wondering why we don't return a Failure error level David Messner blogged that it's a better practice to throw exceptions when in .NET.
  3. Data access methods are encapsulated in their own methods - Tax rate retrieval is contained inside a separate method.  While we pull the value from the TransactionConfig resource, you can design it to pull from other sources such as third-party tax services if you wish. The separate method also keeps the messy details of data retrieval out of our business logic. 
  4. Readable variable names - An important part to any development is leaving your code clean and readable for other developers to maintain.  With everything being boiled down to IL and small machine readable identifiers there is no loss in spending a few extra characters to make your code readable.

Before we wrap up we'll take this opportunity to go a bit further with our example.  You may have a need to exclude certain products from taxation.  To facilitate this we'll add a check for a property indicating that the product is excluded from taxation.  To make it flexibile as to where that property value comes from (e.g. product catalog, user profile) we'll make it configurable as well.  With this change in place our core Execute logic looks as follows:

  1. Retrieve exemption information at multiple levels.
  2. Send the exemption status with the database call so it can exclude any unnecessary data in its rate retrieval process.

Our main execute function now looks like this:

logContext["OrderGroupId"] = orderForm.OrderGroupId;
logContext["SoldToId"] = orderForm.SoldToId;
 
orderForm.TaxIncluded = decimal.Zero;
orderForm.TaxTotal = decimal.Zero;
 
bool countryTaxExempt = false;
bool regionTaxExempt = false;
 
if ((EnableTaxExemptionRules) &&
    (CountryTaxExemptionKeySource == KeySources.OrderForm))
{
    countryTaxExempt = orderForm.GetValue<bool>(CountryTaxExemptionKey);
}
if ((EnableTaxExemptionRules) &&
    (RegionTaxExemptionKeySource == KeySources.OrderForm))
{
    regionTaxExempt = orderForm.GetValue<bool>(RegionTaxExemptionKey);
}
 
foreach (LineItem lineItem in orderForm.LineItems)
{
    logContext["LineItemId"] = lineItem.LineItemId;
 
    OrderAddress shippingAddress = orderForm.Addresses[lineItem.ShippingAddressId];
 
    if ((EnableTaxExemptionRules) &&
        (CountryTaxExemptionKeySource == KeySources.LineItem))
    {
        countryTaxExempt = lineItem.GetValue<bool>(CountryTaxExemptionKey);
    }
    if ((EnableTaxExemptionRules) &&
        (RegionTaxExemptionKeySource == KeySources.LineItem))
    {
        regionTaxExempt = lineItem.GetValue<bool>(RegionTaxExemptionKey);
    }
 
    decimal taxRate = RetrieveTaxRate(pipelineContext.CommerceResources.TransactionConfigSqlConnectionString,
                                      shippingAddress.CountryCode,
                                      countryTaxExempt,
                                      shippingAddress.RegionCode,
                                      regionTaxExempt);
 
    logContext["CountryCode"] = shippingAddress.CountryCode;
    logContext["CountryTaxExempt"] = countryTaxExempt;
    logContext["RegionCode"] = shippingAddress.RegionCode;
    logContext["RegionTaxExempt"] = regionTaxExempt;
    logContext["TaxRate"] = taxRate;
    logContext["ExtendedPrice"] = lineItem.ExtendedPrice;
 
    lineItem.TaxIncluded = decimal.Zero;
    lineItem.TaxTotal = Helper.Round(orderForm.CurrencyDecimalPlaces,
                                     lineItem.ExtendedPrice.GetValueOrDefault() * taxRate);
 
    logContext["TaxTotal"] = lineItem.TaxTotal;
 
    orderForm.TaxTotal += lineItem.TaxTotal;
 
    Helper.Log(EventIdentifiers.CalculateTaxApplyingLineItemTax,
               TraceEventType.Verbose,
               logContext,
               CalculateTax.ComponentCategory,
               StringResources.ApplyingLineItemTax);
 
    logContext.Remove("TaxTotal");
    logContext.Remove("ExtendedPrice");
    logContext.Remove("TaxRate");
    logContext.Remove("RegionTaxExempt");
    logContext.Remove("RegionCode");
    logContext.Remove("CountryTaxExempt");
    logContext.Remove("CountryCode");
    logContext.Remove("LineItemId");
}
 
if ((EnableTaxExemptionRules) &&
    (CountryTaxExemptionKeySource == KeySources.LineItem))
{
    countryTaxExempt = false;
}
if ((EnableTaxExemptionRules) &&
    (RegionTaxExemptionKeySource == KeySources.LineItem))
{
    regionTaxExempt = false;
}
 
foreach (Shipment shipment in orderForm.Shipments)
{
    logContext["ShipmentId"] = shipment.ShipmentId;
 
    OrderAddress shippingAddress = orderForm.Addresses[shipment.ShippingAddressId];
    decimal taxRate = RetrieveTaxRate(pipelineContext.CommerceResources.TransactionConfigSqlConnectionString,
                                      shippingAddress.CountryCode,
                                      countryTaxExempt,
                                      shippingAddress.RegionCode,
                                      regionTaxExempt);
 
    logContext["CountryCode"] = shippingAddress.CountryCode;
    logContext["CountryTaxExempt"] = countryTaxExempt;
    logContext["RegionCode"] = shippingAddress.RegionCode;
    logContext["RegionTaxExempt"] = regionTaxExempt;
    logContext["TaxRate"] = taxRate;
    logContext["ShippingTotal"] = shipment.ShippingTotal;
 
    shipment.TaxIncluded = decimal.Zero;
    shipment.TaxTotal = Helper.Round(orderForm.CurrencyDecimalPlaces,
                                     shipment.ShippingTotal * taxRate);
 
    logContext["TaxTotal"] = shipment.TaxTotal;
 
    orderForm.TaxTotal += shipment.TaxTotal;
 
    Helper.Log(EventIdentifiers.CalculateTaxApplyingShipmentTax,
               TraceEventType.Verbose,
               logContext,
               CalculateTax.ComponentCategory,
               StringResources.ApplyingShipmentTax);
 
    logContext.Remove("TaxTotal");
    logContext.Remove("ShippingTotal");
    logContext.Remove("TaxRate");
    logContext.Remove("RegionTaxExempt");
    logContext.Remove("RegionCode");
    logContext.Remove("CountryTaxExempt");
    logContext.Remove("CountryCode");
    logContext.Remove("ShipmentId");
}

To expose the configuration we'll use the IPipelineComponentAdmin, IPipelineComponentUI, ISpecifyPipelineComponentUI, and IPersistDictionary interfaces to build a dialog box for configuration inside the pipeline editor:

 

The key methods for loading and saving data is the Load and Save methods:

public void Load(object pdispDict)
{
    CalculateTaxConfiguration configuration = new CalculateTaxConfiguration((IDictionary)pdispDict);
 
    this.enableTaxExemptionRules = configuration.EnableTaxExemptionRules;
    this.countryTaxExemptionKey = configuration.CountryTaxExemptionKey;
    this.countryTaxExemptionKeySource = configuration.CountryTaxExemptionKeySource;
    this.regionTaxExemptionKey = configuration.RegionTaxExemptionKey;
    this.regionTaxExemptionKeySource = configuration.RegionTaxExemptionKeySource;
 
    this.isDirty = false;
}
 
public void Save(object pdispDict, int fSameAsLoad)
{
    CalculateTaxConfiguration configuration = new CalculateTaxConfiguration((IDictionary)pdispDict);
 
    configuration.EnableTaxExemptionRules = this.enableTaxExemptionRules;
    configuration.CountryTaxExemptionKey = this.countryTaxExemptionKey;
    configuration.CountryTaxExemptionKeySource = this.countryTaxExemptionKeySource;
    configuration.RegionTaxExemptionKey = this.regionTaxExemptionKey;
    configuration.RegionTaxExemptionKeySource = this.regionTaxExemptionKeySource;
 
    this.isDirty = false;
}

The pipeline editor calls these methods with a IDictionary that will be streamed and saved to the pipeline configuration file (PCF).  When the editor invokes your dialog you'll be passed the instance of the component from which you can retrieve configuration values:

public void ShowProperties(object pdispComponent)
{
    CalculateTax calculateTax = (CalculateTax)pdispComponent;
 
    this.EnableTaxExemptionRules.Checked = calculateTax.EnableTaxExemptionRules;
    this.CountryTaxExemptionKey.Text = calculateTax.CountryTaxExemptionKey;
    this.CountryTaxExemptionKeySource.Text = calculateTax.CountryTaxExemptionKeySource.ToString();
    this.RegionTaxExemptionKey.Text = calculateTax.RegionTaxExemptionKey;
    this.RegionTaxExemptionKeySource.Text = calculateTax.RegionTaxExemptionKeySource.ToString();
 
    EnableTaxExemptionRules_CheckedChanged(null, EventArgs.Empty);
 
    DialogResult result = this.ShowDialog();
    if (result == DialogResult.OK)
    {
        calculateTax.EnableTaxExemptionRules = this.EnableTaxExemptionRules.Checked;
        calculateTax.CountryTaxExemptionKey = this.CountryTaxExemptionKey.Text;
        calculateTax.CountryTaxExemptionKeySource = (CalculateTax.KeySources)Enum.Parse(typeof(CalculateTax.KeySources), this.CountryTaxExemptionKeySource.Text, true);
        calculateTax.RegionTaxExemptionKey = this.RegionTaxExemptionKey.Text;
        calculateTax.RegionTaxExemptionKeySource = (CalculateTax.KeySources)Enum.Parse(typeof(CalculateTax.KeySources), this.RegionTaxExemptionKeySource.Text, true);
    }
}

One thing to note is that if an exception is thrown that you may not see it in the pipeline editor. If your dialog is behaving in a suspectable manner attach to the process using Visual Studio and watch the Output window for exceptions being thrown.

When the pipeline is executed the pipeline system will initialize your component and pass in the dictionary with the configuration values stored in the PCF. It does remind me that it would be great to see documentation at some point of the event life cycle for a pipeline component.

Now that the component is complete I added one last bit of functionality to make deployment easier - a pipeline component category attribute. This attribute does away with the need for the pipeline component registration wizard by allowing you to decorate your class with the category/stage identifiers that your component applies to. Then you can use InstallUtil to install/uninstall the registry entries assocaited with it:

[ComVisible(true), Description("RSG.CalculateTax"), ProgId("RSG.CalculateTax")]
[Guid("01f7bda9-9cb0-458d-95f4-7ac4ca33c4fc")]
[PipelineCategory(PipelineCategories.CommerceServer, PipelineCategories.AllStages,
                  PipelineCategories.Tax)]
public class CalculateTax : IPersistDictionary, IPipelineComponent,
                            IPipelineComponentAdmin, IPipelineComponentDescription,
                            ISpecifyPipelineComponentUI
{

Finally there are 378 unit tests that cover just over half of the code in the library.  Because I am working with COM objects that aren't fully documented in addition to the potential for type issues I wanted to cover some critical bits of code to ensure they worked as I expected. I'm proud of the code that executes the pipeline components and the entire pipeline itself because it makes it easy to ensure your logic will work as expected. Thanks to Nihit Kaul for a starting point on the pipeline side.  I have made it a bit more dynamic than his project so you can rely on the app.config sections which you would normally see in your web project to ensure the pipeline functions in a similar manner.  I also managed to expose some short comings of testing with VSTS (no support for satellite assemblies) and Commerce Server under Vista (pipeline tests don't run as a normal user, you need to elevate to admin, but that's okay because it's technically not supported until SP1 is released).

Thanks to Andy Miller for pointing this out - To get this all working you will need a copy of the Enterprise Library 2.0 (I have tested with the 2554 patch). That being said, if log4net and some other data access layer is your cup of tea the functions are pretty isolated inside the Helper.cs and CalculateTax.cs components.  You should be able to replace the code with little problem.

After weeks of work when ever I found time please find attached the 17,000+ line starter kit for building a pipeline component library. I hope you find it useful and I appreciate any and all feedback you have.

Comments Subscribe to Post Comments Feed

Andy Miller said:
Colin,

This is absolutely wonderful. You really are a rock star.

You used IPipelineComponentAdmin, IPipelineComponentUI, ISpecifyPipelineComponentUI, and IPersistDictionary to edit custom properties. What would you use for a shipping method component? Shipping method components are normally run by the router and do not appear separately in the pipeline. So the pipeline editor is not available.

I'm thinking of writing my own configuration editor using site resources similar to the way the pipeline editor uses the PCF file. That is, my editor would serialize the custom values into a single site resource for each shipping method. Then during runtime I would deserialize the configuration.
Colin said:
If you are building a shipping rate calculation component to be run by the router you can store your settings in the shipping method table beside each entry, or in an independent table.

From there you can likely override the orders web service to extend the ShippingMethodDataSet to populate the data (or create a separate data set retrieval method) and the Customer and Orders Manager UI to manage that data.  It's a bit more involved than the pipeline method, but I suspect we'll see a lot more clarity in the next major CS release.  I suspect we'll see pipelines move to Windows Workflow since every other workflow technology in MS is moving in that direction.

A good idea for an example that someone should write is a UPS/Fedex/Purolator/USPS shipping rate calculation component using their web services.
Andy Miller said:
The sample you supply include a very gracious license that includes the phrasing "Permission is hereby granted, free of charge, ... to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so".

It is clear that if I merge substantial portions of your Software into my product that I should include the notice. Does it also mean that I need to permit my customers to sell my product or give it away free of charge?

I plan on writing and providing an example similar to yours using one of the commercial carrier rate services. However, I also plan on porting a for-pay product I sell for another ecommerce platform. I'm unclear on how or if I could use your code in my for-pay product.

amiller at structured dash solutions dot net
Paul Tew said:
Thanks Colin this has been a huge help.
Max Akbar said:
Dude you rock!
Colin Bowern said:
Hey Andy..  No, you don't need to permit your customers to sell your product or give it away.  It just says if you include a substantial portion of the work inside yours that a mention would be appreciated! :)
OC said:
Hello Colin, this is a great article and while i am working on a similar pipeline component I noticed that you do not account for a small detail. Depending on the type of product you are sellin, some will be taxable and some wont, furthermore some will be tazable in some states and not in others.
I had been toying with the idea of storing an IsTaxable key in the Catalog Manager; This would work if the item had the same tax rules acrsso the planet. Even if this was the case, i do not see a way to get Product information in the Total pipeline.
Any thoughts on how to accomplish this?
Minah said:

Hi Collin,

 I have added a base property VAT(value added tax) using Catalog and Inventory schema manager, which I should add or subtract according to shipping address selected(country). So what should I do. I know how to add a component. But tell me where should i add the component in Total pipeline(i think i should add it here). what should be the code in execute(). And wether I can get the value of VAT in the pipeline

  In your code samples above

  1. How you are using "OrderForm orderForm = new OrderForm((IDictionary)pdispOrder); " Its giving me an error saying the constructor accepts only string.

  2. How you are using "OrderAddress shippingAddress = orderForm.Addresses[lineItem.ShippingAddressId];" when its not at all showing Addresses[""] after typing "orderFrom."

And how I will get the property "VAT" in similar way.

Please help me, and thanks in advance.

Colin said:

OC - It really depends on your tax situation.  We use a tax schedule which takes the IsTaxable flag to another level by factoring in the customer's location as well.

Minah - The OrderForm class is a custom wrapper class.  It is likely you are missing a support file from my sample.  Likewise for the OrderAddress class.

Marc said:

Hello Colin!

Thanks for this post... I tried to download your PipelineComponentsStarterKit.zip file, but winzip says it's not a valid archive? could you please check it for me? thanks a lot.

Marc

Colin said:

Marc - Works fine from here, try re-downloading it.

Marc said:

Thanks Colin for your reply...

In IE 7, it does not work, but in Firefox yes... Funny one...

Colin said:

Interesting.. I used IE and it worked like a charm.  Perhaps a Community Server bug.

Ken said:

Hi Colin,

I've been trying to get this component to work on my site, but I've had no success. I'm utilizing the starter site and attempting to plug your component into the 'total' pipeline. When I go to checkout, I receive the following error:

Component Execution failed for component[0x6]  hr: 0x80004003

ProgID: RSG.CalculateTax

Object reference not set to an instance of an object...

in baskethelper.cs.  I realize something isn't being created, but I'm having no luck in debugging this portion either.

Any thoughts on where to go from here?

Thanks, Ken

Aaron said:

Hi Colon,

Forgive me if this is a stupid quesiton, but I keep getting the following error after trying to execute a RunTotalPipeline after squeezing your excellent CalculateTax component into total.pcf:

The configuration section for Logging cannot be found in the configuration source.

Any thoughts on why this could be happening?

Thanks, Aaron

Have Your Say