Windows Workflow Foundation (WF), included in .NET 3.0, provides the programming model, engine, and tools for quickly building workflow-enabled applications. It gives developers the ability to model business processes in a visual designer by drawing flow chart diagrams. Complex business operations can be modeled as a workflow in the visual workflow designer included in Visual Studio 2008, and coded using any .NET programming language. WF consists of the following parts:
Activity model
Activities are the building blocks of workflow—think of them as a unit of work that needs to be executed. Activities are easy to create, either from writing code or by composing them from other activities. Out of the box, there are a set of activities that mostly provide structure, such as parallel execution, if/else, and calling a web service.
Workflow designer
This is the design surface in Visual Studio, which allows for the graphical composition of workflow, by placing activities within the workflow model.
Workflow runtime
Workflow runtime is a lightweight and extensible engine that executes the activities that make up a workflow. The runtime is hosted within any .NET process, enabling developers to bring workflow to anything, from a Windows forms application to an ASP.NET web site or a Windows service.
Rules engine
WF has a rules engine that enables declarative, rule-based development for workflows and any .NET application to use. Using the rule engine, you can eliminate hardcoded rules in your code and move them from the code to a more maintainable declarative format on the workflow diagram.
Although a workflow is mostly used in applications that have workflow-type business processes, you can use a workflow in almost any application as long as the application does complex operations. In this Start page application, some operations, like first visit, are complex and require multistep activities and decisions. So, such applications can benefit from workflow implementation.
The entire business layer is developed using WF. Each of the methods in the DashboardFacade do nothing but call individual workflows. There’s absolutely no business code that is not part of any workflow.
“This is insane!” you are thinking. I know. Please listen to why I went for this approach. Architects can “design” business layer operations in terms of activities, and developers can just fill in a small amount of unit code to implement each activity.
This is actually a really good reason because architects can save time by not having to produce Word documents on how things should work. They can directly go into Workflow designer, design the activities, connect them, design the data flow, and verify whether all input and output are properly mapped or not. This is lot better than drawing flow charts, writing pseudocode, and explaining in stylish text how an operation should work. It’s 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 a small amount of very specific reusable code inside each one. They know what the activity’s input (like function parameters) will be and they know what to produce (return value of function). This makes the activities reusable, and architects can reuse an activity in many workflows.
Workflows can be debugged right in Visual Studio Designer for WF. So, developers can easily find defects in their implementation by debugging the workflow. Architects can enforce many standards like validations, input output check, and fault handling on the workflow. Developers cannot but comply and, therefore, 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 wanted to study how a particular operation works, they could just print out the workflow and read it through.
Performance Concerns with WF
But what about performance? You will read from some blog posts that WF is a pretty big library and can be a memory hog. Also, the workflow runtime is quite big and takes time to start up. So, I did some profiling on the overhead of workflow execution, and it is actually very fast for synchronous execution. Here’s proof from Visual Studio’s output window log:
334ec662-0e45-4f1c-bf2c-cd3a27014691 Activity: Get User Guid
0.078125
b030692b-5181-41f9-a0c3-69ce309d9806 Activity: Get User Pages
0.0625
b030692b-5181-41f9-a0c3-69ce309d9806 Activity: Get User Setting
0.046875
The first four entries are the time taken by individual activities during data access only, not the total time it takes to execute the whole activity. The time entries here are in seconds, and the first four entries represent the duration of database operations inside the activities. The last one is the total time for running a workflow with the four activities shown and some extra code. If you sum up all of the individual activity execution time for only database operations, it is 0.2500, which is just 0.015625 seconds less than the total execution time. This means that executing the workflow itself along with the overhead of running activities takes about 0.015 seconds, which is almost nothing (around 6 percent) compared to the total effort of doing the complete operation.
Each user action can be mapped to a workflow that responds to that action. For example, when a user wants to add a new widget, a workflow can take care of creating the widget, positioning it properly on the page, and configuring the widget with the default value. The first visit of a brand new user to the site is a complex operation, so it is a good candidate to become a workflow. This makes the architecture quite simple on the web layer—just call a workflow on various scenarios and render the UI accordingly, as illustrated in Figure 4-2.
Figure 4-2.User actions are mapped to a workflow. For example, when a user adds a new tab, the request goes to a workflow. The workflow creates a new tab, makes it current, configures tab default settings, adds default widgets, etc. Once done, the workflow returns success and the page shows the new tab.
Instead of using complex diagrams and lines of documentation to explain how to handle a particular user or system action, you can draw a workflow and write code inside it. This serves both as a document and a functional component that does the job. The next sections show scenarios that can easily be done in a workflow.
Dealing with First Visit by a New User (NewUserSetupWorkflow)
Handling the first visit of a brand new user is the most complex operation your web site will handle. It’s a good candidate for becoming a workflow. Figure 4-3 shows a workflow that does all the business layer work for the first-time visit and returns a complete page setup. The Default.aspx just creates the widgets as it receives them from the workflow and is not required to perform any other logic.
The operations involved in creating the first-visit experience for a new user are as follows:
Create a new anonymous user
Create two default pages
Put some default widgets on the first page
Construct a object model that contains user data, the user’s page collection, and the widgets for the first page
If you put these operations in a workflow, you get the workflow shown in Figure 4-3 .
Figure 4-3. New user visit workflow creates a new user account and configures the account with the default setup
The workflow takes the ASP.NET anonymous identification provider generated byUserNameas an input to the workflow from the Default.aspx page.
The first step in passing this input parameter to the workflow while running the workflow is to call theGetUserGuidActivityto get theUserIdfrom theaspnet_userstable for that user (see Example 4-17).
Example 4-17. GetUserGuidActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { using( new TimedLog(UserName, "Activity: Get User Guid") ) { var db = DatabaseHelper.GetDashboardData();
this.UserGuid = (from u in db.AspnetUsers where u.LoweredUserName == UserName && u.ApplicationId == DatabaseHelper. ApplicationGuid select u.UserId).Single();
return ActivityExecutionStatus.Closed; } }
This activity is used in many places because it is a common requirement to get theUserIdfrom the username found from the ASP.NETContextobject. All the tables have a foreign key in theUserId column but ASP.NET gives only theUserName. So, in almost all the operations,UserNameis passed from the web layer and the business layer converts it toUserId and does its work.
Theusing(TimedLog)block records the execution time of the code inside theusingblock. It prints the execution time in the debug window as you read earlier in the “Performance Concerns with WF ” section.
The next step is to create the first page for the user usingCreateNewPageActivityshown in Example 4-18.
Example 4-18. CreateNewPageActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { DashboardData db = DatabaseHelper.GetDashboardData();
var newPage = new Page(); newPage.UserId = UserId; newPage.Title = Title; newPage.CreatedDate = DateTime.Now; newPage.LastUpdate = DateTime.Now;
This activity takes theUserIDas input and produces theNewPageId property as output. It creates a new page, and default widgets are added on that page.CreateDefaultWidgetActivitycreates the default widgets on this page as shown in Example 4-19.
Example 4-19. CreateDefaultWidgetActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { var db = DatabaseHelper.GetDashboardData();
var defaultWidgets = db.Widgets.Where( w => w.IsDefault == true ).ToList(); var widgetsPerColumn = (int)Math.Ceiling((float)defaultWidgets.Count/3.0);
var row = 0; var col = 0;
foreach( Widget w in defaultWidgets ) { var newWidget = new WidgetInstance(); newWidget.PageId= this.PageId; newWidget.ColumnNo = col; newWidget.OrderNo = row; newWidget.CreatedDate = newWidget.LastUpdate = DateTime.Now; newWidget.Expanded = true; newWidget.Title = w.Name; newWidget.VersionNo = 1; newWidget.WidgetId = w.ID; newWidget.State = w.DefaultState;
Compute the number of widgets to put in each column so they have an even distribution of widgets based on the number of default widgets in the database.
Run theforeachloop through each default widget and created widget instances.
Create the second empty page.
Call another workflow namedUserVisitWorkflow to load the page setup for the user. This workflow was used on both the first visit and subsequent visits because loading a user’s page setup is same for both cases.
TheInvokeWorkflowactivity that comes with WF executes a workflow asynchronously. So, if you are calling a workflow from ASP.NET that in turn calls another workflow, the second workflow is going to be terminated prematurely instead of executing completely. This is because the workflow runtime will execute the first workflow synchronously and then finish the workflow execution and return. If you useInvokeWorkflowactivity to run another workflow from the first workflow, it will start on another thread, and it will not get enough time to execute completely before the parent workflow ends, as shown in Figure 4-4.
Figure 4-4. InvokeWorkflow executes a workflow asynchronously, so if the calling workflow completes before the called workflow, it will terminate prematurely
So,InvokeWorkflowcould not be used to execute theUserVisitWorkflowfromNewUserSetupWorkflow. Instead it is executed using theCallWorkflow activity, which takes a workflow and executes it synchronously. It’s a handy activity I found on Jon Flanders’ blog (http://www.masteringbiztalk.com/blogs/jon/ PermaLink,guid,7be9fb53-0ddf-4633-b358-01c3e9999088.aspx).
The beauty of this activity is that it properly maps both inbound and outbound properties of the workflow that it calls, as shown in Figure 4-5.
TheUserNameproperty is passed from theNewUserVisitWorkflow, and it is returning theUserPageSetup, which contains everything needed to render the page for the user.
UserVisitWorkflow creates a composite object named UserPageSetup that holds the user’s settings, pages, and widgets on the current page. The Default.aspx gets everything it needs to render the whole page fromUserPageSetup, as shown in Figure 4-6.
Figure 4-5. You can map CallWorkflow to a workflow and it will call that workflow synchronously. You can also see the parameters of the workflow and map them with properties in the current workflow.
Figure 4-6. UserVisitWorkflow design view
Just like the previous workflow,UserVisitWorkflowtakesUserNameand converts it toUserGuid. It then calls theGetUserPagesActivity, which loads the pages of the user (see Example 4-20).
Example 4-20. GetUserPagesActivity’s Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { using( new TimedLog(UserGuid.ToString(), "Activity: Get User Pages") ) { var db = DatabaseHelper.GetDashboardData();
this.Pages = (from page in db.Pages where page.UserId == UserGuid select page).ToList();
return ActivityExecutionStatus.Closed; } }
After that, it calls theGetUserSettingActivity, which gets or creates the user’s setting. TheUserSettingobject contains the user’s current page, which is used byGetUserSettingActivityto load the widgets of the current page.
The code inGetUserSettingActivityis not straightforward (see Example 4-21). It first checks ifUserSettinghas been created for the user and, if not,GetUserSettingActivitycreates it.
Example 4-21. GetUserSettingActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { using( new TimedLog(UserGuid.ToString(), "Activity: Get User Setting") ) { DashboardData db = DatabaseHelper.GetDashboardData();
var query = from u in db.UserSettings where u.UserId == UserGuid select u;
Loading the existing user’s settings is optimized by getting only theCurrentPageId instead of the wholeUserSettingobject. This results in a very small query that does a scalar selection, which is a bit faster than a row selection because it doesn’t involve constructing a row object or sending unnecessary fields to a row.
The final activity loads the widgets on the current page (see Example 4-22). It takes thePageId and loads widget instances on the page, including the widget definition for each instance.
Example 4-22. GetWidgetsInPageActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { using( new TimedLog(UserGuid.ToString(), "Activity: Get Widgets in page: " + PageId) ) { var db = DatabaseHelper.GetDashboardData();
// Load widget instances along with the Widget definition // for the specified page this.WidgetInstances = (from widgetInstance in db.WidgetInstances where widgetInstance.PageId == this.PageId orderby widgetInstance.ColumnNo, widgetInstance.OrderNo select widgetInstance) .Including(widgetInstance => widgetInstance.Widget) .ToList();
return ActivityExecutionStatus.Closed; } }
The LINQ query that loads the widget instances has two important actions:
Loads widget instances on the page and orders them by column, and then row. As a result, you get widget instances from left to right and in proper order within each column.
Fetches the widget object by producing anINNER JOINbetweenWidgetand theWidgetInstancetable.
The collection of the widget instance is mapped to theWidgetInstanceproperty of the activity. The final code block—ReturnUserPageSetup—populates theUserPageSetupproperty of the workflow with loaded data (see Example 4-23).
Example 4-23. PopulateUserPageSetup property with widgets, pages, and user settings needed to render the page
The workflow takes an emptyUserPageSetup object; when it completes, it populates the empty object with the loaded data. So, from ASP.NET, theUserPageSetupobject is passed and emptied. Once the workflow completes, the instance is fully populated.
Please check back next week for the conclusion to this series.