By Zoiner Tejada, Hershey Technologies
Articles in this series
Published: February, 2009
In this article we explore an alternative use of client-side
workflows, whereby the workflows themselves are computational in nature, and it
is the ability to model the computation logic that creates the preference over
procedural programming.
Computational workflows can be compared to functions in
procedural code: they take as input some parameters, process them and return a
set of values representing the result. They are different from their human
workflow cousins, in that they usually execute in a single burst and return a
result without persisting. In addition, they differ from page flow scenarios,
because they are not strictly speaking driving the presentation layer, but are
instead an implementation of the business logic. A concrete example of a
computational workflow can be seen in the requirements of calendar scheduling,
and this is the business scenario explored.
Scenario Introduction
Busy schedules are a fact of doing business, and the growing
availability and sophistication of calendaring applications such as that
offered via Outlook and Exchange ultimately serve the purpose of ensuring that
the right people can get together, at the right place, at the right time.
Sometimes, however, it can be quite difficult to coordinate that initial
appointment that works for most everyone involved. It turns out that the logic for assisting
users to schedule an appointment is well modeled by a computational workflow.
In this particular case, we focus on the problem of
selecting the initial group of attendees and time frame. Given that
information, we can execute a workflow that queries a calendar server such as
Exchange for each attendee’s Free/Busy information. If we are lucky, then all
of the attendees are available and the appointment request gets sent to the
attendees for their confirmation. Otherwise, there is value in the system
helping us choose the next best time that works for all involved, or even
recommending who, by making their attendance optional, might increase the
chance of fully confirmed meeting on the first proposal. Figure 1 illustrates
this flow from a high level.
.jpg)
Figure 1 - High level
Scheduling Assistant Workflow
We will explore the implementation of this workflow using
both Sequential and State Machine incarnations, and in the course explore some
of the relevant exception handling approaches used to make the computational
workflow more resilient.
Inputs & Outputs
Because we are modeling this as a computational workflow, it
helps to look at a sampling of the inputs and outputs of the workflow. The
following table briefly summarizes the users used in this scenario, and how the
calendaring service represents their Free/Busy information:
Table 1 - Users and
their "availability" in the simulation
Figure 2 shows all the inputs (Subject, Location, Time Frame
and Attendees). The left list below Attendees enumerates the attendees who can
be scheduled, and the list on the right is where selected attendees are moved
from the left list and configured as required or optional.
.jpg)
Figure 2 - Scheduling
an appointment
These are all packaged into a single type AppointmentRequest
that is used to initialize the workflow:
[Serializable]
public class AppointmentRequest
{
public string Subject { get; set; }
public string Location {get;set;}
public DateTime Start { get; set; }
public DateTime End { get; set; }
public ObservableCollection<Attendee> Attendees { get; set; }
}
[Serializable]
public class Attendee
{
public string Name { get; set; }
public string Email { get; set; }
public bool Required { get; set; }
}
Parameters can be passed into a new workflow instance via
the WorkflowRuntime.CreateWorkflow overload which takes the type of the
workflow to create, and a Dictionary<string, object> where each string
key must match the name of a public property on the workflow, the value object
can be any serializable type. This workflow exposes a public property of type
AppointmentRequest, with the name AppointmentRequest.
There is no concrete difference between input and output
workflows. Our Scheduling Assistant workflow has a public property of type
FreeBusyResults that is interrogated when the workflow completes (by handling
the WorkflowCompleted event and reading the value from the OutputParameters
dictionary). This output is used to suggest schedule alternatives when
appropriate. For example, running the workflow with the input from Figure 2,
produces the suggestions shown in Figure 3. In this case the workflow returns
six results. The first three suggest later start times can accommodate all
users. The last three suggest times made available by making the user called
“Never There” an optional attendee.
.jpg)
Figure 3 - Resulting
alternatives to Figure 2
Note that this workflow is a simulation of the approach one
would take to compute such suggestions and does not itself employ any
sophisticated heuristics or neural networks to provide the result. Instead we focus on how such logic might be
embedded and executed by a computation workflow.
With the inputs and outputs in mind, let’s turn to the
implementation of the workflow, starting with the sequential version.
Scheduling Assistant Workflow – Sequential Version
The workflow itself is relatively simple, on account of the
use of two complex custom activities, and is almost completely described by
Figure 4.
.jpg)
Figure 4 - The
Scheduling Assistant Workflow in Sequential form.
When the computational workflow is launched the with
appointment request, it first executes the Replicator called ProcessAllSchedules.
This replicator runs one copy of the LookupFreeBusy custom activity for each
attendee requested, which in turn simulates communication with the calendar
server to get a simple Boolean answer for each user: free or not? If all
attendees marked as required are in fact free, then the workflow goes ahead and
sends out the appointment requests as any calendaring application might
(simulated via the no-op SendAppointments activity). In this case, the workflow
does not return any alternatives. However, if at least on required attendee is
unavailable, then the workflow executes the SuggestAlternatives custom activity
which executes logic to determine viable alternatives from the input provided.
The activity itself has some pre-programmed decisions which guide it in
building this list of alternatives. In the real-world, this is where one might
stick the sophisticated logic using a form of solver, neural networks or even
make a service call to perform the computation.
An interesting facet of this workflow is the error handling.
In this case, there are two exceptions the workflow is concerned with and can
react to: being unable to communicate with the server (throws a
CalendarServerCommunicationException) and not be authorized to lookup a
person’s Free/Busy information (which throws a
CalendarAccessNotAllowedException). Recall by default a workflow with an
unhandled exception will terminate, so where possible you want to use fault
handlers, which behave similarly to try/catch blocks, to make your workflows
resilient.
The CalendarAccessNotAllowedException is only thrown by the
LookupFreeBusy activity. It is not thrown by the SuggestAlternatives custom
activity as one might suspect, because that activity handles it internally and
instead suggest that the attendee be marked as optional. Therefore, we handle
this exception in the closest parent of an activity offering a
FaultHandlersActivity, which means the ProcessAllSchedules sequence as shown:
.jpg)
Figure 5 - Handling
CalendarAccessNotAllowedException.
To define a fault handler, we must first add a
FaultHandlerActivity to the FaultHandlersActivity. As shown in figure 5, the
FaultHandlersActivity is created as HandleLookupFailures and, to its strip, we
have added the single FaultHandlerActivity HandleNotAllowed (highlighted in
blue). The fault handler is configured to execute the sequence described by
HandleNotAllowed, which runs a custom activity that simply checks if the
attendee was required or not, and if so configures the workflow state to
indicate that. Effectively, not all required attendees could be scheduled so
the workflow recommends making the attendee optional in subsequent steps. This
handler keeps the exception from bubbling up any higher and allows other
instances of LookupFreeBusy and subsequent activities to execute normally.
A CalendarServerCommunicationException can be thrown by
either of the two activities which query the calendar server. In this case, we
simply decided to complete the workflow (e.g., not terminate) and show the
communication error message to the user rather than encode retry logic in the
error handler. In a sequential workflow, the best place to put a common, root
level error handler such is this is at the root sequence and this is what is
shown in Figure 5. The SetFaultResult activity within the FaultHandlerActivity
HandleCommunicationFault simply sets the Fault property on the FreeBusyResult
output property with the exception.
.jpg)
Figure 6 - Handling a
CalendarServerCommunicationException.
Next, we explore how this workflow might be expressed as a
state machine.
Scheduling Assistant Workflow – State Machine Version
For computational workflows, sequential workflows are
usually more practical to model the single burst of execution. State Machines
are geared more towards handling the specific events that cause the transition
between states-- in a computational workflow where there are few to no
intermediate events the state machine may be as simple as a single state or a
series of connected states linked by State Initialization activities that
effectively describe a sequence using state machine semantics. The latter
approach was followed and is shown in Figure 6.
.jpg)
Figure 7 - The
Scheduling Assistant Workflow in State Machine form.
This biggest difference in implementation between the
sequential and state machine version has to do with the handling of
workflow-level exceptions, such as the CalendarServerCommunicationException.
Unlike sequential workflows, the root State Machine Workflow has no place to
express an exception handler and you can only add exception handlers to
sequences of StateInitialization, EventDriven and StateFinalization. To handle
the exception required us to add fault handlers within both
InitProcessAllSchedules and InitSuggestAlternatives, as shown in Figure 8 and
Figure 9 respectively. Note that both perform exactly the same function, but
must be duplicated because the state machine affords no place more global in
which to define a common exception handler.
.jpg)
Figure 8 - Handling a
CalendarServerCommunicationException within InitProcessAllSchedules
.jpg)
Figure 9 - Handling a
CalendarServerCommunicationException within InitSuggestAlternatives
Related Links