A Quick Overview of WF Rules
Windows Workflow Foundation (WF) includes a flexible Rule Engine, allowing the Workflow developer to logically separate your application's business rules from the Workflow Assemblies. Rules allow you to model and implement the business requirements of your application into a set of declarative statements, which can be enforced at runtime.
A WF Rule is composed out of the following three separate parts:
- A Condition which evaluates to a Boolean result.
- A Then Action which is executed if the condition evaluates to true.
- An optional Else Action which is executed if the condition evaluates to false.
The WF Visual Studio designers include a set of Rule Editor Dialogs. These editors allows the developer to create individual rules, and combine them together into a RuleSet. Alternatively, you can use any .NET language to create the a RuleSet through custom code.
Traditionally, WF Rules are used in the following two context:
- Within a Declarative Rule Condition. Such a rule condition can be part of an IfElseBranchActivity, a WhileActivity, or any other activity which needs to evaluate a simple or compound boolean expression.
- Within a Policy activity. A Policy activity evaluates a RuleSet, and applies the Then or Else actions of each Rule in the RuleSet, based upon the Rule's condition. The Rule Engine has the ability to apply forward chaining, which is a mechanism in which dependencies within rules are automatically detected, and Rules can be re-evaluated as a result of these dependencies.
In this article, we will go beyond the traditional approach outlined above, and look at some exciting things we can do with WF Rules. Specifically, we look at the following:
- First, we take a look at how can externalize a rule set, thereby decoupling it from the Workflow Assemblies in which they are applied.
- Next, we will take a look at how we can apply an external RuleSet to perform complex validation on WinForms and ASP.NET Web Forms.
- Finally, we will illustrate a mechanism through which you can achieve application extensibility by means of a WF RuleSet and targeted application extension points.
Externalizing WF Rules
Why externalize Rules?
Within a typical WF application, the Rules defined within the scope of a Workflow are serialized to a .rules file that is compiled as an embedded resource to your Workflow assembly. The .rules resource is deserialized and evaluated at runtime. While this approach might be completely feasible for some applications, it does require that the application is re-built and re-deployed whenever any of the rules change.
The External RuleSet Sample Project
At the WF site on .NetFx3.com, there is a sample available called "External RuleSet Demo", which illustrates how you can store WF RuleSets in an external database. Leveraging a database allows us to manage, edit and deploy the RuleSets without the need to rebuild and re-deploy our Workflow Assemblies.
This sample includes a setup script, which creates a database called "Rules". The schema for this database is very simple, as is illustrated below:

The Rules database has a single table called RuleSet. Each record in the RuleSet represents a particular version of a RuleSet (identified by Name, Major- and Minor version).
The RuleSet itself is serialized as XML into the RuleSet column. The Status column indicates whether or not the RuleSet has been validated successfully.
The Visual Studio solution of the sample contains the following projects:
- ExternalRuleSetLibrary. This project contains the definition of the RuleSetData and RuleSetInfo classes, which model a RuleSet representation in the Rules database.
- RuleSetService. This service implements a WorkflowRumetime service, which allows you to load a specified RuleSet from the Rules database.
- PolicyActivities. This project is a custom activity library, which implements a custom Policy activity called RunMembershipRules which leverages the RuleSetService to load the specified RuleSet from the Rules database, instead of loading it from an embedded .rules resource as is done by the standard PolicyActivity.
- RuleSetTool. This is a windows application, which allows you to:
- Load and save RuleSets from the Rules Database.
- Create a new RuleSet
- Import a RuleSet from a .rules file into the Rules database, or export a RuleSet from the Rules database to a .rules file.
- Edit a RuleSet, and associate it with an external type which is typically (but not necessarily) a Workflow. We will have more to say about this later on in this article. This editor is basically the standard Rules editor that you use when you edit a RuleSet associated with a standard Policy activity in Visual Studio.
Using an external RuleSet in your Workflow Project
To create a Workflow which leverages a RuleSet defined in the Rules database is very simple:
- First, you need to create the database by running the setup.cmd script in the root directory. Make sure to modify the invoked setup.sql script to target the appropriate SQL Server instance.
- Next, open the ExternalRuleSetToolkit solution and build the project.
Sample Project Description
Once you have this preparation work out of the way, you can start using the RuleSetService and custom Policy Activity in your own WF projects. In this article, we will create a simple workflow with the following requirements:
Our workflow is part of an e-commerce system, which allows it's customers to sign up as members. When a customer signs up, he/she has the choice between 3 levels of memberships:
- Standard. This level of membership allows a member to purchase products at retail cost. This membership is free.
- Gold. Gold members get 10% discount on all products. The membership fee for this level is $75/year.
- Platinum. Platinum members get 15% off on all products. Also, if a platinum member places an order with a final cost (after discount) of more than $200, then the company will waive their standard shipping cost of $5. The membership fee for this level is $100/year, except when the final order cost is greater than $300, in which case the membership cost for the first year is only $90.
Workflow Design
Our workflow is invoked when the member signs up, and (optionally) places his first order. The workflow will be invoked with the following parameters:
- The Membership Level (represented by a member of the MemberShipLevel enumeration).
- The total cost of the purchased products.
Upon completion, the workflow will make the following output parameters available:
- The membership cost
- The final cost of the member's initial order.
- The shipping cost.
Creating the Workflow Project
To implement this workflow, follow these steps:
- Create an empty workflow project called NewMemberSignup, and delete the default workflow (Workflow1.cs) from the project.
- Add a reference to the PolicyActivities.dll and the RuleSetServices.dll projects.
- Add a new Sequential workflow (with Code Separation) to the project, and call it NewMemberSignup.xoml, as is shown below:

(Personally, I always prefer XAML-based workflows with code separation above "pure code" workflows, since I can modify them with both Visual Studio AND my favorite XML editor ;-).
- Add the MembershipLevel enumeration to the project:
/// <summary>
/// These are our different Membership Levels
/// </summary>
public enum MembershipLevel
{
Standard,
Gold,
Platinum
}
- Switch to the code-behind of the Workflow, and add a private field and a public property for each workflow parameter. The code is shown below:

- Drag the PolicyFromServiceActivity in the Workflow, between the start and end markers. Rename the activity RunMembershipRules. This activity will be the only activity in the workflow.
- Build the project. It is important the build the project BEFORE you create the RuleSet, otherwise your Workflow Type will not have the properties you just added available for use in the RuleSet editor.
- Before we can set the RuleSet name in this activity, we should first go ahead and create out RuleSet. Start the RuleSetTool from the demo project, and create a new Rule by clicking the New button. Name the RuleSet NewMemberSignup, and keep the version # at "1.0".
- Now, we need to associate the RuleSet with our Workflow, so it can access our properties. Use the Browse button to navigate to the bin\debug directory and select the NewMemberSignup.dll assembly, and the NewMemberSignup.NewMemberSignup type, as shown below:

- At this time, it is best to save the created RuleSet by selection RuleStore | Save.
- Next, click Edit Rules to start creating the RuleSet contents. Click Add Rule to add the first rule. Name this rule CalcGoldMembershipCost. The details of the rule are shown below:

- Add a rule named CalcPlatinumMembershipCost to calculate the cost for a platinum member:

Following are the CalcDiscountGoldMembership and CalcDiscountPlatinumMembership rules:


we also need a rule to set the final order cost to the input order cost in case the member is a standard member (rule name: CalcNoDiscountStandardMember):

and finally, we need a rule to adjust the Membership fee for a Platinum member with a final order amount about $300 (rule name: AdjustPlatinumMembershipCost):

- Now that we have our RuleSet created, we need to reference it correctly in the Workflow editor. Switch back to the design view for the workflow, and Set the RuleSet name, major and minor version as shown below:

Creating a test project
Now that our workflow is complete, we need a little test program to test our Workflow as follows:
- Add Sequential Workflow Console Application to your solution and name it TestNewMemberSignup. You can delete the Workflow1 workflow created for the project, since we will be using our Workflow in the NewMemberSignup project. Make sure to add the following references to your console application:
- A project reference to this project in your new console application.
- A reference to the RuleSetService
- We will be retrieving the RuleSet from the Rules database. Therefore, we will need an application configuration file with the appropriate connection string, as shown below:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="RuleSetStoreConnectionString"
connectionString="Initial Catalog=Rules;Data Source=localhost;Integrated Security=SSPI;"
providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>
- As we start implementing the code for our test project, let's summarize what this code should do:
- The program will received test values for the Membership Level and the Order Cost as command-line arguments
- We should create an instance of the workflow runtime.
- The RuleSetService is implemented as a standard workflow runtime service (it inherits from System.Workflow.Runtime.Hosting. WorkflowRuntimeService), therefore we can add it directly to the Workflow runtime.
- We should pass the MembershipLevel and the OrderCost as arguments to the workflow, and start the workflow.
- When the workflow terminates, we should retrieve the output arguments.
- The full code is shown below (note: You can download all of the source code in the downloads section of our site):
1 using System;
2 using System.Collections.Generic;
3 using System.Threading;
4 using System.Workflow.Runtime;
5 using System.Workflow.Runtime.Hosting;
6
7 using RuleSetServices;
8 using NewMemberSignup;
9
10 namespace TestNewMemberSignup
11 {
12 class Program
13 {
14 private static AutoResetEvent waitHandle;
15
16 static void Main(string[] args)
17 {
18 if (args.Length != 2)
19 {
20 Usage();
21 return;
22 }
23
24 // Create the workflow runtime
25 using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
26 {
27 // Create our wait event, hookup our handlers
28 waitHandle = new AutoResetEvent(false);
29 workflowRuntime.WorkflowCompleted += workflowRuntime_WorkflowCompleted;
30 workflowRuntime.WorkflowTerminated += workflowRuntime_WorkflowTerminated;
31
32 // Add the RuleSetService
33 workflowRuntime.AddService(new RuleSetService());
34
35 // Create our arguments
36 Dictionary<string, object> wfArguments =
37 new Dictionary<string, object>();
38
39 MembershipLevel membershipLevel =
40 (MembershipLevel)Enum.Parse(typeof(MembershipLevel), args[0]);
41 Console.WriteLine("Membership Level: {0}", membershipLevel);
42 wfArguments.Add("MembershipLevel", membershipLevel);
43
44 decimal orderCost = decimal.Parse(args[1]);
45 Console.WriteLine("Order Cost: {0}", orderCost);
46 wfArguments.Add("OrderCost", orderCost);
47
48 // Start the Workflow
49 WorkflowInstance instance = workflowRuntime.CreateWorkflow(
50 typeof(NewMemberSignup.NewMemberSignup), wfArguments);
51 instance.Start();
52
53 // Wait for the Workflow to complete
54 waitHandle.WaitOne();
55 }
56 }
57
58 static void workflowRuntime_WorkflowCompleted(
59 object sender,
60 WorkflowCompletedEventArgs e)
61 {
62 Console.WriteLine("Membership Cost: {0}",
63 e.OutputParameters["MembershipCost"]);
64 Console.WriteLine("Final order price: {0}",
65 e.OutputParameters["FinalOrderCost"]);
66 Console.WriteLine("Shipping Cost: {0}",
67 e.OutputParameters["ShippingCost"]);
68
69 waitHandle.Set();
70 }
71
72 static void workflowRuntime_WorkflowTerminated(
73 object sender,
74 WorkflowTerminatedEventArgs e)
75 {
76 Console.WriteLine("Workflow terminated, reason: {0}", e.Exception.Message);
77 waitHandle.Set();
78 }
79
80 private static void Usage()
81 {
82 Console.WriteLine("Usage <MembershipLevel> <OrderCost>");
83 }
84 }
85 }
86
The most important aspects of the above code are discussed below:
- At lines 28 through 30, we hookup the WorkflowCompleted and WorkflowTerminated events. It is a good habit to ALWAYS process the WorkflowTerminated event, this way you will immediately know if something went wrong with your workflow. We need to process WorkflowCompleted, since this is the only way that we can access the OUTPUT arguments of the workflow. The OutputParameters property of the WorkflowCompletedEventArgs class contains a Dictionary with the output parameters. We display those in lines 62-67.
- We add the RuleSetService to the WorkflowRuntime in line #33.
- To pass input arguments to a workflow, we need to create a Dictionary<String, Object> instance, and add our values. Note that our keys should be identical to the public property names used in our Workflow (lines 36-42). The CreateWorkflow method of the WorkflowRuntime takes the Workflow arguments as the second parameter (lines 49-50).
Running the Test Project
Below is a table with the results of a number of test runs:

From the above table, it is clear that our rules were evaluated as expected. For tests 2 and 3, the discounts and membership fees are calculated as expected. For test number four, we see that because the discounted order cost is above 200, the member gets free shipping.
The most interesting result is the last result. Because the OrderCost is greater than $300, the membership fee is reduced from $100 to $90. To avoid a result where Rule # 3 (calculation of Platinum membership cost) would override Rule # 5 (discount for platinum members with a total order > $300), we need to make sure that we know how rules are evaluated.
Within the same priority, rules are evaluated in alphabetical order. Since Rule #5 starts with an "a", it will be evaluated first, so Rule #3 will be evaluated later. This would undo the result of Rule #5. Therefore, we will give Rule#3 a HIGHER priority then Rule # 5,so that it will be evaluated before Rule #. As you can see, this provides us with the expected result.
What is so powerful of external rules is that I could now go in and change the above rules with the RuleSet editor, and re-run the application without any recompilation, and I would immediately get result that would reflect these adjusted rules!
Performing Forms Validation with WF Rules
Because of the length of this post, we will defer this topic to a later post.
WF Rules and Application Extensibility
Same here.. Stay tuned!