A question came up on the Commerce Server newsgroups recently about transactional pipeline components. It boiled down to: “What is the proper way to build a transactional SQL pipeline component?”. Well, it turns out there’s really nothing special you need to do. By default, any SqlConnection you create and use in your pipeline component will automatically enlist in the DTC transaction if there is one. According to this MSDN reference on Accessing the Current Transaction, “For database connections, ADO.NET checks Transaction.Current automatically when the Connection.Open method is called, and transparently enlists the connection in that transaction (unless the Enlist keyword is set to false in the connection string).”
I’ve included the sample component I built to test this out with the Commerce Server 2007 Starter Site, but the same code should work with a Commerce Server 2002 Feature Pack 1 site, I believe.
The proper way to use transactions with the checkout process is often misunderstood. You should mark the entire checkout page as transacted:
On the checkout page you need to run all three of the main pipelines together before committing the order: basket, total, and checkout, in that order. The checkout pipeline is the only one that needs to be configured as transacted in the Web.config. This just means a transaction is required for that pipeline to execute. The other pipelines will participate in the outer page transaction if one exists (to COM+ they are registered as transactions Supported, but not required). The Basket.SaveAsOrder() method will also participate in the transaction. Thus if any failure (or warning) is reported by any pipeline component in any of the pipelines, the transaction will be rolled back and the basket will remain in the pre-checkout state.
Pipeline components can report their completion status in a couple of different ways. If the component fails for some fatal/unrecoverable reason such as SQL server being unavailable, then the component can throw an exception. In COM terms, that means setting the HRESULT to the appropriate failure code and setting the IErrorInfo on the thread. For .NET components, they can simply throw an exception and let the CLR’s .NET interop facilities translate that into an HRESULT. Another way a component can denote failure (not recommended) is by returning a value greater or equal to Error (3). In this case the OrderGroup.RunPipeline() method will throw a PipelineExecutionException.
If the component excecutes successfully but needs to report a business validation error, it should return an error level of Warning (2). This indicates the component executed successfully but reported warnings (use Success (1) to indicate completion with no errors or warnings; you shouldn’t use an error level of zero as that’s not defined). A well-behaved component that reports such a warning should also add a string to either the _Basket_Errors or _Purchase_Errors lists, depending on which pipeline the component is associated with (_Basket_Errors for the basket and total pipelines and _Purchase_Errors for the checkout pipeline is the general guideline – the distinction is mostly legacy – you should check for and display messages from both lists as a rule).
A transactional pipeline will not vote to commit the transaction if the error level returned by a component is Warning (2) or Error (3) (or higher). You can find out the error level by checking the return value of OrderGroup.RunPipeline (a PipelineExecutionResult enum value). If it is Warning (2), the site code should be prepared to display any messages from the _Basket_Errors and _Purchase_Errors lists. These messages are usually retrieved by the pipeline component from the COM MessageManager object that is configured for the site (MessageManager is a legacy carryover from before the days of .NET ResourceManagers). MessageManager will return the localized display message corresponding to well-defined error code strings.
The below code sample is what I came up with for demonstrating and playing with this. To build this pipeline component,
- Create a C# class library project in VS.NET 2005
- Add references to Microsoft.CommerceServer.Interop.dll, Microsoft.CommerceServer.Shared.dll, and Microsoft.CommerceServer.Runtime.dll
- Configure the project to register components with COM so that a .tlb will be generated
- Regasm the assembly (with the /codebase option unless you are deploying it to the GAC)
- Use the pipeline registration tool in the Commerce Server SDK (pipereg.exe) to register the component as a pipeline component
After the component has been verified to execute correctly in your checkout scenario, to test out the rollback, the easiest thing to do is add a Scriptor component to the pipeline configured to execute after the sample component. Edit the scriptor (right click in pipeline editor and select “properties”, then click the “Edit Script” button) and modify it to return error level Warn (2) or Error (3) instead of the default value of Success (1). Note that if you get back error level Warn (2) from a transacted pipeline running on a page that is set to require transactions and ignore the return value and try to run OrderGroup.SaveAsOrder anyway, then .NET will complain loudly (by throwing an exception) because you’re trying to perform SQL operations within a DTC transaction that’s already been rolled back by the pipeline infrastructure. It looks like our CS2006 Starter Site currently has this bug – moral of the story – check that return value from the pipeline!
One more thing about pipeline error levels that is little known (not sure how useful it is, but that’s a different matter) is that in the pipeline editor you can configure the “error level tolerance” of each stage in the pipeline. This can be used to skip executing certain components (entire stages, really) when the error level is greater than the configured threshold. So for example if the error threshold for the Accept stage of the checkout pipeline is one (it is, trust me), then components in that stage will not execute if any component that came before returned an error level of two or greater. Also note that the final error level returned from pipeline execution is the greatest of the error level return values from all of the components executed within the pipeline.
The final figure below the code sample is the dump output showing all the keys and values in the entire OrderForm and PipelineContext dictionaries when my component was executed in the checkout pipeline of the CS2007 starter site (current builds). The technique I used to generate that HTML is just the few lines of code starting at line 59. The DumpUtils class in the Microsoft.CommerceServer.Runtime.Diagnostics namespace is very useful when it comes to inspecting Commerce Server object hierarchies that include the COM types such as Dictionary, SimpleList, and ContentList. DumpUtils is legacy code from CS2002 (and too tightly tied to System.Web.UI), so maybe one of these days I’ll find time to update that to emit XML that can be styled instead (that would be more useful, eh?).
1 using System;
2 using System.Diagnostics;
3 using System.Data;
4 using System.Data.SqlClient;
5 using System.Data.OleDb;
6 using System.Collections.Generic;
7 using System.Text;
8 using System.IO;
9 using System.Web.UI;
10
11 using System.Runtime.InteropServices;
12 using Microsoft.CommerceServer.Runtime;
13 using Microsoft.CommerceServer.Runtime.Configuration;
14 using Microsoft.CommerceServer.Runtime.Diagnostics;
15 using Microsoft.CommerceServer.Interop;
16 using Microsoft.CommerceServer.Interop.Orders;
17
18 namespace TxSqlTester
19 {
20 // ===========================================================
21 // Create this table in the Commerce Server Transactions databse
22 // before running the sample
23 //
24 //CREATE TABLE [dbo].[UserOrderInfo] (
25 // [UserId] [nvarchar] (50) NOT NULL ,
26 // [NumLineItems] [int] NOT NULL ,
27 // [BasketOrderGroupId] [nvarchar] (50) NOT NULL ,
28 // [DateTimeStamp] [datetime] NULL
29 //)
30 //GO
31 //
32 // By adding this component to the Checkout.pcf pipeline,
33 // an entry will be written to the UserOrderInfo
34 // table each time a checkout occurs. If another component
35 // down the line returns errorlevel >= 2, then the transaction
36 // will be rolled back.
37 // ===========================================================
38
39 // A pipeline component that writes to SQL server
40 // Note that there is no special code required to enlist in the
41 // DTC transaction owned by the pipeline
42 [ComVisible(true)]
43 [GuidAttribute(“1A3CD295-A758-4e83-9301-A4BBD41D8F75”)]
44 public class TxSqlTester : IPipelineComponent
45 {
46 #region IPipelineComponent Members
47 public int Execute(object pdispOrder, object pdispContext, int lFlags)
48 {
49 // If any other component in the transacted pipeline returns ErrorLevel >= 2 (Warn),
50 // then the Tx will roll back along with any SQL writes performed here.
51
52 IDictionary context = (IDictionary)pdispContext;
53 IDictionary orderForm = (IDictionary)pdispOrder;
54
55 // retrieve and sanitize the Transactions connection string
56 string txConnectionString = null;
57 SqlUtil.CleanSqlClientConnectionString(this.GetTxConnectionString(context), out txConnectionString);
58
59 //StringWriter textWriter = new StringWriter();
60 //System.Web.UI.HtmlTextWriter output = new System.Web.UI.HtmlTextWriter(textWriter);
61 //DumpUtils.DumpDictionary(orderForm, output, “OrderForm”);
62 //DumpUtils.DumpDictionary(context, output, “Context”);
63 //string dumpDictionariesAsHtml = textWriter.ToString();
64
65 // retrieve the values we will be writing to the SQL table from the Order Form
66 string userid = orderForm[“user_id”].ToString();
67 string basketOrderGroupId = orderForm[“BasketOrderGroupId”].ToString();
68 int numLineItems = ((ISimpleList)orderForm[“items”]).Count;
69
70 // perform the SQL write operation
71 UserOrderUpdateSql updater = new UserOrderUpdateSql(txConnectionString);
72 updater.WriteUserOrderRecord(userid, basketOrderGroupId, numLineItems);
73
74 return 1; // (1=Success, 2=Warn, 3=Fail)
75 }
76
77 // Legacy IPipelineComponent method – no need to do anything
78 public void EnableDesign(int fEnable)
79 {
80 }
81 #endregion // IPipelineComponent
82
83 private string GetTxConnectionString(IDictionary context)
84 {
85 CommerceResourceCollection resources = context[“CommerceResources”] as CommerceResourceCollection;
86 if (resources != null)
87 return resources[“Transactions”][“connstr_db_transactions”].ToString();
88 else
89 throw new ApplicationException(“Unable to retrieve Commerce Resources”);
90 }
91 }
92
93 // This class inserts a row into the UserOrderInfo table.
94 public class UserOrderUpdateSql
95 {
96 private string connstr;
97
98 public UserOrderUpdateSql(string connstr)
99 {
100 this.connstr = connstr;
101 }
102
103 public void WriteUserOrderRecord(string userId, string basketOrderGroupId, int numLineItems)
104 {
105 SqlConnection conn = null;
106 const string tsql =
107 “insert into [UserOrderInfo] ([UserId], [BasketOrderGroupId], [NumLineItems]) values (@userid, @basketid, @numlineitems)”;
108 using (conn = new SqlConnection(connstr))
109 {
110 conn.Open();
111 SqlCommand cmd = new SqlCommand(tsql, conn);
112 cmd.Parameters.Add(new SqlParameter(“@userid”, userId));
113 cmd.Parameters.Add(new SqlParameter(“@basketid”, basketOrderGroupId));
114 cmd.Parameters.Add(new SqlParameter(“@numlineitems”, numLineItems));
115 cmd.ExecuteNonQuery();
116 }
117 }
118 }
119
120 internal static class SqlUtil
121 {
122 // removes the “Provider” keyword present in Commerce Server connection strings so that
123 // it will work with the SQLClient
124 public static bool CleanSqlClientConnectionString(string connectionString, out string cleaned)
125 {
126 bool successStatus;
127 cleaned = null;
128 try
129 {
130 OleDbConnectionStringBuilder builder = new OleDbConnectionStringBuilder(connectionString);
131 builder.Remove(“Provider”);
132 cleaned = builder.ConnectionString;
133 successStatus = true;
134 }
135 catch (ArgumentException)
136 {
137 successStatus = false;
138 }
139 return successStatus;
140 }
141 }
142 }
Dumping Dictionary ‘OrderForm’
_cy_oadjust_subtotal=<System.Decimal> 200
_event=<System.String> “SOLD”
_winners=<Commerce.SimpleList>
Dumping SimpleList ‘_winners’
<empty>
user_id=<System.String> “{575d3603-daac-42ad-b85a-dcc615e44ce8}”
promo_user_identity=<System.String> “”
shipments=<Commerce.SimpleList>
Dumping SimpleList ‘shipments’
<Commerce.Dictionary>
Dumping Dictionary ‘list item 1’
shipment_id=<System.String> “{f412aa14-6b23-47d8-a8df-96ae753ea4fe}”
ItemIndexes=<Commerce.SimpleList>
Dumping SimpleList ‘ItemIndexes’
<System.Int32> 0
shipping_method_name=<System.String> “Second-Day Air”
shipping_address_id=<System.String> “24d35f86-ff97-475b-82a4-de9e29068b52”
_shipping_discounts_applied=<Commerce.SimpleList>
Dumping SimpleList ‘_shipping_discounts_applied’
<empty>
shipping_method_id=<System.String> “{b75f90be-49bc-47b9-b268-d5996ad3baf8}”
_cy_shipping_total=<System.Decimal> 10
_cy_shipping_discounts_subtotal=<System.Decimal> 0.0000
shipment_tracking_number=<System.String> “”
shipment_status=<System.String> “”
payments=<Commerce.SimpleList>
Dumping SimpleList ‘payments’
<Commerce.Dictionary>
Dumping Dictionary ‘list item 1’
cc_identifier=<System.String> “”
cy_amount=<System.Decimal> 210
PaymentProfile=<System.String> “cf634e36-9385-40d7-bfe7-6eddaff0e9b4”
payment_id=<System.String> “{6d639441-2f9f-4417-8d9d-5d386e446bca}”
billing_address_id=<System.String> “96ab2cc7-5587-49b6-967d-ab309b264942”
payment_status=<System.String> “”
authorization_code=<System.String> “”
_cc_expyear=<System.Int32> 2009
_cc_expmonth=<System.Int32> 1
payment_method_name=<System.String> “Visa”
customer_name_on_payment=<System.String> “Davied Messner”
derived_class_name=<System.String> “CreditCardPayment”
_cc_number=<System.String> “411111111111”
cc_type=<System.String> “f2e8f93d-fdc2-4dfd-8239-02f6f187359a”
validation_code=<System.String> “”
payment_method_id=<System.String> “{f2e8f93d-fdc2-4dfd-8239-02f6f187359a}”
_Verify_With=<Commerce.Dictionary>
Dumping Dictionary ‘_Verify_With’
<empty>
_Purchase_Errors=<Commerce.SimpleList>
Dumping SimpleList ‘_Purchase_Errors’
<empty>
promo_codes=<Commerce.SimpleList>
Dumping SimpleList ‘promo_codes’
<empty>
_Basket_Errors=<Commerce.SimpleList>
Dumping SimpleList ‘_Basket_Errors’
<empty>
billing_address_id=<System.String> “96ab2cc7-5587-49b6-967d-ab309b264942”
order_id=<System.String> “A4NLAXB8VQLN9MFPBN8UJ2RX04”
orderform_id=<System.String> “{3618dbda-25cc-42cb-8784-95e5c98c9a45}”
_cy_tax_included=<System.Decimal> 0.0000
orderform_status=<System.String> “”
Addresses=<Commerce.Dictionary>
Dumping Dictionary ‘Addresses’
24d35f86-ff97-475b-82a4-de9e29068b52=<Commerce.Dictionary>
Dumping Dictionary ’24d35f86-ff97-475b-82a4-de9e29068b52′
last_name=<System.String> “Messner”
order_group_id=<System.String> “{90551d41-c547-4c27-806d-be81d70fb5b7}”
address_line1=<System.String> “6602 149th”
address_id=<System.String> “24d35f86-ff97-475b-82a4-de9e29068b52”
state=<System.String> “WA”
region_name=<System.String> “”
address_line2=<System.String> “”
country_code=<System.String> “”
email=<System.String> “”
first_name=<System.String> “David”
name=<System.String> “6602 149th”
country_name=<System.String> “”
evening_phone_number=<System.String> “”
day_time_phone_number=<System.String> “”
postal_code=<System.String> “98052”
city=<System.String> “Redmond”
promo_code_info=<Commerce.SimpleList>
Dumping SimpleList ‘promo_code_info’
<empty>
items=<Commerce.SimpleList>
Dumping SimpleList ‘items’
<Commerce.Dictionary>
Dumping Dictionary ‘list item 1’
_product_BaseCatalogName=<System.String> “Adventure Works Catalog”
_product_#Inv_Origvariantid#=<System.String> “3”
_product_BackorderedQuantity=<System.Decimal> 0.0000
_product_ProductColor=<System.String> “Blue”
cy_placed_price=<System.Decimal> 200
_product_categories=<Array>
Dumping Array ‘_product_categories’
<System.String> “SleepingBags”
_product_InventoryCondition=<System.Int32> 3
_cy_itemlevel_discounts_subtotal=<System.Decimal> 0.0000
product_catalog=<System.String> “Adventure Works Catalog”
_product_Image_filename=<System.String> “./images/sleepingbags03.gif”
shipping_method_name=<System.String> “Second-Day Air”
_product_#Catalog_Lang_Oid=<System.Int32> 96
_product_#Inv_ProductId#=<System.String> “AW200-12”
_cy_oadjust_discount=<System.Decimal> 0.0000
_product_IntroductionDate=<System.DateTime> 3/3/1996 4:00:00 PM
_product_ProductID=<System.String> “AW200-12”
_product_LastModified=<System.DateTime> 9/28/2000 3:19:37 PM
_product_ProductCode=<System.String> “AW200-12”
shipping_address_id=<System.String> “24d35f86-ff97-475b-82a4-de9e29068b52”
_product_OnSale=<System.Int32> 0
_product_SkuTimestamp=<Array>
Dumping Array ‘_product_SkuTimestamp’
<empty>
_product_#Inv_BaseCatalogName#=<System.String> “Adventure Works Catalog”
_product_#Inv_variantid#=<System.String> “3”
_product_OrigProductID=<System.String> “AW200-12”
_cy_iadjust_currentprice=<System.Decimal> 200
_cy_oadjust_adjustedprice=<System.Decimal> 200
_inventory_allow_backorder_and_preorder=<System.Boolean> True
_product_TimeStamp=<Array>
Dumping Array ‘_product_TimeStamp’
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 13
<System.Byte> 237
_cy_iadjust_regularprice=<System.Decimal> 200
_product_Description=<System.String> “Generously cut sleeping bag, goose down with polyster taffeta, cotton storage sack included.”
shipping_method_id=<System.String> “{b75f90be-49bc-47b9-b268-d5996ad3baf8}”
_product_Oid=<System.Int32> 96
_product_InventoryCondition1=<System.Int32> 3
product_variant_id=<System.String> “3”
_product_ParentOID=<System.Int32> 95
_product_i_ClassType=<System.Int32> 2
_n_unadjusted=<System.Int32> 1
_orderlevel_discounts_applied=<Commerce.SimpleList>
Dumping SimpleList ‘_orderlevel_discounts_applied’
<empty>
_product_#Catalog_row_num=<System.Int32> 1
_product_#Inv_OrigProductId#=<System.String> “AW200-12”
_product_#Catalog_Lang_Timestamp=<Array>
Dumping Array ‘_product_#Catalog_Lang_Timestamp’
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 0
<System.Byte> 13
<System.Byte> 239
_product_cy_list_price=<System.Decimal> 200
_product_ExportReady=<System.Boolean> True
_product_CatalogName=<System.String> “Adventure Works Catalog”
_inventory_condition=<System.Int32> 0
item_status=<System.String> “”
_product_Image_width=<System.Int32> 120
_product_OriginalPrice=<System.Decimal> 200
index=<System.String> “{4246b16b-fdd6-4f8d-b6b3-e9741c3fa77b}”
_inventory_back_order_for_request=<System.Decimal> 0.0000
_product_DisplayName=<System.String> “Big Sur”
_product_VariantCode=<System.Int32> 3
_product_VariantID=<System.String> “3”
_product_PreorderedQuantity=<System.Decimal> 0.0000
_product_DefinitionName=<System.String> “SleepingBag”
_product_Image_height=<System.Int32> 120
_itemlevel_discounts_applied=<Commerce.SimpleList>
Dumping SimpleList ‘_itemlevel_discounts_applied’
<empty>
_product_UseCategoryPricing=<System.Boolean> False
_cy_orderlevel_discounts_subtotal=<System.Decimal> 0.0000
product_id=<System.String> “AW200-12”
_inventory_in_stock_for_request=<System.Decimal> 0.0000
_product_Name=<System.String> “Big Sur”
_product_OrigVariantID=<System.String> “3”
quantity=<System.Int32> 1
_inventory_pre_order_for_request=<System.Decimal> 0.0000
_product_#TmpCatalogoid#=<System.Int32> 96
_cy_tax_total=<System.Decimal> 0.0000
_cy_shipping_total=<System.Decimal> 10
_discounts=<IDataContainer>
Dumping DataContainer ‘_discounts’
<empty>
_content=<IDataContainer>
Dumping DataContainer ‘_content’
<empty>
_cy_shipping_discounts_total=<System.Decimal> 0.0000
BasketOrderGroupId=<System.String> “{90551d41-c547-4c27-806d-be81d70fb5b7}”
_cy_total_total=<System.Decimal> 210
_cy_handling_total=<System.Decimal> 0.0000
_performance=<Commerce.Dictionary>
Dumping Dictionary ‘_performance’
<empty>
Dumping Dictionary ‘Context’
Language=<System.String> “en-US”
MessageManager=<Microsoft.CommerceServer.Interop.MessageManagerClass> Microsoft.CommerceServer.Interop.MessageManagerClass
CacheManager=<Microsoft.CommerceServer.Interop.Caching.CacheManagerClass> Microsoft.CommerceServer.Interop.Caching.CacheManagerClass
CatalogContext=<Microsoft.CommerceServer.Catalog.CatalogContext> Microsoft.CommerceServer.Catalog.CatalogContext
pipelines=<ICollection>
Dumping Collection ‘pipelines’
<Microsoft.CommerceServer.Runtime.Pipelines.OrderPipeline> basket
<Microsoft.CommerceServer.Runtime.Pipelines.OrderPipeline> checkout
<Microsoft.CommerceServer.Runtime.Pipelines.OrderPipeline> product
<Microsoft.CommerceServer.Runtime.Pipelines.OrderPipeline> total
<Microsoft.CommerceServer.Runtime.Pipelines.ContentSelectionPipeline> advertising
<Microsoft.CommerceServer.Runtime.Pipelines.ContentSelectionPipeline> discounts
<Microsoft.CommerceServer.Runtime.Pipelines.ContentSelectionPipeline> recordevent
<Microsoft.CommerceServer.Runtime.Pipelines.OrderPipeline> creditcard
ProfileService=<Microsoft.CommerceServer.Interop.Profiles.ProfileServiceClass> Microsoft.CommerceServer.Interop.Profiles.ProfileServiceClass
CommerceResources=<ICollection>
Dumping Collection ‘CommerceResources’
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
<Microsoft.CommerceServer.Runtime.Configuration.CommerceResource> Microsoft.CommerceServer.Runtime.Configuration.CommerceResource
CacheName=<System.String> “Discounts”