Note: This section explains how to work with shopping carts using the older APIs. Episerver recommends using the abstraction APIs to manage shopping carts, as described in Shopping carts.
The shopping cart (or basket) is where the shopping and ordering process starts. This topic describes the main components of the shopping cart in Episerver Commerce.
Classes in this topic are available in the following namespaces:
- Mediachase.Commerce.Orders
- Mediachase.Commerce.Catalog
- Mediachase.Commerce.Marketing
- Mediachase.Commerce.Website.Helpers
Key files and controls
The Episerver Commerce sample website contains a number of template controls and helper classes for managing carts and the checkout procedure.
- Commerce front-end website under \wwwroot\Templates\Sample\Units\CartCheckout, for cart and checkout examples.
- Commerce front-end website under \wwwroot\Templates\Sample\Pages for wishlist examples.
- Cart.cs located in the Mediachase.Commerce.Orders namespace, is the object containing the references to related object collections. It is a meta-class and inherits from OrderGroup, which contains a collection of OrderForm and OrderAddress objects.
- CartHelper.cs in the Mediachase.Commerce.Website package in the Commerce tools and code sample download package. The CartHelper class lets you do common tasks with carts efficiently, such as add a line item to a cart, or add an address to a cart (for example, shipping or billing).
Cart object
You can associate multiple carts with a single customer. Each cart associated with a customer is named with a string value. By default, the sample Episerver Commerce site uses the static string Cart.DefaultName property to name the default cart. The wishlist is named using the static string CartHelper.WishListName property. However, you do not have to follow that convention.
Accessing and adding cart Items
A cart contains one or more OrderForm instances in the OrderForms property; a single OrderForm is usually sufficient for most sites. When an SKU is added to the cart, SKU information is put into a LineItem object in one of the cart's OrderForms. CartHelper demonstrates creating a LineItem instance and adding it to a cart in the AddEntry() method.
Example: creating a LineItem, adding it to an OrderForm, then adding the OrderForm to a Cart
//If GetCart can't find the cart, it will create it for you
Cart defaultCart = OrderContext.Current.GetCart(Cart.DefaultName, SecurityContext.Current.CurrentUserId);
//manually create an orderform and add a lineitem to it
orderForm = new OrderForm();
lineItem = new LineItem();
lineItem.CatalogEntryId = "theCodeForOurSku";
lineItem.DisplayName = "mySKUDisplayName";
orderForm.LineItems.Add(lineItem);
//add the orderform to the cart
defaultCart.OrderForms.Add(orderForm);
//now save the changes to the database
defaultCart.AcceptChanges();
Example: using CartHelper to do the same task
//retrieve the default cart with the helper
//when initialized, it adds an orderform to the cart
helper = new CartHelper(Cart.DefaultName);
//this method automatically transfers built-in properties (not metafield values);
//This method automatically saves the changes to the database
//the SKU variable is an Entry object representing a SKU
helper.AddEntry(SKU);
Calculating totals workflow
To calculate shopping cart and order totals, ECF uses Microsoft Workflow Foundation. The workflow separates the business logic associated with calculating cart totals and validating the cart. The workflow associated with the shopping cart is CartValidate. It is executed every time the cart is loaded in the CartView.ascx control. Because carts are persisted in the database, the availability and pricing for items can change over time. Workflows are executing the Cart.RunWorkflow() method, passing in the name of the workflow to be executed.
The CartValidate workflow addresses these issues and calculates totals by doing the following:
- Determines whether the item is still available, based on the Active status and the start and end dates associated with each item. If items are not available, they are removed from the cart, and an error message is returned and displayed in the CartView.ascx.
- Whether the items in the cart are still available (based on remaining stock in inventory, reserved inventory stock, and whether backordering is permitted). If they are not available, they are removed and an error message is returned.
- The price for each cart item, based on tiered pricing. If pricing has changed, a message is returned that can be displayed to the user regarding the change.
- Calculates the extended price for a cart item, for instance multiplying price by quantity purchased.
- Calculates discounts that apply to cart items.
The workflow updates a number of cart properties to indicate various totals. These properties let you display the price for a line item, the discount amount, and the after-discount-price. These include:
- Cart.SubTotal. Cart total after discounts are applied.
- LineItem.ListPrice. Store msrp for calculating margins in reports or the placed price.
- LineItem.PlacedPrice. Price before any discounts.
- LineItem.ExtendedPrice. Price for a LineItem, given the quantity of the item and applicable discounts.
- LineItem.LineItemDiscountAmount . Total discount amount for a LineItem.
The following example shows a workflow activity (CheckInventoryActivity.cs) that performs an availability check on the SKUs in the cart based on inventory status. Then, it removes unavailable cart items.
Example: checking availability, inventory status and cart update
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing;
using System.Web.Security;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Mediachase.Commerce.Orders;
using Mediachase.Commerce.Marketing;
using Mediachase.Commerce.Marketing.Objects;
using Mediachase.Commerce.Catalog;
using Mediachase.Commerce.Catalog.Dto;
using Mediachase.Commerce.Customers.Profile;
using Mediachase.Commerce.Catalog.Managers;
namespace Mediachase.Commerce.Workflow.Activities.Cart
{
public partial class CheckInventoryActivity : Activity
{
public static DependencyProperty OrderGroupProperty = DependencyProperty.Register("OrderGroup", typeof(OrderGroup), typeof(CheckInventoryActivity));
public static DependencyProperty WarningsProperty = DependencyProperty.Register("Warnings", typeof(StringDictionary), yypeof(CheckInventoryActivity));
///
/// Gets or sets the order group.
///
/// The order group.
[ValidationOption(ValidationOption.Required)]
[BrowsableAttribute(true)]
public OrderGroup OrderGroup
{
get
{
return (OrderGroup)(base.GetValue(CheckInventoryActivity.OrderGroupProperty));
}
set
{
base.SetValue(CheckInventoryActivity.OrderGroupProperty, value);
}
}
///
/// Gets or sets the warnings.
///
/// The warnings.
[ValidationOption(ValidationOption.Required)]
[BrowsableAttribute(true)]
public StringDictionary Warnings
{
get
{
return (StringDictionary) base.GetValue(CheckInventoryActivity.WarningsProperty));
}
set
{
base.SetValue(CheckInventoryActivity.WarningsProperty, value)
}
}
///
/// Initializes a new instance of the class.
///
public CheckInventoryActivity()
{
InitializeComponent();
}
///
/// Called by the workflow runtime to execute an activity.
///
/// The to associate with this and execution.
///
/// The of the run task, which determines whether the activity remains in the executing state, or transitions to the closed state.
///
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
try
{
// Validate the properties at runtime
this.ValidateRuntime();
// Calculate order discounts
this.ValidateItems();
// Retun the closed status indicating that this activity is complete.
return ActivityExecutionStatus.Closed;
}
catch
{
// An unhandled exception occured. Throw it back to the WorkflowRuntime.
throw;
}
}
///
/// Validates the items.
///
private void ValidateItems()
{
foreach (OrderForm form in OrderGroup.OrderForms)
{
if (form.Name != Mediachase.Commerce.Orders.Cart.DefaultName) //We don't need to validate quantity in the wishlist
{
continue;
}
foreach (LineItem lineItem in form.LineItems)
{
if (lineItem.CatalogEntryId != "0" && !String.IsNullOrEmpty(lineItem.CatalogEntryId) && !lineItem.CatalogEntryId.StartsWith("@")) // ignore custom entries
{
if (lineItem.InventoryStatus != InventoryStatus.Enabled)
{
continue;
}
// Check Inventory
// item exists with appropriate quantity
if (lineItem.InStockQuantity >= lineItem.Quantity)
{
continue;
}
else if (lineItem.InStockQuantity > 0) // there still exist items in stock
{
// check if we can backorder some items
if (lineItem.AllowBackordersAndPreorders)
{
if (lineItem.InStockQuantity + lineItem.BackorderQuantity >= lineItem.Quantity)
{
continue;
}
else
{
lineItem.Quantity = lineItem.InStockQuantity + lineItem.BackorderQuantity;
Warnings.Add("LineItemQtyChanged-" + lineItem.Id.ToString(), String.Format("Item \"{0}\" quantity has been changed, some items might be backordered.", lineItem.DisplayName));
continue;
}
}
else
{
lineItem.Quantity = lineItem.InStockQuantity;
Warnings.Add("LineItemQtyChanged-" + lineItem.Id.ToString(), String.Format("Item \"{0}\" quantity has been changed.", lineItem.DisplayName));
continue;
}
}
else if (lineItem.InStockQuantity == 0)
{
if (lineItem.AllowBackordersAndPreorders && lineItem.PreorderQuantity > 0)
{
if (lineItem.PreorderQuantity >= lineItem.Quantity)
{
continue;
}
else
{
lineItem.Quantity = lineItem.PreorderQuantity;
Warnings.Add("LineItemQtyChanged-" + lineItem.Id.ToString(), String.Format("Item \"{0}\" quantity has been changed.", lineItem.DisplayName));
continue;
}
}
else if (lineItem.AllowBackordersAndPreorders && lineItem.BackorderQuantity > 0)
{
if (lineItem.BackorderQuantity >= lineItem.Quantity)
{
continue;
}
else
{
lineItem.Quantity = lineItem.BackorderQuantity;
Warnings.Add("LineItemQtyChanged-" + lineItem.Id.ToString(), String.Format("Item \"{0}\" quantity has been changed.", lineItem.DisplayName));
continue;
}
}
}
// Remove item if it reached this stage
Warnings.Add("LineItemRemoved-" + lineItem.Id.ToString(), String.Format("Item \"{0}\" has been removed from the cart because it is no longer available.", lineItem.DisplayName));
// Delete item
lineItem.Delete();
}
}
}
}
///
/// Validates the runtime.
///
private bool ValidateRuntime()
{
// Create a new collection for storing the validation errors
ValidationErrorCollection validationErrors = new ValidationErrorCollection();
// Validate the Order Properties
this.ValidateOrderProperties(validationErrors);
// Raise an exception if we have ValidationErrors
if (validationErrors.HasErrors)
{
string validationErrorsMessage = String.Empty;
foreach (ValidationError error in validationErrors)
{
validationErrorsMessage +=
string.Format("Validation Error: Number {0} - '{1}' \n",
error.ErrorNumber, error.ErrorText);
}
// Throw a new exception with the validation errors.
throw new WorkflowValidationFailedException(validationErrorsMessage, validationErrors);
}
// If we made it this far, then the data must be valid.
return true;
}
///
/// Validates the order properties.
///
/// The validation errors.
private void ValidateOrderProperties(ValidationErrorCollection validationErrors)
{
// Validate the To property
if (this.OrderGroup == null)
{
ValidationError validationError = ValidationError.GetNotSetValidationError(CheckInventoryActivity.OrderGroupProperty.Name);
validationErrors.Add(validationError);
}
}
}
}
Cart persistence
Carts are always stored in the database. To save a cart, call the AcceptChanges() method on the Cart object.
Anonymous users are managed using the ASP.NET anonymous personalization feature. This is configured in the web.config file, using <anonymousIdentification enabled="true" />. This stores a unique ID (GUID) in a cookie, which provides a temporary ID for the user. The cookie lifetime is configured in the authentication element of the web.config file, as shown in the following snippet:
<authentication mode="Forms">
<forms timeout="4320" loginUrl="~/Login.aspx" name="EPiServer-CMS">
</forms>
</authentication>
In the example, the cookie lifespan is configured for 4,320 seconds, or 3 days. The anonymous ID is used as the userId when storing a cart in the database for anonymous users. To access the user ID (anonymous or logged in), use SecurityContext.Current.CurrentUserId.
If an anonymous user adds items and then logs in, the previous cart associated with that logged-in user is merged. This is done in the Global.asax, Profile_MigrateAnonymous event handler.
Wishlist
A wishlist is just a cart with a different name.
Example: displaying and retrieving a wishlist cart
Cart wishlistCart = OrderContext.Current.GetCart(CartHelper.WishListName, SecurityContext.Current.CurrentUserId);
Last updated: Apr 01, 2021