Rule engine with WYSIWYG rule serialization

I’ve been searching for a rule engine for our projects, and the one that suits our needs is a rule engine that comes with Windows Workflow Foundation. Although Microsoft does not advertise WF rule engine as an independent component that can be used in workflow-less applications, nothing prevents developers from grabbing System.Workflow.Activities.dll and start creating and executing RuleSet instances. The example (with sample project) of how to build and execute stand-alone rule sets can be found in Guy Burstein’s blog.

In principle this should be sufficient to start using WF rule engine in any project that will benefit from rules described in an external storage rather than written directly in application source code. However, something stopped me from replacing my rule-alike patterns with rules that can be executed by WF rule engine. And it was not engine functionality – the engine is both very powerful and simple to use – it was such insignificant detail as rule serialization style.

WF rule engine uses XML serialization with CodeDom expressions. To give you an idea of how a serialized rule might look, I wrote a simple rule that assigned value “10” to a “ServiceFee” property and pasted here not the whole rule but only its assignment part. So the meaning of the next 18 lines is just “ServiceFee = 10”, not more.

    1 <RuleStatementAction.CodeDomStatement>

    2     <ns0:CodeAssignStatement LinePragma={p1:Null} xmlns:ns0=clr-namespace:System.CodeDom;Assembly=System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089>

    3         <ns0:CodeAssignStatement.Left>

    4             <ns0:CodePropertyReferenceExpression PropertyName=ServiceFee>

    5                 <ns0:CodePropertyReferenceExpression.TargetObject>

    6                     <ns0:CodeThisReferenceExpression />

    7                 </ns0:CodePropertyReferenceExpression.TargetObject>

    8             </ns0:CodePropertyReferenceExpression>

    9         </ns0:CodeAssignStatement.Left>

   10         <ns0:CodeAssignStatement.Right>

   11             <ns0:CodePrimitiveExpression>

   12                 <ns0:CodePrimitiveExpression.Value>

   13                     <ns1:Int32 xmlns:ns1=clr-namespace:System;Assembly=mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089>10</ns1:Int32>

   14                 </ns0:CodePrimitiveExpression.Value>

   15             </ns0:CodePrimitiveExpression>

   16         </ns0:CodeAssignStatement.Right>

   17     </ns0:CodeAssignStatement>

   18 </RuleStatementAction.CodeDomStatement>

A simple (and respectful) response to my observation is “who cares?” Really, why should anyone even check what format is used by an external component to serialize its data? And has it been a case of a graphical editor, I would probably fully share this opinion. Wait, not fully. If you use a component that stores graphics not in JPEG or GIF, but in its own proprietary format, you would probably care. Or at least you would make sure that materials prepared by your graphic designers can be converted into that proprietary format without much hassle.

The same argument is valid here. And the argument is even stronger, because unlike graphical image that you won’t likely be adjusting using a hex editor, it is very tempting to be able to change the statement “ServiceFee = 10” to “ServiceFee = 15” by hand. No matter how the rule is physically stored (a database, an XML or a plain text file), there is an advantage that it is both human readable and human writable. Especially during development phase when various rules are created and modified all the times for unit tests and QA, but also in a production phase when a customer can read the content of the rule file over the phone.

Another aspect is rule authoring. Guy Burstein’s sample uses RuleSetDialog class to manage rules. I assume you would like to have a control over rule management (even though RuleSetDialog supports Intellisense) and if your application is configured over the Web then using Windows forms is not even an option. If your configurable rules are based on simple conditions and actions (and I believe this is the common case), ability to represent these rules in a simple form will save you some development efforts.

The task of making rule serialization format human-friendly is not really difficult, because built-in Workflow Foundation rule engine has an internal parser that has no problem of understanding what “ServiceFee = 10” means without requiring this sentence to be converted first to CodeDom statement. Unfortunately, System.Workflow.Activities.Rules.Parser is not a public class, so it is not possible to instantiate it without using reflection. But with a little help of Type.GetType and a recent blog post that focuses exactly on this topic, invoking the internal WF rule parser does not become complicated. In my examples below I will be using with slight modifications the implementation suggested by beaucrawford.net’s blog owner (he does not expose his name in the blog).

So let us now build a simple rule editor that can be used to author and execute rules using Windows Workflow Foundation rule engine. The main requirement to the implementation is that rules should be stored and displayed in a WYSIWIG format, in most cases identical to how you would write them in C#.

Does this mean we need to support “using”?

WF rule engine resolves names within the scope of the type of an object that was sent to a RuleExecution constructor. RuleExecution constructor has the following signature:

RuleExecution(RuleValidation validation, object thisObject);

It is type of “thisObject” that is used as a base for name resolution. So in case of statement “ServiceFee = 10” it is assumed that there is a type with a property “ServiceFee” and it is an instance of this type that is sent for rule execution. In fact, RuleExpressionCondition and RuleAction ToString overrides add “this” qualifier to emphasize that properties belong to a respective object instance: “this.ServiceFee = 10”.

So far so good, and we can probably live with “this” prefix, although it is a beginning of a compromise with WYSIWIG, but the situation gets worse with enums. Rule expressions tend to need enum values unless they are trivial or completely data driven. And since enum definitions obviously don’t belong to “thisObject” type, you will have to provide fully qualified type name for them:

PaymentMethod = MyCompany.PaymentServices.CommonDefinitions.PaymentMethod.CreditCard;

Well, it is still understandable but after hand-writing a hundred rules you may start to regret that you gave up RuleSetDialog with its Intellisense support. And there is not much you can do here because rule parser is CodeDom-driven, and anything that does not belong to “thisObject” instance must be fully qualified, there is no magic here. But unlike Workflow Foundation framework that is domain-neutral, your components are domain-specific and can take advantage of knowledge about namespaces and types that are used in rule definition.

So I modified the code that invokes WF rule parser to preprocess rule condition and action lists and replace values of known enumerators with fully qualified representations. I also modified the code that displays RuleSet items to perform the opposite conversion. When doing this, I also used opportunity to get rid of “this” prefixes.

I should make myself clear: my string conversions are straightforward (they all use string.Replace), so they can’t be used for rules of an arbitraty complexity (for example, when a rule condition may contain string literals matching not fully qualified enumerator values). I am not saying however that this approach won’t fit your needs. Actually I believe it fits majority of rule sets. At least I am not going to spend a few hours on regex matching just to enable a string pattern “PaymentMethod.CreditCard” to be used in my rules with any other meaning than enum value.

So we are now ready to proceed with full WYSIWIG support, and by saying this I mean that you can write a statement “PaymentMethod = PaymentMethod.CreditCard”, and it will be stored and displayed exactly as you wrote it.

Time to define some rules

For the purpose of proof of concept we don’t need to deal with very complex rules. It’s worth however to demonstate some of the advantages of using a powerful rule engine, so next time there is a need to handle configurable rules, it will be easy to decide if it is justified to drag a reference to System.Workflow.Activities assembly into your project.

Here is a list of criteria that our rules should satisfy:

  • Rule sets should be based on different types, e.g. when creating instances of RuleValidation and RuleExecution objects, we should experiment with different “thisType” and “thisObject” arguments. This does not add complexity to the rules, but it helps building auxilliary classes in a more generalized fashion.
  • Individual rule sets should contain multiple rules. This requirement opens for inspection of rules priority and chaining.
  • At least one rule should trigger re-execution of previously evaluated rule. This means that a value that is used inside one rule condition sould later be modified in another rule’s action.
  • At least one rule should contain a call to a method that invokes a delegate. Use of delegates in rule sets increases their extensibility.
  • Executing rules on different data should demonstrate optimized rule evaluation, so expressions that require expensive method calls can be built in such a way that skips their evaluation in case other conditions fails.

We will use a traditional domain of Web shops and define two rule sets: one with rules for payments and the second one with rules for shipment. The rule sets will be extremely minimalistic, but they should satisfy the requirements above.

Payment rules

The base class for payment rules is PaymentDetails which is defined here:

[Flags]
public enum PaymentMethod
{
    None,
    PayPal = 1,
    CreditCard = 2,
    Any = PayPal | CreditCard
}

public class PaymentDetails
{
    public int Weight { get; set; }
    public decimal OrderAmount { get; set; }
    public decimal ShipmentCost { get; set; }
    public decimal TotalAmount { get; set; }
    public PaymentMethod AvailablePaymentMethods { get; set; }
}

There are two rules that are defined for these payment details:

  • If Weight is less than 10, then ShipmentCost is 5, otherwise shipment cost is 10.
  • If TotalAmount is less than 20, then AvailablePaymentMethods contain only PayPal, otherwise any payment method is allowed.

The rules are very simple, however notice that the second rule is affected by the first rule: available payment methods depend on total amount that in turn depends on shipment cost that depends on weight.

Shipment rules

Shipment rules are defined based on class ShipmentDetails:

[Flags]
public enum ShipmentMethod
{
    None,
    FedEx = 1,
    UPS = 2,
    DHL = 4,
    International = UPS | DHL,
    Any = FedEx | UPS | DHL
}

public enum ShipmentPreference
{
    None,
    CostOptimization,
    SpeedOptimization
}

public class ShipmentDetails
{
    internal Func hasPendingOrders;

    public string Country { get; set; }
    public string Address { get; set; }
    public ShipmentMethod AvailableShipmentMethods { get; set; }
    public ShipmentPreference Preference { get; set; }
    public bool HasPendingOrders() { return hasPendingOrders(); }
    public bool ReadyForShipment { get; set; }
}

There are also two rules for shipment:

  • If Country is “USA”, then any shipment methods are available, otherwise only UPS and DHL.
  • If shipment is optimized for speed, then it is ready, otherwise it is ready if there are no other pending orders.

The first rule is as simple as it looks, but the second rule lets us experience execution of rules with delegate invocation. Pending orders are checked within HasPendingOrders method that invokes a potentially expensive delegate assigned to “hasPendingOrders” and should only be called when such check is necessary.

Simple Rule Editor

I usually explorer components by writing unit tests, but in this case it is worth building a simple UI to manage and execute rules. Rules will be administered by people from different departments, some people will write them, other people will test them. So I wanted to get a visual impression of rule management process. What fields should rule editor contain? How easy is to understand what rules do by looking at editor’s window? How can people test the rules when defining them?

So I wrote a simple rule editor. Here how it looks (I loaded with payment rules defined earlier and highlighted the rule for available payment methods):

SimpleRuleEditor

 

Using this editor we can quickly define payment and shipment rules. You can download the editor with its source code and rule definition files, and the following table contains all conditions and actions exactly as their are specified in these files:

Rule type

Rule name

Priority

Condition

Then Action

Else Action

PaymentShipment cost

2

Weight < 10

ShipmentCost = 5;
TotalAmount = OrderAmount + ShipmentCost

ShipmentCost = 10;
TotalAmount = OrderAmount + ShipmentCost
PaymentPayment method

1

TotalAmount < 20AvailablePaymentMethods = PaymentMethod.PayPalAvailablePaymentMethods = PaymentMethod.Any
ShipmentShipment method

1

Country == “USA”AvailableShipmentMethods = ShipmentMethod.AnyAvailableShipmentMethods = ShipmentMethod.International
ShipmentReadiness

2

Preference == ShipmentPreference. CostOptimization && HasPendingOrders()ReadyForShipment = FalseReadyForShipment = True

The editor’s main form has “Apply” button that can be used to execute the currently loaded rule set on a sample data. The data fill-in form is tailored to match respective rule category.

PaymentRules

The result of this sample data execution exposes one important WF rule engine feature: re-evaluation of affected rules. As you can see, the total amount is 23, but this value was set only after execution of the “Shipment cost” rule. If you swap “Shipment cost” and “Payment method” rules priorities, so “Payment method” will be executed first (greater priority value means earlier execution), befor the total order amount is calcuated, the rule set execution result will still be correct. This is because the editor assigns rules chaining behavior to RuleChainingBehavior.Full. If you change chaining behavior to None, then you will have to ensure the rules are executed in correct order, otherwise affected rules will not be re-evaluated.

Now let’s load shipment rules and apply them to sample data.

ShipmentRules1

If we press now “Execute” button, the rule engine should invoke a method HasPendingOrders that in turn calls a delegate with the following definition:

hasPendingOrders = () => { MessageBox.Show("Checking pending orders...");  return this.checkPendingOrders.Checked; }

So we should see a message box informing us that the delegate is executed. And here it comes:

PendingOrders

Now let’s change the sample data, so the shipment is optimized for speed. Since the rule condition says “Preference == ShipmentPreference. CostOptimization && HasPendingOrders()” we should expect rule engine to skip a call to HasPendingOrders and proceed straigth to “Then” action. And it does! No message box is displayed this time, and we can see the rule execution result with status “Ready”:

ShipmentRules2

Conclusion

After playing a few hours with Windows Workflow Foundation rule engine and customizing presentation of rule sets, so they can be stored and displayed in a very compact form that is easy to understand to even non-technical people, I am very positive about using this rule engine in projects that include configurable business rules. I predict reduction in code change requests caused by business rules modifications. With proper definition of rule base classes in many cases it will be sufficient just to redeploy new rules.

One thought on “Rule engine with WYSIWYG rule serialization”

  1. Hi,

    Is it possible to

    use the ouput of one rule in to another like
    if this.rule1.output < this.rule2.output
    then output = 50
    else output = 100

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>