Monday, May 3, 2010

Custom Payments in Commerce Server 2009

Introduction

One of my biggest headaches so far has been adding custom payment types with our Commerce Server 2009 solution. The new Mojave API presents payment types as straightforward and easily extended at first glance. However, like many areas of this new product, what seems to be consistent and extensible when working inside the default site box quickly just becomes yet another layer that you will need to trudge through when making your extensions. I'll take you through that process in this article, from bottom to top.

For sake of discussion, we'll be adding a "Free" payment type. A free payment can be added to an order to make up some or the entire total. This pans out the same as a discount to the customer, but by working on the payment end of the order. All of the totals on the order will be unaffected, which may suit certain situations for customer service. The free payment type will be very basic, and include no additional fields beyond the default, built-in payment fields.

1 - Add Database Table

At the very bottom of the stack, we need to add data storage for our free payment type. To do this, I used a create script generated for the built-in PurchaseOrders table and tweaked it to my needs:
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[FreePayments]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[FreePayments](
[PaymentId] [uniqueidentifier] ROWGUIDCOL NOT NULL,
[OrderFormId] [uniqueidentifier] NOT NULL,
[OrderGroupId] [uniqueidentifier] NOT NULL,
[BillingAddressId] [nvarchar](50) NULL,
[PaymentMethodId] [uniqueidentifier] NOT NULL,
[PaymentMethodName] [nvarchar](128) NULL,
[Amount] [money] NOT NULL,
[PaymentType] [int] NOT NULL,
[Status] [nvarchar](64) NULL,
[MarshalledData] [image] NULL,
CONSTRAINT [PK_FreePayments] PRIMARY KEY CLUSTERED
(
[PaymentId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

ALTER TABLE [dbo].[FreePayments] WITH NOCHECK ADD CONSTRAINT [FK_FreePayments_OrderForms] FOREIGN KEY([OrderFormId])
REFERENCES [dbo].[OrderForms] ([OrderFormId])


ALTER TABLE [dbo].[FreePayments] CHECK CONSTRAINT [FK_FreePayments_OrderForms]

END
GO

2 - Add Payment Methods

The new payment type will require at least one payment method. Each payment method is assigned an integer identifier, and a number of "Custom" placeholder payment methods are built-in (see MSDN). I'll be adding a new payment method "Free", which will be assigned the Custom1 payment method type:
INSERT INTO [PaymentMethod]
([PaymentMethodId]
,[LanguageId]
,[PaymentMethodName]
,[Description]
,[PaymentProcessor]
,[ConfiguredMode]
,[PaymentType]
,[Enabled]
,[GroupId]
,[Created]
,[LastModified]
,[IsDefault])
VALUES
(newid(), 'en-US', 'Free', 'Free', '',0,6,1,NEWID(),GETDATE(),GETDATE(),1)
GO

3 - Create Extended Payment Classes

Now we need to create the Commerce Server Payment class for this payment type:
[Serializable]
[ComVisible(true)]
public class FreePayment : Payment
{
public const PaymentMethodTypes PaymentMethodType = PaymentMethodTypes.Custom1;

public FreePayment()
{
base.ProtectedPaymentType = PaymentMethodType;
}

public FreePayment(string billingAddressId, Guid paymentMethodId)
{
base.ProtectedPaymentType = PaymentMethodType;
base.BillingAddressId = billingAddressId;
base.PaymentMethodId = paymentMethodId;
}

protected FreePayment(SerializationInfo info, StreamingContext context) :
base (info, context)
{
base.ProtectedPaymentType = PaymentMethodType;
}

[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
}
}
Build the project and deploy the assembly to the GAC or to the bin folders of your OrdersWebService and Commerce website(s).
4 - Update Mappings

To get Commerce Server to start using your new payment class, you must configure the mappings. This happens in two files: OrderObjectMappings.xml and OrderPipelineMappings.xml.

OrderObjectMappings.xml controls how classes map to the underlying tables. We will again use an existing payment as a template and add the following sections (I put each of mine right before the PurchaseOrderPayment version of that section).
<Table Name="FreePayments">
<Columns>
<Column Name="PaymentId" DataType="uniqueidentifier" GUID="true" />
<Column Name="OrderFormId" DataType="uniqueidentifier" />
<Column Name="OrderGroupId" DataType="uniqueidentifier" />
<Column Name="BillingAddressId" DataType="nvarchar" Precision="50" IsNullable="true" />
<Column Name="PaymentMethodId" DataType="uniqueidentifier" IsNullable="false" />
<Column Name="PaymentMethodName" DataType="nvarchar" Precision="128" IsNullable="true" />
<Column Name="Amount" DataType="money" />
<Column Name="PaymentType" DataType="int" />
<Column Name="Status" DataType="nvarchar" Precision="64" IsNullable="true" />
<Column Name="MarshalledData" DataType="image" IsNullable="true" />
</Columns>
<Constraints>
<PrimaryKey Name="PK_FreePayments">
<ColumnRef Name="PaymentId" />
</PrimaryKey>
<!-- No need for ON DELETE CASCADE semantics - generated code will delete table by table -->
<ForeignKey Name="FK_FreePayments_OrderForms" ForeignTable="OrderForms" CascadeDelete="false">
<ColumnMatch Name="OrderFormId" ForeignName="OrderFormId" />
</ForeignKey>
</Constraints>
</Table>
<Class Name="FreePayment">
<Property Name="PaymentId"/>
<Property Name="OrderGroupId"/>
<Property Name="OrderFormId"/>
<Property Name="BillingAddressId"/>
<Property Name="PaymentMethodId"/>
<Property Name="PaymentMethodName"/>
<Property Name="Amount"/>
<Property Name="PaymentType"/>
<Property Name="Status"/>
</Class>
<ClassTableMap Class="FreePayment" Table="FreePayments">
<PropertyMap Property="PaymentId" Column="PaymentId" />
<PropertyMap Property="OrderFormId" Column="OrderFormId" />
<PropertyMap Property="OrderGroupId" Column="OrderGroupId" />
<PropertyMap Property="BillingAddressId" Column="BillingAddressId" />
<PropertyMap Property="PaymentMethodId" Column="PaymentMethodId" />
<PropertyMap Property="PaymentMethodName" Column="PaymentMethodName" />
<PropertyMap Property="Amount" Column="Amount" />
<PropertyMap Property="PaymentType" Column="PaymentType" />
<PropertyMap Property="Status" Column="Status" />
</ClassTableMap>
OrderPipelineMappings.xml controls how the Commerce classes are mapped to the dictionaries available to pipeline components. Again we'll use the strategy of copying from an existing payment type template:
<Class Name="FreePayment">
<Property Name="BillingAddressId" DictionaryKey="billing_address_id" />
<Property Name="PaymentId" DictionaryKey="payment_id" ClassKey="true" />
<Property Name="PaymentMethodId" DictionaryKey="payment_method_id" />
<Property Name="PaymentMethodName" DictionaryKey="payment_method_name" />
<Property Name="Amount" DictionaryKey="cy_amount" />
<Property Name="DerivedClassName" DictionaryKey="derived_class_name" />
<Property Name="Status" DictionaryKey="payment_status" />
</Class>
Deploy both of these files to your OrdersWebService and Commerce website folder(s).
5 - Update Web.Configs

In order for Commerce Server to use your custom class in the mappings you just created, you must define the type in the configuration files. For all of your Web.Config files (OrdersWebService, Commerce website), find the "Types" section that defines all of the Commerce Server types (Basket, PurchaseOrder, etc.). In that section, add a tag corresponding to your type and assembly. In our example, the Key and UserTypeName attributes would both be "FreePayment".

6 - Regenerate OrdersStorage.sql

One step remains in updating the SQL layer of this mess, which you are now prepared to execute. Open a command prompt and navigate to your OrdersWebService directory, and execute the following command (your path to Commerce Server may vary):
"C:\Program Files (x86)\Microsoft Commerce Server 2007\Tools\OrderMapping.exe" /w Web.Config /i
If successful, this will create an OrdersStorage.sql file in the current directory. Run that script against your transactions database.

7 - Register With Mojave

Now you have the Commerce Server 2007 extensions complete. Just think, if you hadn't upgraded, you'd be done! Since we're using the new Mojave API, however, we have a bit more work to do. First, we need to let Mojave know about this type by registering it in MetadataDefinitions.xml. Add the following element right after the existing CommerceEntity element for GiftCertificatePayment:
<CommerceEntity name="FreePayment">
<EntityMappings>
<EntityMapping
csType="[Fully-qualified '07 Payment Type Name]" csAssembly="[Fully-qualified '07 Payment Type Assembly]">
<PropertyMappings>
<PropertyMapping property="Id" csProperty="PaymentId"/>
</PropertyMappings>
</EntityMapping>
</EntityMappings>
</CommerceEntity>
Once you've made this change, this file must be deployed to your Commerce website(s).

8 - Create Translators

Now we need to enable Mojave to translate between an '07 FreePayment and an '09 CommerceEntity. Since Mojave actually represents the single '07 payment as two entities, Payment and PaymentAccount, there are two translators that come into play here. The built-in translator PaymentTranslator takes care of the translation to/from the Payment entity, and this one we won't need to touch. However, we will need to create a translator to/from the PaymentAccount (FreePayment in our case):
using CSPayment = Microsoft.CommerceServer.Runtime.Orders.Payment;
public class FreePaymentTranslator
: IToCommerceEntityTranslator, IToExternalEntityTranslator
{
private PropertyTranslator propertyTranslator;

public FreePaymentTranslator()
{
this.propertyTranslator
= new PropertyTranslator(
new FromCommerceServerPropertyTranslator(this.TranslateFromStronglyTypedCommerceServerProperty),
new FromCommerceServerPropertyTranslator(this.TranslateFromWeaklyTypedCommerceServerProperty),
new ToCommerceServerPropertyTranslator(this.TranslateToStronglyTypedCommerceServerProperty),
new ToCommerceServerPropertyTranslator(this.TranslateToWeaklyTypedCommerceServerProperty));
}

#region Implementation of IToCommerceEntityTranslator

public void Translate(object source, CommerceEntity destinationCommerceEntity, CommercePropertyCollection propertiesToReturn)
{
FreePayment commerceServerObject = source as FreePayment;
if (commerceServerObject != null)
{
this.propertyTranslator.TranslateToCommerceEntity(commerceServerObject, destinationCommerceEntity,
propertiesToReturn);
}
}

#endregion

#region Implementation of IToExternalEntityTranslator

public void Translate(CommerceEntity sourceCommerceEntity, object destination)
{
if (sourceCommerceEntity.ModelName.Equals("FreePayment"))
{
FreePayment commerceServerObject = destination as FreePayment;
this.propertyTranslator.TranslateToCommerceServer(sourceCommerceEntity, commerceServerObject, null);
}
}

#endregion

protected bool TranslateToWeaklyTypedCommerceServerProperty(
CSPayment commerceServerObject,
string commerceServerPropertyName,
object value)
{
commerceServerObject[commerceServerPropertyName] = value;
return true;
}

protected bool TranslateFromWeaklyTypedCommerceServerProperty(
CSPayment commerceServerObject,
string commerceServerPropertyName,
CommerceEntity commerceEntity,
string mojavePropertyName)
{
commerceEntity.Properties[mojavePropertyName] = commerceServerObject[commerceServerPropertyName];
return true;
}

protected virtual bool TranslateToStronglyTypedCommerceServerPropertyBase(
CSPayment commerceServerObject,
string commerceServerPropertyName,
object value)
{
switch (commerceServerPropertyName)
{
case "BillingAddressId":
commerceServerObject.BillingAddressId = value as string;
break;

case "CustomerNameOnPayment":
commerceServerObject.CustomerNameOnPayment = value as string;
break;

default:
return false; // the property was NOT translated
}

return true; // the property was translated
}

protected virtual bool TranslateFromStronglyTypedCommerceServerPropertyBase(
CSPayment commerceServerObject,
string commerceServerPropertyName,
CommerceEntity commerceEntity,
string mojavePropertyName)
{
switch (commerceServerPropertyName)
{
case "PaymentId":
commerceEntity.Properties[mojavePropertyName] = commerceServerObject.PaymentId.ToString("B");
break;

case "BillingAddressId":
commerceEntity.Properties[mojavePropertyName] = commerceServerObject.BillingAddressId;
break;

case "CustomerNameOnPayment":
commerceEntity.Properties[mojavePropertyName] = commerceServerObject.CustomerNameOnPayment;
break;

default:
return false; // the property was NOT translated
}

return true; // the property was translated
}

protected bool TranslateFromStronglyTypedCommerceServerProperty(
FreePayment commerceServerObject,
string commerceServerPropertyName,
CommerceEntity commerceEntity,
string mojavePropertyName)
{
if (this.TranslateFromStronglyTypedCommerceServerPropertyBase(commerceServerObject, commerceServerPropertyName, commerceEntity, mojavePropertyName))
{
return true;
}
return false;
}

protected bool TranslateToStronglyTypedCommerceServerProperty(
FreePayment commerceServerObject,
string commerceServerPropertyName,
object value)
{
if (this.TranslateToStronglyTypedCommerceServerPropertyBase(commerceServerObject, commerceServerPropertyName, value))
{
return true;
}
return false;

}

}
Note that you would need to add handling to TranslateToStronglyTypedCommerceServerProperty and TranslateFromStronglyTypedCommerceServerProperty for any additional properties you put on the payment.

To register this translator, open your ChannelConfiguration.config file, and add the following tags to the "ToCommerceEntities" element near the bottom:
<Translator sourceType="[Fully-qualified '07 Type]" destinationModelName="Payment" type="Microsoft.Commerce.Providers.Translators.ProfileAddressTranslator, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35" />
<Translator sourceType="[Fully-qualified '07 Type]" destinationModelName="FreePayment" type="[Fully-qualified Translator Type]" />
Then, add the following tags to the "ToExternalEntities" element:
<Translator sourceModelName="Payment" destinationType="[Fully-qualified '07 Type]" type="Microsoft.Commerce.Providers.Translators.PaymentTranslator, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35" />
<Translator sourceModelName="FreePayment" destinationType="[Fully-qualified '07 Type]" type="[Fully-qualified Translator Type]" />
This file will need to be deployed, but not until later because there are other changes coming.

9 - Create New Payment Response Builder

Commerce Server's PaymentResponseBuilder creates the CommerceEntity for payments returned in a basket query. The built-in version does not handle the custom payment types and also does not allow for convenient extension, so we will build our own replacement response builder. If you're adding more than one custom payment type, note that you will only need one of these classes, but it must provide handling for all of your custom types.
public class CustomPaymentsResponseBuilder : PaymentsResponseBuilder
{
private static void QueryPaymentAccount(CommerceQueryRelatedItem paymentQuery, CommerceEntity responsePayment, Payment commerceServerPayment)
{
CommerceQueryRelatedItem paymentAccountQuery = paymentQuery.GetRelatedOperations("PaymentAccount").FirstOrDefault();
if (paymentAccountQuery != null)
{
CommerceEntity item = null;
switch (commerceServerPayment.PaymentType)
{
case PaymentMethodTypes.CreditCard:
item = new CommerceEntity("CreditCard");
break;

case PaymentMethodTypes.GiftCertificate:
item = new CommerceEntity("GiftCertificate");
break;

case PaymentMethodTypes.PurchaseOrder:
item = new CommerceEntity("PurchaseOrder");
break;

case PaymentMethodTypes.CashCard:
item = new CommerceEntity("CashCard");
break;
default:
switch (commerceServerPayment.DerivedClassName)
{
case "FreePayment":
item = new CommerceEntity("FreePayment");
break;
}
break;
}

if (item != null)
{
responsePayment.Properties["PaymentAccount"] = new CommerceRelationship(item);
CommerceEntity model = paymentAccountQuery.Model;
Translator.ToCommerceEntity(commerceServerPayment, item, model.Properties);
QueryPaymentMethod(paymentAccountQuery, item, commerceServerPayment);
}
}
}

private static void QueryPaymentMethod(
CommerceQueryRelatedItem paymentAccountQuery,
CommerceEntity paymentAccount,
Payment commerceServerPayment)
{
var paymentMethodQuery = (paymentAccountQuery.GetRelatedOperations("PaymentMethod")).FirstOrDefault();

if (paymentMethodQuery == null)
{
return; // PaymentMethod is not requested
}

var paymentMethodModel = paymentMethodQuery.GetModel("PaymentMethod");
var paymentMethod = PaymentHelper.GetPaymentMethodByMethodId(
OperationContext.CurrentInstance.SiteName,
OperationContext.CurrentInstance.RequestContext.UserLocale,
commerceServerPayment.PaymentMethodId.ToString(),
paymentMethodModel.Properties);

paymentAccount.Properties["PaymentMethod"] = new CommerceRelationship(paymentMethod);
}

protected override void QueryRelatedItem(CommerceQueryRelatedItem queryRelatedItemOperation)
{
CommerceEntity model = queryRelatedItemOperation.GetModel("Payment");
foreach (CommerceEntity entity2 in base.GetResponseCommerceEntities("Basket"))
{
CommerceRelationshipList list = entity2.Properties["Payments"] as CommerceRelationshipList;
if (list == null)
{
list = new CommerceRelationshipList();
entity2.Properties["Payments"] = list;
}
foreach (Payment payment in base.OperationCache.GetCachedCommerceServerOrderGroup(entity2.Id).GetDefaultOrderForm().Payments)
{
CommerceEntity item = new CommerceEntity("Payment");
list.Add(new CommerceRelationship(item));
Translator.ToCommerceEntity(payment, item, model.Properties);
QueryPaymentAccount(queryRelatedItemOperation, item, payment);
}
}
}
}
Now register this in ChannelConfiguration.config by replacing all occurrences of
Microsoft.Commerce.Providers.Components.PaymentsResponseBuilder, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35
with
Rosetta.Crane.CommerceSchemas.OrderExtensions.CustomPaymentsResponseBuilder, Rosetta.Crane.CommerceSchemas.OrderExtensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e2e8fcf336e8629f
Now you can deploy this file to your Commerce website(s).

10 - Update Site Code

I'm going to be much less specific here, because it depends much more on your implementation. The bottom line is, now Commerce knows about your new payment type, and you just need to use it! A gotcha for me was to remember that I had previously implemented a basket query assuming only a credit card payment type. You do have to specifically request each payment account type. For example, this code:
var paymentOperation = new CommerceQueryRelatedItem(Basket.RelationshipName.Payments, CreditCardPayment.ModelNameDefinition);
var creditCardOperation = new CommerceQueryRelatedItem(CreditCardPayment.RelationshipName.PaymentAccount, CreditCard.ModelNameDefinition);
var creditCardPaymentMethod = new CommerceQueryRelatedItem(CreditCard.RelationshipName.PaymentMethod, PaymentMethod.ModelNameDefinition);
creditCardOperation.RelatedOperations.Add(creditCardPaymentMethod);
paymentOperation.RelatedOperations.Add(creditCardOperation);
list.Add(paymentOperation);
becomes:
var paymentOperation = new CommerceQueryRelatedItem(Basket.RelationshipName.Payments, Payment.ModelNameDefinition);
string[] paymentAccounts = {
CreditCard.ModelNameDefinition,
PurchaseOrder.ModelNameDefinition,
PaymentAccount.FreePaymentModelDefinitionName
};
foreach (var model in paymentAccounts)
{
var accountOperation = new CommerceQueryRelatedItem(Payment.RelationshipName.PaymentAccount, model);
var paymentMethodOperation = new CommerceQueryRelatedItem(
PaymentAccount.RelationshipName.PaymentMethod, PaymentMethod.ModelNameDefinition);
accountOperation.RelatedOperations.Add(paymentMethodOperation);
paymentOperation.RelatedOperations.Add(accountOperation);
}
list.Add(paymentOperation);
Conclusion

That should be it, you are now ready to use your custom payment type. Another painless task with Commerce Server 2009!

Back to Commerce Server

I've been away working on custom ASP.NET sites for a while now, but now I'm back and getting up to speed on Commerce Server 2009 for my latest project. This process has been full of challenges and surprises, so I will try to document some of the lessons learned here in the coming months.

Saturday, June 30, 2007

Basket Pipeline Performance

We're nearing the end of development for one of the projects I'm on right now. This site utilized some AJAX and these functions needed to be snappy to really work, so I started looking into tuning performance. After profiling, I found that by far the site's biggest bottleneck was the basket pipeline.

Like most e-commerce sites, this one has a shopping basket summary on every page to show the user how many items they have in their basket, and the basket's subtotal. We were running the basket pipeline before rendering this summary to make sure that the total was up to date, which means that the pipeline was running at least once per page. It also ran additional times if any items were added to the cart during that request. As it turned out, even running it once was our bottleneck by a long shot.

This information led us to a new approach:
  • Whenever the basket is modified (e.g., when an item is added), the basket pipeline will be run and the basket saved.
  • The basket summary will use the totals saved from the last time the pipeline was run. These totals may be stale if any item's price has changed since it was added to the cart.
  • The user will be required to review their cart before checking out. This page will run the basket pipeline again to make sure that the user is being given the most up-to-date prices.
This approach means that the summary might have a stale total if prices change after items are added to the basket, but we considered this acceptable since they are required to review their basket before checking out. Max Akbar also validated this approach, and based on his response I think this is how we are expected to handle these summary controls.

Saturday, June 23, 2007

Targeted Ads Based on User Profile

I recently had to prepare a demonstration of Commerce Server, to include targeted ads. Marketing is one area that I'm not familiar with, and the documentation is a little sparse, so I wanted to share the solution to the major snag I hit: targeting based on the current user's profile. I based the demo on a site that was already displaying ads, and so will assume here that this is your starting point as well. If not, I think you will find enough documentation to get to that point.

First, you'll need to modify one of your regular ads to be targeted:
  1. Add the UserObject targeting context. For the user profile to be available for your targeting expressions, the context needs to be added in the marketing manager. Open the Marketing Manager, and choose the "Expressions" view in the left pane. Then click "Set Targeting Profiles" in the task list, and add the User Object (or whatever profile object represents your user profile) if it is not already present in the right-hand pane.
  2. Create your expression. Click "Create new target expression" in the list of tasks. Choose a name for your expression (123 area code), and build the expression ("UserObject.Telephone Number, Begins with, 123"). Click OK when you're done.
  3. Add the targeting expression to your ad. Open the ad you would like to target, and disapprove it if it is already approved. On the targeting tab, click Add in the "Targeting Expressions" section, and choose your new targeting expression. If you check "Create a Local Copy of this Global Expression", it will make a copy of the expression rather than link to the one you choose. This means that changes to the expression in the future won't affect your ad.
  4. Choose a targeting Action. You just added a condition to your ad. Now you need to specify whether your ad is only included where that condition is true ("Require"), or never shown if it's true ("Exclude"). These are just the simple choices; check MSDN for the full details.
  5. Save, Approve, and close the ad.
Now that the ad is configured, you may need to make some changes to make it appear correctly on your site. You should already have some code written that uses a ContentSelector object to grab a few ads to display. Remember when you had to make sure that the UserObject was available for creating your targeting expression? You have to do essentially the same thing for the ContentSelector. You might think that it will use the CommerceContext.UserId or ComemrceContext.UserProfile to determine the current user, but it does not; you have to tell it which user profile to use by adding it to the Profiles collection. You should end up with something like this:

There you have it. Your targeted ad should now be worked into the set of ads returned by the ContentSelector based on the current user's profile. One more thing though, just in case: tracing. There is a trace mode for the ContentSelector, which you can use to see into the content selection process a bit. Its messages are quite cryptic, but it was useful for me to help troubleshoot why certain ads weren't showing. Here's some example code that will drop the trace messages into the page (borrowed from some Commerce Server boot camp training materials!):

ContentSelector cso = adSelector;
Response.Write(Environment.NewLine + Environment.NewLine + "<hr>");
Response.Write(Environment.NewLine + "<h2>cso Values</h2>");
Response.Write(Environment.NewLine + "ItemsRequested - '" + cso.ItemsRequested + "'<br/>");
Response.Write(Environment.NewLine + "Name - '" + cso.Name + "'<br/>");
Response.Write(Environment.NewLine + "PageHistory - '" + cso.PageHistory + "'<br/>");
Response.Write(Environment.NewLine + "Size - '" + cso.Size + "'<br/>");

Response.Write(Environment.NewLine + Environment.NewLine + "<h2>TraceMessages</h2>");
Response.Write(Environment.NewLine + "<table border=1>");
int i = 0;
foreach (StringCollection strcol in cso.TraceMessages)
{
Response.Write(Environment.NewLine + Environment.NewLine + "<tr><td>Item" + i + "</td><td>");
foreach (string s in strcol)
{
Response.Write(Environment.NewLine + s + "<br/>");
}
Response.Write(Environment.NewLine + "</td></tr>");
i++;
}
Response.Write(Environment.NewLine + "</table>");