Commerce Server 2009 OperationSequenceComponent Extensibility, Part 2

In my previous post, I discussed using the Commerce Foundation’s extensibilty model to create a basic pricing engine.  I created a simple OperationSequenceComponent that I configure to run whenever a query for a product comes through the foundation.

While this was simple to do and worked great, as Brian pointed out in the comments, it doesn’t completely solve the problem.  As you can see, once we add that item to the basket, then the pricing reverts back to the original pricing.

image

The reason that this occurs is that basket calculations are not done in the Commerce Foundation, but are done down in the legacy COM-based pipelines.  The legacy pipelines actually query for product information directly to the database.  It does not go through the Commerce Foundation.  This means that are extension that we built never gets hit.

Lets open up the pipelin editor, which is in the tools folder and then navigate to the basket.pcf, which contains the sequence of components to run when the basket pipeline is run.

image

Fortunately for us, we know that it is the “QueryCatalogInfo” pipeline component, which is the first component that runs, that retrieves the basic information for a product and pushes it into the context for later use by other pipeline components.

So it would be nice to know what this component does.  The easiest way to do that is to turn on logging.  Pipelines can log everything read or written to the context, so it’s relatively easy to turn on logging and look at what the various components do.  To turn on logging, you go to the web.config and find the entry in the <pipelines> section and change logging to “true”.

pipeline name="basket" path="pipelines\basket.pcf" transacted="false" type="OrderPipeline" loggingEnabled="true" />

The basket pipeline runs every time you load the basket page, so it’s easy to see what happens by refreshing the basket page.  There should be a new folder underneath the Pipelines folder in your virtual directory which has file with an extension “.pipelog”  Open the “basket.pipelog” up in notepad and you should see a log of everything that the pipeline read or wrote during it’s execution.  The first pipeline component that executed is what we are interested in and so we can look at that:

PIPELINE:++ component[0x0] about to be called ProgID: Commerce.QueryCatalogInfo RootObject:    ReadValue    _Basket_Errors    VT_DISPATCH    PV=[0xc20e9d8]    VT_EMPTY    __empty__    RootObject:    ReadValue    Items    VT_DISPATCH    PV=[0xc20ead8]    VT_EMPTY    __empty__    RootObject:    ReadValue    catalog_language    VT_NULL    __null__    VT_EMPTY    __empty__    items:    ReadItem    0    VT_DISPATCH    PV=[0xf27ce48]    VT_EMPTY    __empty__    :    ReadValue    product_catalog    VT_BSTR    Adventure Works Catalog    VT_EMPTY    __empty__    :    ReadValue    product_id    VT_BSTR    AW200-12    VT_EMPTY    __empty__    :    ReadValue    product_variant_id    VT_BSTR    3    VT_EMPTY    __empty__    :    ReadValue    catalog_language    VT_NULL    __null__    VT_EMPTY    __empty__    items:    ReadItem    0    VT_DISPATCH    PV=[0xf27ce48]    VT_EMPTY    __empty__    :    WriteValue    _product_#QCI_LineItem    VT_EMPTY    __empty__    VT_I4    0    :    WriteValue    _product_BaseCatalogName    VT_EMPTY    __empty__    VT_BSTR    Adventure Works Catalog    :    WriteValue    _product_OrigProductID    VT_EMPTY    __empty__    VT_BSTR    AW200-12    :    WriteValue    _product_OrigVariantID    VT_EMPTY    __empty__    VT_BSTR    3    :    WriteValue    _product_cy_list_price    VT_EMPTY    __empty__    VT_CY    200    :    ReadValue    _product_cy_list_price    VT_CY    200    VT_EMPTY    __empty__    :    WriteValue    _product_UseCategoryPricing    VT_EMPTY    __empty__    VT_BOOL    0    :    WriteValue    _product_OriginalPrice    VT_EMPTY    __empty__    VT_CY    200    :    ReadValue    _product_OriginalPrice    VT_CY    200    VT_EMPTY    __empty__    :    WriteValue    _product_i_ClassType    VT_EMPTY    __empty__    VT_I4    2    :    WriteValue    _product_ParentOID    VT_EMPTY    __empty__    VT_I4    127    :    WriteValue    _product_ProductID    VT_EMPTY    __empty__    VT_BSTR    AW200-12    :    WriteValue    _product_VariantID    VT_EMPTY    __empty__    VT_BSTR    3    :    WriteValue    _product_LastModified    VT_EMPTY    __empty__    VT_DATE    5/11/2010 2:42:22 PM    :    WriteValue    _product_CatalogName    VT_EMPTY    __empty__    VT_BSTR    Adventure Works Catalog    :    WriteValue    _product_ExportReady    VT_EMPTY    __empty__    VT_BOOL    -1    :    WriteValue    _product_DefinitionName    VT_EMPTY    __empty__    VT_BSTR    SleepingBag    :    WriteValue    _product_ProductCode    VT_EMPTY    __empty__    VT_BSTR    AW200-12    :    WriteValue    _product_VariantCode    VT_EMPTY    __empty__    VT_I4    3    :    WriteValue    _product_Image_filename    VT_EMPTY    __empty__    VT_BSTR    sleepingbags03.png    :    WriteValue    _product_Image_height    VT_EMPTY    __empty__    VT_I4    120    :    WriteValue    _product_Image_width    VT_EMPTY    __empty__    VT_I4    120    :    WriteValue    _product_IntroductionDate    VT_EMPTY    __empty__    VT_DATE    4/19/2006 12:35:02 PM    :    WriteValue    _product_OnSale    VT_EMPTY    __empty__    VT_BOOL    0    :    WriteValue    _product_DisplayName    VT_BSTR    Big Sur (Blue)    VT_BSTR    Big Sur (Blue)    :    WriteValue    _product_Description    VT_EMPTY    __empty__    VT_BSTR    Generously cut sleeping bag, goose down with polyster taffeta, cotton storage sack included.    :    WriteValue    _product_Name    VT_EMPTY    __empty__    VT_BSTR    Big Sur    :    WriteValue    _product_Rating    VT_EMPTY    __empty__    VT_BSTR    3.5    :    WriteValue    _product_ProductColor    VT_EMPTY    __empty__    VT_BSTR    Blue    :    WriteValue    _product_categories    VT_EMPTY    __empty__    VT_VARIANT | VT_ARRAY   

This gives us a good idea of what this “QueryCatalogInfo” component does.  Now we have a couple of choices here.

1) We can create another com-based pipeline component that we put right after the "QueryCatalogInfo” component that adjusts the price per our business rules

2) We remove this legacy component and replace it with an OperationSequenceComponent that does the same thing.

If I want to simply stick another COM component in there then I end up having to run my rules-engine with any caching etc in two places.  I would much rather simply change the way the pipeline works so that it retrieves the product information using the Commerce Foundation, executing my pricing extension in the same way it does when I retrieve a product for display.

The other thing that you might notice is that it writes a lot of stuff into the context that you really don’t need because no other pipeline component uses them.  We can probably make something that is more simplified and only passes in information that we need.

What you might notice in looking at the above list of values read and written is that there are certain values that the pipeline assumes are already there.  For example, it assumes that there is an “Items” entry and it assumes that for each item in the items dictionary there is a productid, variantid, catalog,, catalog-language. 

So if those entries are already there, how did they get there?  The answer is that there is certain information pushed into the context by the Commerce Foundation before a pipeline is even run.  In our case the helper method for the basket pipeline knows that some elements of the orderform need to be in the pipeline context before it is run and it helpfully (hence the name…) copies them over for us.

Now if it loaded the orderform and directly copied the values into the context in the same OperationSequenceComponent, then we would have a very hard time extending it.  Luckily for us, this was something that was considered and it was broken into several operations.

If you open up the “channelconfiguration.config” file and search for “QueryOperation_basket” you will see the following entries:

<Component name="Basket Loader" type="Microsoft.Commerce.Providers.Components.BasketQueryProcessor, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35" />

          <Component name="Order Pipelines Processor" type="Microsoft.Commerce.Providers.Components.OrderPipelinesProcessor, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35">
<Configuration customElementName="OrderPipelinesProcessorConfiguration" customElementType="Microsoft.Commerce.Providers.Components.

OrderPipelinesProcessorConfiguration, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35">
<OrderPipelinesProcessorConfiguration>
<!--
The name attribute should contain one of the Order pipeline names defined in the pipelines section of the CommerceServer site configuration.
The type attribute should contain one of the values of the Microsoft.CommerceServer.Runtime.Orders.OrderPipelineType enumeration:
Custom Indicates a custom pipeline type.
Product Indicates a Product pipeline (e.g. Product.pcf).
Basket Indicates a Basket pipeline (e.g. Basket.pcf).
Total Indicates a Total pipeline (e.g. Total.pcf).
Checkout Indicates a Checkout pipeline (e.g. Checkout.pcf).
AcceptBasket Indicates an AcceptBasket pipeline (e.g. AcceptBasket.pcf).
-->
<OrderPipelines>
<Pipeline name="basket" type="Basket" />
<Pipeline name="total" type="Total" />
</OrderPipelines>
</OrderPipelinesProcessorConfiguration>
</Configuration>
</Component>

You see that there is a “Basket Loader” component that runs first.  This loads the basket and places it in the operationCache.  Later the “Order PipelinesProcessor” runs and this takes that cached order and writes it into the context.

This is great because we have the ability to inject our own logic between those two entries that modifies the basket object in the operationCache and adds our additional information to the basket line items.

So we can write a new OperationSequenceComponent that looks like this:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Commerce.Providers.Components; //using Microsoft.Commerce.Server; using Microsoft.CommerceServer.Runtime.Orders;

using Microsoft.Commerce.Contracts.Messages; using Microsoft.Commerce.Broker; using Microsoft.Commerce.Common.MessageBuilders; using Microsoft.Commerce.Contracts;

namespace Microsoft.Commerce.Samples.Pipelines { public class QueryCatalogInfo : OperationSequenceComponent { private static void UpdateCatalogInfo(OperationCacheDictionary operationCache) {

            var ordersGroup = (Dictionary<string, OrderGroup>)operationCache["OrderGroup_071CA882-C773-4ca8-AF95-188D10492B85"];

            foreach (KeyValuePair<string, OrderGroup> orderGroup in ordersGroup) { var orderForm = orderGroup.Value.OrderForms[0];

                var lineItems = orderForm.LineItems;

                foreach (LineItem lineItem in lineItems) { var productId = lineItem.ProductId; //string variantId = lineItem.ProductVariantId; var catalogId = lineItem.ProductCatalog;

                    var productQuery = new CommerceQuery<CommerceEntity>("Product");

                    // Set the search criteria to get the product desired productQuery.SearchCriteria.Model.Properties["CatalogId"] = catalogId; productQuery.SearchCriteria.Model.Id = productId;

                    // Add Related Query Operation for Variants { var queryVariants = new CommerceQueryRelatedItem<CommerceEntity>("Variants"); productQuery.RelatedOperations.Add(queryVariants); }

                    var request = productQuery.ToRequest(); var response2 = OperationService.InternalProcessRequest(request); var commerceOperationResponse = (CommerceQueryOperationResponse)response2.OperationResponses[0];

                    var commerceEntity = commerceOperationResponse.CommerceEntities[0];

                    lineItem["_product_cy_list_price"] = System.Convert.ToDecimal(commerceEntity.Properties["ListPrice"]); lineItem["_product_OriginalPrice"] = System.Convert.ToDecimal(commerceEntity.Properties["ListPrice"]); lineItem["_product_salePrice"] = System.Convert.ToDecimal(commerceEntity.Properties["salePrice"]); lineItem.DisplayName = commerceEntity.Properties["DisplayName"] as string; if (commerceEntity.Properties.Contains("ShippingCost")) { lineItem["_product_ShippingCost"] =

System.Convert.ToDecimal(commerceEntity.Properties["ShippingCost"])  } lineItem["_product_Image_filename"] = commerceEntity.Properties["Image_filename"];

                } } }

        public override void ExecuteQuery(CommerceQueryOperation queryOperation, OperationCacheDictionary operationCache, CommerceQueryOperationResponse response) { UpdateCatalogInfo(operationCache); }

        public override void ExecuteUpdate(CommerceUpdateOperation updateOperation, OperationCacheDictionary operationCache, CommerceUpdateOperationResponse response) { UpdateCatalogInfo(operationCache); }

    } }

What this component does is retrieve the basket from the operationCache, walk through each of it’s order forms and each line and calls out to the foundation to retrieve data for each item.  This will then execute the “QueryOperation_Product” which will execute our custom pricing rule.  The retrieved data is then copied into the order forms lineItem dictionary.

This means that when the basket is copied into the context later on in the component sequence, it will already have those values in place that the QueryCatalogInfo is supposed to put there.  So we can remove that piepline component so that it will not run.

We strong-name, build and GAC the component above and we can go into the “channelconfiguration.config” file and add this new components into the list right above the “Order Pipelines Processor” component in the “QueryOperation_basket” message handler.  Note that the “Order Pipeline Processor” component is called from a number of message handlers and we need to add this entry above each one:

<Component name="QueryCatalogInfo" type="Microsoft.Commerce.Samples.Pipelines.QueryCatalogInfo, Microsoft.Commerce.Samples.Pipelines, Version=1.0.0.0, Culture=neutral,PublicKeyToken=b23706c1d1011ab9" />

It looks like we have everything configured now, so lets restart IIS (give it a clean slate) and refresh the basket again and see what we get.

image

Amazingly enough, my basket now contains the pricing generated by my pricing engine.  I’ve also removed a legacy com-based component from a pipeline which is executed quite a lot, which gives me more control of the overall solution.