OdeToCode IC Logo

Workflow Policy, Rules, and Collections

Monday, October 2, 2006

Q: Great article on the WWF Rules engine. I am curious how one would implement a rule that is applied to a collection of items, something akin to "IF request.Payments[n].Amount > 10000 THEN request.Payments[n].RequiresApproval = true"

A: There are quite a few approaches to choose from. As usual - it depends on the application!

One solution is to use a WhileActivity and loop through the collection. This explicit approach would run a PolicyActivity for each item in the collection.

The Windows SDK contains an interesting alternative approach in the "Processing Collections in Rules" entry. This is approach is worthy of note because it uses the features and flexibility of the rules engine to perform the iteration without any outside help. The approach specifically relies on the following features:

  • Every rule in a Windows Workflow rule set has an associated priority number. The priority number determines when the rule will execute relative to other rules.
  • The rules engine in WF analyzes the dependencies between rules. When a rule updates a dependency, the rules engine can re-evaluate previous rules that used the dependency.

With a Payment class defined in your domain model, the workflow class can accept an array of Payment references as a parameter:

public partial class Workflow1 : SequentialWorkflowActivity
{      
  
public Payment[] Payments
   {
      
get { return _payments; }
      
set { _payments = value; }
   }
  
  
Payment[] _payments;
  
IEnumerator _enumerator;
  
Payment _currentPayment;
}

Next comes the RuleSet:

Name Priority Rule
GetEnumerator 3 IF 1=1 THEN
  this._enumerator = this._payments.GetEnumerator()
MoveEnumerator 2 IF this._enumerator.MoveNext() THEN
  this._currentPayment = (Payment)this._enumerator.Current
One Or More Biz Rules 1 this._currentPayment.Amount > 10000 THEN
  this._currentPayment.RequiresApproval = True
ELSE
  this._currentPayment.RequiresApproval = False
Force re-eval 0 IF this._currentPayment == this._currentPayment THEN
  Update("this/_enumerator/")

GetEnumerator runs first because it has the highest priority. MoveEnumerator runs second - again because of priority. After these two rules finish, _currentPayment will reference the first Payment object in the array. All the "business" rules could now execute on that payment and decide on an outcome.

The interesting piece happens in the last rule, which always evaluates to true and performs an explicit "Update" on the _enumerator field. "Update" is a rule action that explicitly tells the engine about a side effect. In this rule, we are telling the engine that we've changed _enumerator. Even though we haven't actually changed _enumerator, we've forced the engine to look for previous rules with a dependency on _enumerator.

The rules engine knows the MoveEnumerator rule has a dependency on _enumerator, so it re-executes this rule. If MoveNext returns true in this rule, we update the _currentPayment field. The rules engine also detects this implicit side effect, and will reevaluate all the business rules that depend on _currentPayment. If MoveNext returns false, there are no more side effects and the Policy activity can close.

This is a clever pattern which keeps collection processing inside of a single activity.