OdeToCode IC Logo

Windows Workflow: Rules and Conditions

Friday, September 1, 2006
Programming Windows Workflow Foundation by Scott Allen Programming Windows Workflow Foundation If you enjoyed this article, you'll enjoy the book even more! Order now from Packt Publishing and save 10%!

Software applies knowledge to data. This is true for all software from business applications to video games. The knowledge inside software is generally a combination of procedural knowledge and declarative knowledge. Procedural knowledge is information about how to perform a task, like how to make a car and hotel reservation using an electronic travel broker. Procedural knowledge is easy to express using a general-purpose programming language like C#, Visual Basic, or any of their predecessors.

Declarative knowledge, on the other hand, is about the relationships in data. For example, a piece of declarative knowledge would be that hotel reservations made at least 14 days in advance receive a 10% discount, unless the cost of the room is less than $100. The date and the price share a relationship and can affect each other. Expressing this type of knowledge using a general-purpose programming language isn't difficult on a small scale, but breaks down as the amount of knowledge grows. We must transform the knowledge into procedural code using if-then-else statements. Many software applications require an enormous amount of declarative knowledge: tax preparation systems, mortgage-banking software, and hotel reservation systems, to name just a few. We often refer to declarative knowledge as our "business rules".

Encoding business rules into procedural code makes the rules harder to find, read, and modify. Over the years, the software industry has invented tools for working with declarative knowledge. We categorize these tools as rules engines, inference engines, and logic machines. A rules engine specializes in making declarative knowledge easier to implement, process, isolate, and modify.

Windows Workflow offers the best of both worlds. We can use Sequence activities to implement procedural knowledge, and Policy activities to execute declarative knowledge. In this article, we will focus on the activities that use rules and conditions to express declarative knowledge. The topics will include the Policy activity, the ConditionedActivityGroup, and others.

What Are Rules and Conditions?

Three important terms we will use in this chapter are conditions, rules, and rule sets. In WF, conditions are chunks of logic that return true or false. A number of WF activities utilize conditions to guide their behavior. These activities include the While activity, the IfElseBranch activity, and the ConditionedActivityGroup. The While activity, for instance, loops until it's Condition property returns false. We can implement conditions in code, or in XML.

Rules are conditions with a set of actions to perform. Rules use a declarative if-then-else style, where the "if" is a condition to evaluate. If the condition evaluates to true, the runtime performs the "then" actions, otherwise the "else" actions. While this sounds like procedural code, there are substantial differences. The if-then-else constructs in most languages actively changes the flow of control in an application. Rules, on the other hand, passively wait for an execution engine to evaluate their logic and invoke their actions.

A rule set is a collection of one or more rules. As another example from the hotel business, we might have three rules we use to calculate the discount on the price of a room (shown here in pseudo-code).

if person's age is greater than 55
   then discount = discount + 10%

if length of stay is greater than 5 days
   then discount = discount + 10%

if discount is greater than 12%
   then discount = 12%

Before we can evaluate these three rules, we need to group them inside a rule set. We can assign each rule a priority to control the order of evaluation. WF can revisit rules if later rules change the data used inside previous rules. We can store rules in an external XML file, and feed external rules to the workflow runtime when creating a new workflow. WF provides an API for us to programmatically update, create, and modify rule sets and rules at runtime. The features and execution semantics described above give us more flexibility when compared to procedural code. We can dynamically customize rules to meet the needs of a specific customer or business scenario.

We will return to rules and rule sets later in the article. For now we will drill into conditions in Windows Workflow.

Working with Conditions

The While activity

The While activity is one activity that uses a condition. The activity will repeatedly execute its child activity until its Condition property returns false. The Properties window for the While activity allows us to set this Condition property to a Code Condition or a Declarative Rule Condition. In the figure to the right (click to view), we've told the While activity to use a code condition, and that the code condition is implemented in a method named CheckBugIndex.

Code Conditions

A code condition is an event handler in our workflow's code-beside file. A code condition returns a boolean value via a ConditionalEventArgs parameter. Because a code condition is just another method on our workflow class, the conditional logic compiles into the same assembly that hosts our workflow definition.

The implementation of CheckBugIndex is shown below. We have an array of Bug objects for the workflow to process. The array might arrive as a parameter to the workflow, or through some other communication mechanism like the HandleExternalEvent activity. The workflow uses the bugIndex field to track its progress through the array. Somewhere, another activity will increment bugIndex as the workflow finishes processing each bug. If the array of bugs is not initialized, or if the bugIndex doesn't point to a valid entry in the array, we want to halt the While activity by having our code condition return a value of false.

private Bug[] _bugs;
private int bugIndex = 0;

protected void CheckBugIndex(object sender, ConditionalEventArgs e)
{
  
if (_bugs == null || bugIndex >= _bugs.Length)
  {
    e.Result =
false;
  }
  
else
  {
    e.Result =
true;
  }
}

Code conditions, like our method above, are represented by CodeCondition objects at runtime. The CodeCondition class derives from an abstract ActivityCondition class.

ActivityCondition classes

Because the Condition property of the While activity accepts an ActivityCondition object, we have the choice of assigning either a CodeCondition or a RuleConditionReference. Regardless of which we choose, all the runtime needs to do is call the Evaluate method to fetch a boolean result. A CodeCondition will ultimately fire its Condition event to retrieve this boolean value. It is this Condition event that we are wiring up to the method in our code-behind file. We can see this a little more clearly by looking at the XAML markup produced by the designer.

<SequentialWorkflowActivity
    
x:Class="chapter9_rules.conditions.BugLooping"
    
x:Name="BugLooping"
    
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
   <
WhileActivity x:Name="whileActivity1">
      <
WhileActivity.Condition>
         <
CodeCondition Condition="CheckBugIndex" />
      </
WhileActivity.Condition>
      <
CodeActivity x:Name="PrintBug" ExecuteCode="codeActivity1_ExecuteCode" />
   </
WhileActivity>
</
SequentialWorkflowActivity>

Rule Conditions

Declarative rule conditions work differently from code conditions. If we expressed our CheckBugIndex condition as a declarative rule, we would just need to type the following string into the designer:

  bugs == null || bugIndex >= bugs.Length

Windows Workflow will parse and evaluate this rule at runtime. We don't need to create a new method in our workflow class. The definition for this expression will ultimately live inside a .rules file as part of our workflow project. A RuleConditionReference object will reference the expression by name (every rule in WF has a name).

As an example, suppose we are creating a new workflow with a While activity, and we want the activity to loop until a _retryCount field exceeds some value. After we drop the While activity in the designer, we can open the Properties windows and click the drop drown list beside the Condition property. This time, we will ask for a Declarative Rule Condition. The designer will make two additional entries available - ConditionName and Expression. Clicking in the text box beside ConditionName will display the ellipses pointed to in the figure below.

Conditions in the property designer

Clicking the ellipses button launches the Select Condition dialog, shown below (click to expand). This dialog will list all of the declarative rule conditions in our workflow, and will initially be empty. Along the top of the dialog are buttons to create, edit, rename, and delete rules. The Valid column on the right-hand side will let us know about syntax errors and other validation problems in our rules.

Select Condition dialog

At this point we want to create a new rule. Clicking the New… button will launch the Rule Condition Editor shown below (click to expand).

Rule condition editor

Inside this editor is where we can type our expression. The expression we've entered will return true as long as the _retryCount field is less than 4. If we type the C# this keyword (or the Me keyword in Visual Basic), an Intellisense window will appear and display a list of fields, properties, and methods in our workflow class.

Clicking the OK button in the editor will return us to the Select Condition dialog, where we can click the Rename button to give our condition a friendly name (the default name would be Condition1, which isn't descriptive). We will give our rule the name of RetryCountCondition.

The .rules File

After all these button clicks, a new file will appear nested underneath our workflow definition in the Solution Explorer window. The file will have the same name as our workflow class name but with an extension of .rules. Inside is a verbose XML representation of the condition we wrote.

<RuleDefinitions xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
  <
RuleDefinitions.Conditions>
    <
RuleExpressionCondition Name="Condition1">
      <
RuleExpressionCondition.Expression>
        <
ns0:CodeBinaryOperatorExpression Operator="GreaterThan"
              
xmlns:ns0="clr-namespace:System.CodeDom;Assembly=System,
                          Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
">
          <
ns0:CodeBinaryOperatorExpression.Left>
            <
ns0:CodePropertyReferenceExpression PropertyName="Count">
              <
ns0:CodePropertyReferenceExpression.TargetObject>
                <
ns0:CodePropertyReferenceExpression PropertyName="Activities">
                  <
ns0:CodePropertyReferenceExpression.TargetObject>
                    <
ns0:CodeThisReferenceExpression />
                  </
ns0:CodePropertyReferenceExpression.TargetObject>
                </
ns0:CodePropertyReferenceExpression>
              </
ns0:CodePropertyReferenceExpression.TargetObject>
            </
ns0:CodePropertyReferenceExpression>
          </
ns0:CodeBinaryOperatorExpression.Left>
          <
ns0:CodeBinaryOperatorExpression.Right>
            <
ns0:CodePrimitiveExpression>
              <
ns0:CodePrimitiveExpression.Value>
                <
ns1:Int32 xmlns:ns1="clr-namespace:System;Assembly=mscorlib, 
                           Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
">
                          
5000
                </ns1:Int32>
              </
ns0:CodePrimitiveExpression.Value>
            </
ns0:CodePrimitiveExpression>
          </
ns0:CodeBinaryOperatorExpression.Right>
        </
ns0:CodeBinaryOperatorExpression>
      </
RuleExpressionCondition.Expression>
    </
RuleExpressionCondition>
  </
RuleDefinitions.Conditions>
</
RuleDefinitions>

If you remember our XAML discussion from the article "Authoring Workflows", you'll realize this is a XAML representation of objects from the System.CodeDom namespace. The CodeDom (Code Document Object Model) namespace contains classes that construct source code in a language agnostic fashion. For instance, the CodeBinaryOperatorExpression class represents a binary operator between two expressions. The instance in our XAML is a "LessThan" operator, but could be an addition, subtraction, greater than, or bitwise operation.

At compile time, this .rules file becomes an embedded resource in our assembly. WF will read the resource at runtime and use classes in the System.CodeDom.Compiler namespace to generate and compile source code from the XAML. Once the runtime compiles the expression, WF can evaluate the rule to inspect its result.

Available Expressions

Most of the expressions we write in C# or VB.NET will be valid rules. For instance, all of the following expressions are valid. We can invoke methods, retrieve properties, index into arrays, and even use other classes from the base class library, like the RegEx class for regular expressions.

  this.x + 1 < 100

  this.name.StartsWith("Scott")

  Regex.Match(this.AreaCode, @"^\\(\\d{3}\\)\\s\\d{3}-\\d{4}$").Success

  this.CheckIndex()

  this.GetResult() != 10

  this.numbers[this.x] == this.numbers[this.x + 1]

Expressions must evaluate to true or false. The following examples are invalid.

  Console.Write(this.name)

  this.x = this.GetResult()

Rules and Activation

In the Authoring Workflows article, we also discussed workflow activation. Activation allows us to pass a XAML definition of our workflow to the workflow runtime, instead of using a compiled workflow definition. For instance, let's assume we have the following workflow definition in a file named Activation.xoml.

<SequentialWorkflowActivity      
    
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
   <
WhileActivity>
      <
WhileActivity.Condition>
         <
RuleConditionReference ConditionName="Condition1" />
      </
WhileActivity.Condition>
    <
DelayActivity />
   </
WhileActivity>
</
SequentialWorkflowActivity>

Let's also assume our condition (Condition1) is in a file named Activation.rules. We can load and execute the workflow with the following code:

using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
  
AutoResetEvent waitHandle = new AutoResetEvent(false);
  workflowRuntime.WorkflowCompleted +=
delegate
      {
          waitHandle.Set();
      };
  workflowRuntime.WorkflowTerminated +=
delegate
      {
          
Console.WriteLine("exception!"); waitHandle.Set();
      };

  
XmlReader definitionReader;
  definitionReader =
XmlReader.Create(@"..\..\conditions\Activation.xoml");

  
XmlReader rulesReader;
  rulesReader =
XmlReader.Create(@"..\..\conditions\Activation.rules");

  
Dictionary<string, object> parameters = null;
  
WorkflowInstance instance;
  instance = workflowRuntime.CreateWorkflow(
                              definitionReader, rulesReader, parameters
                             );

  instance.Start();
  waitHandle.WaitOne();
}

Activation gives us a great deal of flexibility. For instance, we could store workflow and rule definitions inside of database records, and update the rules without recompiling or redeploying an application.

The Conditioned Activity Group

Before we finish talking about conditions, we need to take a closer look at one condition-centric activity that is flexible and powerful. The ConditionedActivityGroup (CAG) executes a collection of child activities based on a When condition attached to each child. Furthermore, the CAG continues to execute until an Until condition on the CAG returns true. This behaviour makes the CAG somewhat of a cross between a While activity and a Parallel activity.

When we drop the CAG into the workflow designer, it will appear as shown below. In the top of the activity shape is an activity "storyboard" where we can drop activities. The arrows on either side of the storyboard allow us to scroll through the child activities in the storyboard. When we select an activity in the storyboard, the selected activity will appear in the bottom of the activity shape inside the preview box. We can toggle between preview and edit modes using the button in the middle of the CAG's shape.

The Conditioned Activity Group

In the figure below, we've arranged some activities in the CAG's storyboard. The first activity is a Sequence activity, and we've selected the activity for editing. The bottom of the CAG's shape displays the Sequence activity in detail. Inside the Sequence activity, are two Code activities.

CAG details

Editing conditions in the CAG Since the Sequence activity is a direct descendant of the CAG, we can assign the Sequence activity a When condition (click the figure to the right). As with all conditions, the When condition can be a code condition, or a declarative rule.

The CAG only executes a child activity if the child's When condition returns true, however, the When condition is optional. If we do not specify a When condition, the child activity will execute only once. No matter how many times the CAG continues to loop, an activity without a When condition will execute only during the first iteration.

The CAG repeatedly executes child activities until one of two things happen. The CAG itself has an Until condition (see the figure below). When the Until condition returns true, the CAG immediately stops processing and also cancels any currently executing child activities. The CAG will also stop processing if there are no child activities to execute. This can occur when the When condition of all child activities return false.

The Until condition of the CAG

It's important to note that the CAG evaluates the Until condition when it first begins executing. If the Until condition returns true at this point, no child activities will execute. Also, the CAG evaluates the Until condition each time a child activity finishes execution. This means only a subset of the child activities may execute. Finally, the CAG doesn't guarantee the execution order of child activities, which is why the CAG is similar to the Parallel activity. For example, dropping a Delay activity inside the CAG will not block the CAG from executing its other child activities.

CAG Scenarios

The CAG is useful in goal-seeking scenarios. Let's say we are building a workflow to book flight, hotel, and car reservations for a trip. Inside the workflow, we might use web service activities to request pricing information from third party systems. We can arrange the web service calls inside a CAG to request prices repeatedly until we meet a goal. Our goal might be for the total price of the trip to meet a minimum cost, or we might use a more advanced goal that includes cost, total travel time, and hotel class.

Working with Rules

The declarative rule conditions we've seen so far only return a value of true or false. A condition doesn't modify a workflow. A rule, on the other hand, is both a condition and a set of actions in an if-then-else form. The Rule class in Windows Workflow represents this if-then-else concept. The class diagram below displays classes with important relationships to the Rule class.

Rule class relationships

The first concept to notice is that the RuleSet class manages a collection of rules. The Policy activity will use the Execute method of a RuleSet to process the rule collection. We will cover the Policy activity in more detail soon.

Every Rule inside a RuleSet has a Condition property that references a single RuleCondition object. The RuleSet logic will use the Evaluate method of a RuleCondition to retrieve a value of true or false.

Every Rule maintains two collections of RuleAction objects - the ElseActions and the ThenActions. When a rule's condition evaluates to true, the runtime invokes the Execute method of each action in the ThenActions collection, otherwise the runtime invokes the Execute method of the actions in the ElseActions collection.

With a basic understanding of how rules work on the inside, let's take a look at the Policy activity.

The Policy Activity

Encarta describes policy as "a program of actions adopted by an individual, group, or government". Policies are everywhere in real life. Universities define policies for student admissions, and banks define policies for lending money. U.S. banks often base their lending policies on credit scores, and a credit score takes into account many variables, like an individual's age, record of past payment, income, and outstanding debt. Business policy can become very complex, and is full of declarative knowledge. As we discussed at the beginning of the chapter, declarative knowledge is about the relationships in data. For example, one bank's policy might say that if my credit score is less than 500 points, they will charge me an extra one percent in interest.

Creating a Policy Workflow

Although we can use a Policy activity almost anywhere inside of a larger workflow, we will be using a simple workflow with only a single Policy activity inside. All we need is to create a new sequential workflow, and drag a Policy shape into the designer (click the figure below to expand).

The Policy activityClick to view

In the Properties window in the above figure we can see the RuleSetReference property. The RuleSetReference property is the primary property of a Policy activity. We can click the ellipses button in the properties window to launch the Select Rule Set dialog, shown next.

Rule Set Editor

When we first start a workflow, we won't have any rule sets defined. A workflow can contain multiple rule sets, and each rule set will contain one or more rules. Although a Policy activity can only reference a single rule set, we might design a workflow with multiple Policy activities inside, and need them each to reference a different rule set.

Clicking on the New button in the dialog will launch the Rule Set Editor dialog shown below (click to expand).

Rule Editor

The Rule Set Editor exposes many options for rules and the rule set. For now, we are going to concentrate on conditions and actions. Let's suppose we are defining a policy to "score" a software bug. The score will determine if we need to send notifications to team members, who will jump into immediate action. The bug will be a member field in our workflow class, and will expose various properties (Priority, IsOpenedByClient) that we will inspect to compute a score.

Our first three rules will determine a bug's base score by looking at the bug's Priority property. We can start by clicking the "Add Rule" button in the dialog. Our first rule is:

  IF this.Bug.Priority == BugPriority.Low 
  THEN this.Score = 0.
  

In the rules dialog, we can give this rule a meaningful name of SetScore_LowPriority.

The IF conditions in our rules are just like the conditions we examined earlier in the chapter. We can use tests for equality, inequality, greater than or less than. We can call methods, and index into arrays. As long as the IF condition's expression returns a true or false, and can be represented by types in the System.CodeDom namespace, we will have a valid expression.

The actions in our rules have even greater flexibility. An action is not restricted to returning a Boolean value. In fact, most actions will perform assignments and manipulate fields and properties in a workflow. In our first rule, we've used a rule action to assign an initial score of 0. Remember the action property on a rule is a collection, meaning we can specify multiple actions for both the then actions and else actions. We will need to place each action on a separate line inside the action text box.

If we have three possible bug priorities (Low, Medium, and High), we'll need a rule to set the bug score for each priority level. Once we've entered the three rules, the Rule Set Editor should look like the figure below (click to expand).

Rule Editor

Evaluation

With three rules in place, we could execute our workflow and watch the Policy activity compute our bug score. These rules, like the declarative conditions we used earlier, will live in a .rules file. When a rule set executes, it will process each rule by evaluating the rule's condition and executing the then or else actions. The rule set continues processing in this fashion until it has processed every rule in the rule set, or until it encounters a Halt instruction. Halt is a keyword we can place in a rule's action list that will stop the rule set from processing additional rules.

There is still an additional rule we would like to add to our rule set, however. The rule should say, "If the bug's score is greater than 75, then send an email to the development team". This rule presents a potential problem, however, because it would not work if the rule set evaluates this new rule first. We need to set the score for a bug first. We can achieve this goal using rule priorities.

Priority

Each rule has a Priority property of type Int32. We can see this property exposed in the Rule Set Editor screen shot above. Before executing rules, the rule set will sort its rules into a list ordered by priority. A rule with a high priority value will execute before a rule with a low priority value. The rule with the highest priority in the rule set will execute first. Rules of equal priority will execute in the alphabetic order of their Name property.

To make sure our notification rule is evaluated last, we need to assign the rule a priority of 0, and ensure all other rules have a higher priority. In the figure below, we've given our first three rules a priority of 100. The number we use for priority is arbitrary, as it only controls the relative execution order.

Rule Editor

Rule Dependencies

All the rules we've written so far are independent. Our rules do not modify any fields in the workflow that other rules depend upon. Suppose, however, we had a rule that said "If the IsSecurityRelated property of the bug is true, set the bug Priority to High". Obviously, the first three rules we wrote.

One solution to this problem would be to set the relative priorities of the rules to ensure the "set score" rules always execute after any rule that might set the Priority field. However, this type of solution isn't always feasible as the rule set grows larger and dependencies between the rules become more entangled.

Fortunately, Windows Workflow can simplify this scenario. If you look back at the class diagram from earlier, you'll notice the RuleCondition class carries a GetDependencies method, and a RuleAction class carries a GetSideEffects method. These two methods allow the rules engine to match the dependencies of a rule (which are the fields and properties the rule's condition inspects to compute its value) against the side effects of other rules (whcih are the fields and properties a rule's action modifies).

When an action produces a side effect that matches a dependency from a previously executed rule, the rules engine can go back and re-evaluate the previous rule. In rules engine terminology, we call this feature forward chaining.

Implicit Chaining

By default, the forward chaining in Windows Workflow is implicit. The rules engine examines the expressions in each rule condition and each rule action to produce lists of dependencies and side effects. We can go ahead and write our rule without worrying about priorities, as shown in the figure below (click to expand).

Rule Editor

Now, if the workflow looks at a bug with the IsSecurityRelated property set to true, the action of the new rule will change the bug's Priority to High. The rules engine will know that three previous rules have a dependency on the Priority property and re-evaluate all three rules. All of this happens before the NotificationRule runs, so a bug with IsSecurityRelated set will create a score of 100, and the NotificationRule will invoke the SendNotification method. We'll see these exact steps in more detail later.

Implicit chaining is a great feature because we don't have to calculate dependencies manually. For implicit chaining to work, however, the rules engine has to be able to parse the rule expression. If we have a rule that calls into our compiled code, or into third party code, the rules engine can no longer resolve the dependencies. In these scenarios, we can take advantage of chaining using metadata attributes or explicit actions.

Chaining with Attributes

Let's suppose the logic we need to execute for a rule is complicated - so complicated we don't feel comfortable writing all the logic declaratively. What we can do is place the logic inside a method in our code-behind file, and invoke the method from our rule. As an example, let's write the last rule like the following.

IF this.Bug.IsSecurityRelated
THEN this.AdjustBugForSecurity()

The method call presents a problem if we need forward chaining. The rules engine will not know what fields and properties the AdjustBugForSecurity method will change. The good news is, Windows Workflow provides attributes we can use to declare a method's dependencies and side effects.

Attribute Description
RuleWrite Declares a field or property that the method will change (a side effect of the method).
RuleRead Declares a field or property that the method will read (a dependency of the method).
RuleInvoke Declares a method that the current method will invoke. The engine will inspect the second method for additional attributes.

If a method does not carry one of these three attributes, the rules engine will assume the method does not read or write any fields or properties. If we want forward chaining to work with our method, we'll need to define it as follows.

[RuleWrite("Bug/Priority")]
public void AdjustBugForSecurity()
{
  
// .. other work

  Bug.Priority = BugPriority.High;

  
// .. other work
}

The RuleWrite attribute uses a syntax similar to the property binding syntax in Windows Workflow. This particular RuleWrite attribute declares that the method will write to the Priority property of the Bug property.

The rules engine will also parse a wildcard syntax, so that [RuleWrite("Bug/*")] would tell the engine that the method writes to all the fields and properties on the bug object. The RuleRead attribute uses this same syntax, except we would use a RuleRead attribute on methods called from the conditional part of our rules. The RuleRead attribute tells the engine about the method's dependencies.

We can use the RuleInvoke attribute when our method calls into other methods, as shown in the following example.

[RuleInvoke("SetBugPriorityHigh")]
public void AdjustBugForSecurity()
{
  
// .. other work

  SetBugPriorityHigh();

  
// .. other work
}

[
RuleWrite("Bug/Priority")]
void SetBugPriorityHigh()
{
  Bug.Priority =
BugPriority.High;
}

In this code sample, we've told the rules engine that the method called from our rule will in turn call the SetBugPriorityHigh method. The rules engine will follow the lead and inspect the SetBugPriorityHigh method for attributes. In this example the engine will find a RuleWrite attribute and forward chaining will continue to work.

Explicit Chaining

In some scenarios, we may need to call into third party code from our rules. This third party code may have side effects, but since we do not own the code, we cannot add a RuleWrite attribute. In this scenario, we can use an explicit Update statement in our rule actions. For example, if we used an explicit update statement with our AdjustBugForSecurity method instead of using a RuleWrite attribute, we'd write our declarative rule condition like the following.

  this.AdjustBugForSecurity()
  Update("this/Bug/Priority/")

Note that the update statement syntax is again similar to our RuleWrite syntax, and that there is no corresponding Read statement available. It is generally better to use the attribute-based approach whenever possible. This explicit approach is designed for scenarios when we cannot add method attributes, or when we need precise control over the chaining behaviour, as described below.

Controlling Chaining

The forward chaining behaviour of the rule set is powerful. We can execute rules and have them re-evaluated even when we don't know their interdependencies. However, chaining can sometimes produce unpleasant results. For instance, it is possible to put the rules engine into an infinite loop. It is also possible that we will write a rule that we never want the engine to re-evaluate. Fortunately, there are several options available to tweak rule processing.

Chaining Behavior

The first option is a ChainingBehavior property on the RuleSet class. The Rule Set Editor exposes this property with a drop down list labelled "Chaining". The available options are "Sequential", "Explicit Update Only", and "Full Chaining". "Full Chaining" is the default rule set behavior, and provides us with the behavior we've described so far.

The "Explicit Update Only" option tells the rules engine not to use implicit chaining. In addition, the rules engine will ignore RuleWrite and RuleRead attributes. The only mechanism available for chaining is the explicit Update statement we described in the last section. Explicit updates give us precise control over the rules that can cause a re-evaluation of previous rules.

The "Sequential" option disables chaining altogether. A rule set operating with sequential behaviour will execute all its rules only once, and in the order specified by their respective Priority properties (of course, a Halt statement could still terminate the rule processing before all rules complete execution).

Re-evaluation Behavior

Another option to control chaining is to use the ReevaluationBehavior property of a rule. This property is exposed in the Rule Set editor by a drop down list next to a rule labelled "Reevaluation". The available options are "Always" and "Never".

"Always" is the default behaviour for a rule. The rules engine will always re-evaluate a rule with this setting, if the proper criteria are met. This setting would not override a rule set chaining behaviour of "Sequential", for instance.

"Never", as the name implies, turns off re-evaluation. It is important to know that the rules engine only considers a rule "evaluated" if the rule executes a non-empty action. For example, consider a rule that has Then actions, but no Else actions, like the rules we've defined. If the rule is evaluated and its condition returns false, the rule is still a candidate for re-evaluation because the rule did not execute any actions.

Tracing and Tracking

Given the various chaining behaviors, and the complexities of some real world rule sets, we will find it useful to see what is happening inside the rules engine. As we discussed in the article "Hosting Windows Workflow", Windows Workflow takes advantage of the .NET 2.0 tracing API and it's own built-in tracking features to supply instrumentation information. In this section, we will explore the tracing and tracking features of the rules engine. Refer to the previous article for general details on tracing and tracking.

Tracing Rules

To setup tracing for the rules engine we need an application configuration file with some trace switches set. The following configuration file will log all trace information from the rules engine to a WorkflowTrace.log file. The file will appear in the application's working directory.

<configuration>
    <
system.diagnostics>
      <
switches>
        <
add name="System.Workflow.Activities.Rules" value="All" />
        <
add name="System.Workflow LogToFile" value="1" />
      </
switches>
    </
system.diagnostics>
</
configuration>

The amount of detail provided by the trace information can be useful for tracking down chaining and logic problems in our rule sets. The rule set we've been working with in this chapter will produce the following trace information (some editing applied).

Rule "SetScore_HighPriority" Condition dependency: "this/Bug/Priority/" Rule 
    "SetScore_HighPriority" THEN side-effect: "this/Score/" Rule 
    "SetScore_LowPriority" Condition dependency: "this/Bug/Priority/" Rule 
    "SetScore_LowPriority" THEN side-effect: "this/Score/" Rule 
    "SetScore_MediumPriority" Condition dependency: "this/Bug/Priority/" Rule 
    "SetScore_MediumPriority" THEN side-effect: "this/Score/" Rule 
    "AdjustBugForSecurity" Condition dependency: "this/Bug/IsSecurityRelated/" Rule 
    "AdjustBugForSecurity" THEN side-effect: "this/Bug/Priority/" Rule 
    "NotificationRule" Condition dependency: "this/Score/" Rule 
    "SetScore_HighPriority" THEN actions trigger rule "NotificationRule" Rule 
    "SetScore_LowPriority" THEN actions trigger rule "NotificationRule" Rule 
    "SetScore_MediumPriority" THEN actions trigger rule "NotificationRule" Rule 
    "AdjustBugForSecurity" THEN actions trigger rule "SetScore_HighPriority" Rule 
    "AdjustBugForSecurity" THEN actions trigger rule "SetScore_LowPriority" Rule 
    "AdjustBugForSecurity" THEN actions trigger rule "SetScore_MediumPriority"

This first part of the trace will provide information about dependency and side effect analysis. By the end of the analysis, we can see which actions will trigger the re-evaluation of other rules. Later in the trace, we can observe each step the rule engine takes when executing our rule set.

  Rule Set "BugScoring": Executing
  Evaluating condition on rule "SetScore_HighPriority".
  Rule "SetScore_HighPriority" condition evaluated to False.
  Evaluating condition on rule "SetScore_LowPriority".
  Rule "SetScore_LowPriority" condition evaluated to False.
  Evaluating condition on rule "SetScore_MediumPriority".
  Rule "SetScore_MediumPriority" condition evaluated to True.
  Evaluating THEN actions for rule "SetScore_MediumPriority".
  Evaluating condition on rule "AdjustBugForSecurity".
  Rule "AdjustBugForSecurity" condition evaluated to True.
  Evaluating THEN actions for rule "AdjustBugForSecurity".
  Rule "AdjustBugForSecurity" side effects enable rule "SetScore_HighPriority" reevaluation.
  Rule "AdjustBugForSecurity" side effects enable rule "SetScore_LowPriority" reevaluation.
  Rule "AdjustBugForSecurity" side effects enable rule "SetScore_MediumPriority" reevaluation.
  Evaluating condition on rule "SetScore_HighPriority".
  Rule "SetScore_HighPriority" condition evaluated to True.
  Evaluating THEN actions for rule "SetScore_HighPriority".
  Evaluating condition on rule "SetScore_LowPriority".
  Rule "SetScore_LowPriority" condition evaluated to False.
  Evaluating condition on rule "SetScore_MediumPriority".
  Rule "SetScore_MediumPriority" condition evaluated to False.
  Evaluating condition on rule "NotificationRule".
  Rule "NotificationRule" condition evaluated to True.
  Evaluating THEN actions for rule "NotificationRule".
  

There is a tremendous amount of detail in the trace. We can see the result of each condition evaluation, and which rules the engine re-evaluates due to side effects. These facts can prove invaluable when debugging a misbehaving rule set.

A more formal mechanism to capture this information is to use a tracking service, which we cover in the next section.

Tracking Rules

WF provides extensible and scalable tracking features to monitor workflow execution. One tracking service WF provides is a SQL Server tracking service that records events to a SQL Server table. The default tracking profile for this service records all workflow events. Although the tracking information is not as detailed as the trace information, tracking is designed to record information in production applications while tracing is geared for debugging.

To enable tracking, we'll need a tracking schema installed in SQL Server, and an application configuration file to configure tracking. The following configuration file will add the tracking service to the WF runtime and point to a WorkflowDB database on the local machine.

<configuration>
  
  <
configSections>
    <
section
      
name="WorkflowWithTracking"
      
type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection,
            System.Workflow.Runtime, Version=3.0.00000.0,
            Culture=neutral, PublicKeyToken=31bf3856ad364e35
"/>
  </
configSections>

  <
WorkflowWithTracking>
    <
CommonParameters>
      <
add name="ConnectionString"
          
value="Data Source=(local);Initial Catalog=WorkflowDB;
                     Integrated Security=true
"/>
    </
CommonParameters>
    <
Services>
      <
add
        
type="System.Workflow.Runtime.Tracking.SqlTrackingService,
              System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
              PublicKeyToken=31bf3856ad364e35
"              
             />
    </
Services>
  </
WorkflowWithTracking>
  
</
configuration>

If we run our bug scoring workflow with the above tracking, we can pull out rule-related tracking information. When the workflow completes, we can pass the workflow's instance ID to the following method and retrieve the rule tracking information.

private static void DumpRuleTrackingEvents(Guid instanceId)
{
  
WorkflowRuntimeSection config;
  config =
ConfigurationManager.GetSection("WorkflowWithTracking")
          
as WorkflowRuntimeSection;

  
SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery();
  sqlTrackingQuery.ConnectionString =
    config.CommonParameters[
"ConnectionString"].Value;

  
SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance;

  
if (sqlTrackingQuery.TryGetWorkflow(
              instanceId,
out sqlTrackingWorkflowInstance))
  {

    
Console.WriteLine("{0,-10} {1,-22} {2,-17}",
                      
"Time", "Rule", "Condition Result");

    
foreach (UserTrackingRecord userTrackingRecord in
             sqlTrackingWorkflowInstance.UserEvents)
    {
      
RuleActionTrackingEvent ruleActionTrackingEvent =
        userTrackingRecord.UserData
as RuleActionTrackingEvent;

      
if (ruleActionTrackingEvent != null)
      {
        
Console.WriteLine("{0,-10} {1,-24} {2,-17}",
                        userTrackingRecord.EventDateTime.ToShortTimeString(),
                        ruleActionTrackingEvent.RuleName.ToString(),
                        ruleActionTrackingEvent.ConditionResult.ToString());
      }
    }
  }
}

Notice that to retrieve the rule tracking events we need to dig into the user data associated with a UserTrackingRecord. The above code will produce the following output, which includes the result of each rule evaluation.

Rule tracking output

Dynamic Updates

Earlier, we mentioned that one of the advantages to using declarative rules is that we can dynamically modify rules and rule sets at runtime. If these rules were specified in code, we'd have to recompile and redeploy and application. With WF, we can use the WorkflowChanges class to alter an instance of a workflow.

If we give the following code an instance of our bug scoring workflow, it will initialize a new WorkflowChanges object with our workflow definition. We can then find the bug scoring rule set by name via a RuleDefinitions instance. Once we have our rule set, we can make changes to our rules.

private static void ModifyWorkflow(WorkflowInstance instance)
{
  
Activity workflowDefinition = instance.GetWorkflowDefinition();

  
WorkflowChanges workflowchanges;
  workflowchanges =
new WorkflowChanges(workflowDefinition);
  
CompositeActivity transient = workflowchanges.TransientWorkflow;

  
RuleDefinitions ruleDefinitions = (RuleDefinitions)transient.GetValue(
        
RuleDefinitions.RuleDefinitionsProperty
    );

  
RuleSet ruleSet = ruleDefinitions.RuleSets["BugScoring"];
  
foreach (Rule rule in ruleSet.Rules)
  {
    
if (rule.Name == "AdjustBugPriorityForSecurity")
    {
      rule.Active =
false;
    }

    
if (rule.Name == "NotificationRule")
    {
      
RuleExpressionCondition condition;
      condition = rule.Condition
as RuleExpressionCondition;

      
CodeBinaryOperatorExpression expression;
      expression = condition.Expression
as CodeBinaryOperatorExpression;
      expression.Right =
new CodePrimitiveExpression(120);
    }        
  }
  instance.ApplyWorkflowChanges(workflowchanges);
}

Once we have our rule set, we can iterate through our rules. In the above code, we are turning off the "AdjustBugPriorityForSecurity" rule. We can enable and disable rules on the fly by toggling the Active property of a rule.

We make changes that are even more dramatic to our notification rule. We are changing the rule's conditional expression from this.score > 75 to this.score > 120. Expressions can be tricky to manipulate, but remember the .rules file will contain an XML representation of the CodeDom objects that make the rule. We can look inside the file to see how the condition is built for the NotificationRule (shown below).

<RuleExpressionCondition.Expression>
   <
ns0:CodeBinaryOperatorExpression Operator="GreaterThan"
           
xmlns:ns0="clr-namespace:System.CodeDom;Assembly=System,
           Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
">
      <
ns0:CodeBinaryOperatorExpression.Left>
         <
ns0:CodePropertyReferenceExpression PropertyName="Score">
            <
ns0:CodePropertyReferenceExpression.TargetObject>
               <
ns0:CodeThisReferenceExpression />
            </
ns0:CodePropertyReferenceExpression.TargetObject>
         </
ns0:CodePropertyReferenceExpression>
      </
ns0:CodeBinaryOperatorExpression.Left>
      <
ns0:CodeBinaryOperatorExpression.Right>
         <
ns0:CodePrimitiveExpression>
            <
ns0:CodePrimitiveExpression.Value>
               <
ns1:Int32 xmlns:ns1="clr-namespace:System;Assembly=mscorlib,
                  Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
">
                  75
               </ns1:Int32>
            </
ns0:CodePrimitiveExpression.Value>
         </
ns0:CodePrimitiveExpression>
      </
ns0:CodeBinaryOperatorExpression.Right>
   </
ns0:CodeBinaryOperatorExpression>
</
RuleExpressionCondition.Expression>

Looking at the XML we can see that we need to replace the CodePrimitiveExpression assigned to the Right property of the CodeBinaryOperatorExpression. Using the CodeDom types we could replace the condition, modify actions, and even build new rules on the fly.

The modifications the code makes will apply to one specific instance of a workflow. In other words, we aren't changing the compiled workflow definition. If we want to turn the security rule off for all workflows in the future, we'd either have to run this code on every bug scoring workflow we create, or modify the rule set in the designer and recompile.

Summary

In this article, we've covered conditions and rules in Windows Workflow. There are several activities in WF featuring conditional logic, including the powerful ConditionedActivityGroup. The purpose of the Windows Workflow Policy activity is to execute sets of rules. These rules contain declarative knowledge, and we can prioritize rules and use forward chaining execution semantics. By writing out our business knowledge in declarative statements instead of in procedural code, we gain a great deal of flexibility. We can track and trace rules, and update rule sets dynamically. Windows Workflow is a capable rules engine.


Article by K. Scott Allen
Comments? Questions? Bring them to my blog.