By Zoiner Tejada, Hershey Technologies
Articles in this series
Published: March, 2009
Many online music stores allow their customers to purchase
individual songs with a credit card, and the companies encourage a single click
purchase approach that does not force the purchaser to complete a shopping cart
before making payment. This often leads to transactions where users may charge
amounts of less than one dollar, and as the user is shopping, they might make
multiple such small transactions within a short window of time. The merchant
services that are responsible for actually charging the credit cards, and
ultimately pay the online store, usually charge a per transaction fee that is
some combination percentage of total sale plus a minimum flat fee per
transaction. This fee is often over $1. In order to be able to sell items for
less than $1, an approach these online stores take is to batch a user’s
purchases, waiting for some interval to pass and collecting multiple purchases
into one transaction submitted to the merchant service.
Scenario Introduction
In this article we examine the portion of the merchant
service workflow that deals with collecting multiple small transactions under
one order, and when either a time-limit expires, the transaction is “committed”
to the merchant service. Along the way we will demonstrate how to build queue
based custom activities that react to the purchase of an item from the online
store and examine how the differences that surface in using the same activity
in Sequential and State Machine workflows.
From a high level, a user selects an item and makes her
first purchase. This creates an OrderId that will be used to group subsequent
purchases as they occur. When either the window expires or the total amount exceeds
a configured threshold, the workflow automatically commits the order. In real
online stores, if the user then submitted another purchase, a new order would
be started. This could be reflected by the launch of a new workflow instance,
though that is beyond the scope of this sample application.
Sequential Workflow Implementation
The user interface is designed to accommodate the purchase
of individual songs, showing the title and the track cost, both of which can be
freely entered by the user, but serve to simulate the selection of song and
specification of price, as shown in Figure 1.
.jpg)
Figure 1 - Purchasing
user interface
To keep things simple, the item name can be anything, as in
this scenario we are particularly concerned with two values: the cost of the
item purchased and then window of the time that begins when the user makes a
first purchase. When the user first clicks the “Buy It!” button a new workflow
is created that represents that order. Subsequent presses of the button add the
item to the order.
The implementation of the sequential version of this
workflow is quite straightforward, and is shown in Figure 2.
.jpg)
Figure 2 - Sequential
version of the Merchant Service Workflow
In the code behind, the workflow defines the following
variables:
The workflow is created and launched upon the first purchase
and initializes PurchasedItems with a list containing the single item
purchased, Total equal to the item’s cost, OrderId equal to a new GUID and Item
equal to the item. The first activity to execute is the WhileActivity
whileBelowLimits which relies on the following declarative condition:
this.Total <= 50.0m && !this.WaitExpired
This results in the while loop executing so long as the
order total is $50 or less (arbitrarily chosen, in production the threshold
amount would be sourced from configuration) and is still within the order window
(which could be another configurable value). Therefore, if the user makes a
purchase for an item costing more than $50, the workflow will immediately
commit the transaction. Otherwise, it will execute listenActivity which listens
for one of two events to occur, either the delay activity waitInterval’s
duration expires or a user makes another purchase, which causes the execution
of onItemPurchased1, which is a custom activity whose implementation is
detailed in the next section.
In the case that waitInterval executes, the branch
containing onItemPurchased1 is cancelled and setWaitExpired1, which sets the
value of the WaitExpired variable to true, executes. The loop condition will
then be re-evaluated and evaluate to false, causing the commitTransaction
activity to execute and finish the workflow.
In the case that another purchase is made before the delay
expires, the onItemPurchased1 activity executes, which simply receives the item
purchased, adds the item’s cost to the Total variable and the item itself to
the PurchasedItems variable. Again, the while condition is evaluated, and if
the Total threshold is exceeded the workflow will complete after executing
commitTransaction, which is merely a place holder custom activity that could be
replaced with one that makes an actual call to the merchant service. When the threshold is not exceeded, the delay
will be reset and the workflow waits for the next event (either the delay
expiring or an item being purchased).
Using Queues To Communicate With Workflows
One approach used in previous articles to communicate data
from external sources into a workflow instance was to use the
ExternalDataExchangeService, paired with local services that would be called by
external stimuli and would trigger the execution of HandleExternalEvent
activities registered to listen for the event raised. While one can certainly
build custom activities that follow this pattern, and replace the
HandleExternalEvent activity in a workflow, there is an alternative that
removes the need to utilize the ExternalDataExchangeService all together.
One can build custom activities that register with the
Workflow Runtime and listen for data to be input into specific, named queues.
When such an event occurs, the Workflow Runtime resumes the workflow instance
and provides the data item to the waiting custom activity. There are two
interfaces that a custom activity such as OnItemPurchased needs to implement to
be event driven in this manner:
public interface IEventActivity
{
IComparable QueueName { get; }
void Subscribe(ActivityExecutionContext parentContext, IActivityEventListener<QueueEventArgs> parentEventHandler);
void Unsubscribe(ActivityExecutionContext parentContext, IActivityEventListener<QueueEventArgs> parentEventHandler);
}
public interface IActivityEventListener<QueueEventArgs>
{
void OnEvent(object sender, QueueEventArgs e);
}
In the OnItemPurchased custom activity, we create a queue
named after the Order ID. It is this string value that is set as the value for
QueueName when the OnItemPurchased activity is first created.
protected override void Initialize(IServiceProvider provider)
{
this.QueueName = OrderId;
}
In the Subscribe implementation, we use the
WorkflowQueuingService (which is acquired from the Activity Execution Context
passed into Subscribe) to check if the queue already exists, and to create it
if it does not.
public void Subscribe(ActivityExecutionContext parentContext, IActivityEventListener<QueueEventArgs> parentEventHandler)
{
WorkflowQueue workflowQueue = CreateQueue(parentContext);
workflowQueue.RegisterForQueueItemAvailable(parentEventHandler);
}
private WorkflowQueue CreateQueue(ActivityExecutionContext context)
{
WorkflowQueuingService qService = context.GetService<WorkflowQueuingService>();
if (!qService.Exists(this.QueueName))
{
qService.CreateWorkflowQueue(this.QueueName, true);
}
return qService.GetWorkflowQueue(this.QueueName);
}
Depending on where the OnItemPurchased activity is placed,
the activity which is registered (e.g., the value of parentEventHandler in the
Subscribe method) to respond to the queued data item may be either the
activity’s parent (such as in the case of a ListenActivity) or the activity
itself (such as when the activity stands alone in a sequence). In the former
case, the parent will simply schedule OnItemPurchased for execution, and
OnItemPurchased will run through its Execute method (shown below) as if it was
not an event driven activity at all- the implementation for OnEvent will not be
called. In the latter case, OnItemPurchased will run through its Execute
method, but subscribe itself as the event handler. When data arrives in the
queue, the OnItemPurchased.OnEvent will execute.
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
if (ProcessQueueItem(executionContext))
{
return ActivityExecutionStatus.Closed;
}
Subscribe(executionContext, this);
return ActivityExecutionStatus.Executing;
}
public void OnEvent(object sender, QueueEventArgs e)
{
// If activity is not scheduled for execution, do nothing
if (this.ExecutionStatus == ActivityExecutionStatus.Executing)
{
ActivityExecutionContext context = sender as ActivityExecutionContext;
if (this.ProcessQueueItem(context))
{
context.CloseActivity();
}
}
}
The actual work performed by the OnItemPurchased activity
occurs within the ProcessQueueItem method, where we get the item from the named
workflow queue, and update the variables of the activity (which are bound to
the corresponding variables on the workflow).
private bool ProcessQueueItem(ActivityExecutionContext context)
{
WorkflowQueuingService qService = context.GetService<WorkflowQueuingService>();
if (!qService.Exists(this.QueueName))
{
return false;
}
WorkflowQueue queue = qService.GetWorkflowQueue(this.QueueName);
if (queue.Count == 0)
{
return false;
}
Item purchasedItem = queue.Dequeue() as Item;
this.Item = purchasedItem;
this.PurchasedItems.Add(Item);
this.Total += purchasedItem.ItemCost;
Unsubscribe(context, this);
DeleteQueue(context);
return true;
}
Finally, when the activity has done its work or been
canceled, we need to do some housekeeping which involves unsubscribing from
events for the queue and deleting the queue.
protected override ActivityExecutionStatus Cancel(ActivityExecutionContext context)
{
Unsubscribe(context, this);
DeleteQueue(context);
return ActivityExecutionStatus.Closed;
}
public void Unsubscribe(ActivityExecutionContext parentContext, IActivityEventListener<QueueEventArgs> parentEventHandler)
{
WorkflowQueuingService qService = parentContext.GetService<WorkflowQueuingService>();
if (qService.Exists(this.QueueName))
{
WorkflowQueue workflowQueue = qService.GetWorkflowQueue(this.QueueName);
workflowQueue.UnregisterForQueueItemAvailable(
parentEventHandler);
}
}
private void DeleteQueue(ActivityExecutionContext context)
{
WorkflowQueuingService qService = context.GetService<WorkflowQueuingService>();
qService.DeleteWorkflowQueue(this.QueueName);
}
Here too it is important to note, that Unsubscribe may be
called by the activity itself or its parent in the same approach as described
for Subscribe.
With an understanding of how OnItemPurchased is implemented,
let’s turn our attention to using it in the State Machine Workflow.
State Machine Implementation
The state machine merchant service workflow (see Figure 3)
defines the same variables as the sequential version, excepting WaitExpired
because it does not rely on awhile loop that would need this.
.jpg)
Figure 3 - State
Machine version of the Merchant Service workflow
The workflow begins as before, and immediately executes the
State Initialization activity BeginPurchase that checks if the cost of the
initial item purchased exceeds the order total threshold (using the same
condition as was used by the sequential version). Figure 4 below shows this.
.jpg)
Figure 4 - Handling
of initial purchase
If it does, it transitions directly to the CommitPurchases
state (which could include other activities that actually make the call to the
merchant service). Otherwise, it transitions to the PurchasingState, where it
waits for either an item to be purchased (Figure 5) or the delay to expire
(Figure 6).
.jpg)
Figure 5 - Handling
of a subsequent purchase
.jpg)
Figure 6 - Handling
of the delay expiration
Note that in this sequence, the onItemPurchased1 activity is
hosted within the ItemPurchased EventDriven activity (figure 5), just as the
delay is hosted within WaitForExpiration (figure 6). When a subsequent purchase
occurs and ItemPurchased is triggered, the Total threshold is evaluated again
and if it is not exceeded, the workflow loops back to the PurchasingState to
wait again. However, if it is exceeded the workflow transitions to the
CommitPurchases state and completes.
There are a few observations to be made about using the
OnItemPurchased activity in the State Machine Workflow. First, there is no way
to place it outside of anything but an EventDriven activity. In terms of its
implementation, this means in a state machine there will always be an
EventDriven activity that will schedule OnItemPurchased for execution and call
Subscribe/Unsubscribe. Second, while we might want to have more than one
OnItemPurchased activity in an EventDriven sequence, that is not allowed. There
can be only one activity that implements IEventActivity and it must be the
first activity in the sequence. This is different from a sequential workflow,
where the OnItemPurchased activity could be used multiple times directly in the
sequence.
Related Links