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.
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.
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.
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.