In my DropThings project, I have used
Workflows to develop the business layer that run synchronously and
do most of the work in the middle-tier. The business layer facade
named DashboardFacade has no code but to call different
workflows. Each of the workflow serve a particular operation like
new user visit, existing user visit, adding a tab, moving a widget
from one column to another etc. ASP.NET page calls
DashboardFacade for each user action and
DashboardFacade inturn calls a workflow to respond to that
user action.
“This is insane!” you are thinking. I know. Please
hear me out why I went for this approach:
Architects can “Design” business facade
functions in terms of Activities and developers can just fill
in small amount of unit code in each activity.
This is a really good reason because Architects can save time in
writing Word Documents explaining how things should work. They can
directly go into Workflow Designer, design the activities, connect
them, design the flow, and verify whether all input and output are
properly mapped or not. This is lot better than drawing
flow-charts, writing pseudocode, explaining in stylish text how an
operation should work. It is also helpful for developers because
they can see the workflow and easily understand how to craft the
whole operation. They just open up each activity and write small
amount of very specific reusable code. They know what will be the
input to the Activity (like function parameters) and they know what
to produce (return value of function). This makes the activities
reusable and architects can reuse one activity in many workflows.
Workflows can be debugged right on Visual Studio Workflow Designer.
So, developers can easily find out defects in their implementation
by debugging the workflow. Architects can enforce many standards
like validations, input output check, Fault handling on the
workflow. Developers cannot but comply with those and thus produce
really good code. Another great benefit for both architect and
developer is that there’s no need to keep a separate
technical specification document up-to-date because the workflow is
always up-to-date and it speaks for itself. If someone wants to
study how a particular operation works, one can just printout the
workflow and read it through.
“But what about performance”, you say? Both you and
I have heard workflow foundation is a pretty big library and can be
memory hog. Also the workflow runtime is quite big and takes time
to start up. I did some profiling on the overhead of workflow
execution and it is very fast for synchronous execution. Here’s
proof from the log you get in Visual Studio output window:
1:
b030692b-5181-41f9-a0c3-69ce309d9806 Activity: Get User Guid
0.078125
2:
b030692b-5181-41f9-a0c3-69ce309d9806 Activity: Get User Pages
0.0625
3:
b030692b-5181-41f9-a0c3-69ce309d9806 Activity: Get User Setting
0.046875
4:
b030692b-5181-41f9-a0c3-69ce309d9806 Activity: Get Widgets in page:
189 0.0625
5:
b030692b-5181-41f9-a0c3-69ce309d9806 Total: Existing user visit
0.265625
First four entries are the time taken by individual activities
during data access only, not the total time taken to execute the
whole activity. The time entries here are in seconds and the first
four entries represent duration of database operations inside
activities. The last one is the total time for running a workflow
with the four activities shown above and some extra code. If you
sum up all the individual activity execution time for database
operations only, it is 0.2500 which is just 0.015625 sec less than
the total execution time. This means, executing the workflow itself
along with overhead of running activities takes around 0.015 sec
which is almost nothing compared to the total effort of doing the
complete operation.
An example of such workflow is as following:
1:
public
void MoveWidgetInstance(
int widgetInstanceId,
int toColumn,
int toRow )
2: {
3:
using(
new TimedLog(
this._UserName,
"Move Widget:" + widgetInstanceId) )
4: {
5: var properties =
new Dictionary<
string,
object>();
6: properties.Add(
"UserName",
this._UserName);
7: properties.Add(
"WidgetInstanceId", widgetInstanceId);
8: properties.Add(
"ColumnNo", toColumn);
9: properties.Add(
"RowNo", toRow);
10:
11: WorkflowHelper.ExecuteWorkflow(
typeof( MoveWidgetInstanceWorkflow ),
properties );
12: }
13: }
MoveWidgetInstance is a method in
DashboardFacade. It just calls a workflow named
MoveWidgetInstanceWorkflow which does the job for moving a
widget from one column to another, rearrange the widgets on the new
column, pull up widgets on the old column to remove the empty
space, and update all widgets positions. If I wrote all these
code in one function, it becomes quite a big function and requires
documentation. But having a workflow means I have a flowchart of
what’s going on and I have some handy reusable Activities which I
can reuse in other operations.
Implementing business layer as workflows has three important
requirements:
- Execute workflow synchronoulsy within the ASP.NET request
thread
- Get output parameters from workflow which is returned as return
value from Business Facade methods
- Get exceptions raised from Activities in a synchronous manner
inside the same request thread
WorkflowHelper is a handy class I made, which makes use
of Workflow a breeze, especially from ASP.NET. In the business
layer, I need synchronous execution of workflow where the default
implementation of Workflow Foundation is to work asynchronously.
Moreover, I need return values from Workflows after their execution
complete, which is not so easily supported due to asynchronous
nature of workflow. Both of these require some tweaking with
workflow runtime in order to successfully run in ASP.NET
environment. You will find the source code of WorkflowHelper in the
code zip file from www.dropthings.com.
WorkflowHelper.Init function initializes Workflow
Runtime for ASP.NET environment. It makes sure there’s only
one workflow runtime per Application Domain. Workflow Runtime
cannot be created twice in the same application domain. So, it
stores the reference of the Workflow Runtime in Application
Context.
1:
public
static WorkflowRuntime Init()
2: {
3: WorkflowRuntime workflowRuntime;
4:
5:
// Running in local mode, create an return new
runtime
6:
if( HttpContext.Current ==
null )
7: workflowRuntime =
new WorkflowRuntime();
8:
else
9: {
10:
// running in web mode, runtime is initialized
only once per
11:
// application
12:
if( HttpContext.Current.Application[
"WorkflowRuntime"] ==
null )
13: workflowRuntime =
new WorkflowRuntime();
14:
else
15:
return HttpContext.Current.Application[
"WorkflowRuntime"]
as WorkflowRuntime;
16: }
The initialization takes care of both ASP.NET and
Console/Winforms mode. After the initialization, it registers
ManualWorkflowSchedulerService, which take care of
synchronous execution of Workflow.
Activities.CallWorkflowService is a handy
service that executes workflow synchrnously from
another workflow.
Read this post for details. These two services make
Workflow Foundation usable from ASP.NET environment for practical
scenarios.
1: var manualService =
new ManualWorkflowSchedulerService();
2:
workflowRuntime.AddService(manualService);
3:
4: var syncCallService =
new Activities.CallWorkflowService();
5:
workflowRuntime.AddService(syncCallService);
6:
7: workflowRuntime.StartRuntime();
8:
9:
// on web mode, store the runtime in application
context so that
10:
// it is initialized only once. On dekstop mode,
ignore
11:
if(
null != HttpContext.Current )
12: HttpContext.Current.Application[
"WorkflowRuntime"] = workflowRuntime;
13:
14:
return workflowRuntime;
15:
Workflow Runtime is initialized from Application_Start event in
Global.asax. This ensures the initialization happens only once per
App Domain.
1:
void Application_Start(
object sender, EventArgs e)
2: {
3:
// Code that runs on application startup
4:
5:
DashboardBusiness.WorkflowHelper.Init();
6: }
The runtime is disposed from Application_End event in
Gloabal.asax:
1:
void Application_End(
object sender, EventArgs e)
2: {
3:
// Code that runs on application shutdown
4:
DashboardBusiness.WorkflowHelper.Terminate();
5: }
The most interesting function is the ExecuteWorkflow function
which does the following:
- Execute workflow synchronously
- Pass parameters to workflow
- Upon completion, get output parameters from workflow and return
them
Handle exceptions raised in Workflow and raise to ASP.NET exception
handler
First ExecuteWorkflow creates an instance of Workflow and passes
input parameters to it:
1:
public
static
void ExecuteWorkflow( Type workflowType,
Dictionary<
string,
object> properties)
2: {
3: WorkflowRuntime workflowRuntime =
Init();
4:
5: ManualWorkflowSchedulerService
manualScheduler =
workflowRuntime.GetService();
6:
7: WorkflowInstance instance =
workflowRuntime.CreateWorkflow(workflowType, properties);
8: instance.Start();
ManualWorkflowSchedulerService service executes the workflow
synchronously. Next step is to hook WorkflowCompleted and
WorkflowTerminated events of Workflow Runtime so that I can capture
output parameters and exceptions and handle them properly.
1:
EventHandler completedHandler =
null;
2: completedHandler =
delegate(
object o, WorkflowCompletedEventArgs e)
3: {
4:
if (e.WorkflowInstance.InstanceId
==instance.InstanceId)
5: {
6:
workflowRuntime.WorkflowCompleted -= completedHandler;
7:
8:
// copy the output parameters in the specified
properties dictionary
9: Dictionary<
string,
object>.Enumerator enumerator =
e.OutputParameters.GetEnumerator();
10:
while( enumerator.MoveNext() )
11: {
12: KeyValuePair<
string,
object> pair = enumerator.Current;
13:
if( properties.ContainsKey(pair.Key) )
14: {
15: properties[pair.Key] =
pair.Value;
16: }
17: }
18: }
19: };
When workflow completes, WorkflowCompletedEventArgs gives me the
OutputParameters dictionary. It contains all the public properties
of Workflow. I read all the entries in the OutputParameters and
update the input parameters Dictionary with the new values. This is
required in the AddWidget function of DashboardFacade where I need
to know the widget instance created by the workflow.
WorkflowTerminated fires when there’s an exception. When
any activity inside the workflow raises exception, this event fires
and workflow execution aborts. I capture this exception and throw
it again so that ASP.NET can trap this exception using its default
exception handler.
1: Exception x =
null;
2:
EventHandler terminatedHandler =
null;
3: terminatedHandler =
delegate(
object o, WorkflowTerminatedEventArgs e)
4: {
5:
if (e.WorkflowInstance.InstanceId ==
instance.InstanceId)
6: {
7:
workflowRuntime.WorkflowTerminated -= terminatedHandler;
8: Debug.WriteLine( e.Exception );
9:
10: x = e.Exception;
11: }
12: };
13: workflowRuntime.WorkflowCompleted
+= completedHandler;
14: workflowRuntime.WorkflowTerminated
+= terminatedHandler;
15:
16:
manualScheduler.RunWorkflow(instance.InstanceId);
17:
18:
if (
null != x)
19:
throw
new WorkflowException(x);
This helps me get exceptions shown in ASP.NET Exception handler
like this:
The WorkflowHelper is a reusable class that you can use
in your work project. Just copy the class file out of my project
and add in your own.