Another real world application of MGrammar (Oslo)

June 12th, 2009 | Tags:

I have received a ton of emails and questions following my previous post on MGrammar (which is a part of Oslo). Some of those have been people asking advice on how they can adopt MGrammar as a basis for a rule engine for more generic purposes than the one I provided.

So, I decided to sit down, download the latest Oslo SDK and write a new rule engine based on a DSL implemented in MGrammar.

Basically, what I wanted to demonstrate, was how you can create a domain specific language, in which your business analysts, end-users and grease-monkeys can express business rules in natural-like-language, which you can then feed through MGrammar and parse into something relatively coherent.

Consider the following typical business rules at Northwind Traders Co.:

  • Only supervisors may place orders with a discount higher than 7%
  • The total price of a sales order must exceed the total cost.
  • Even though the designers of the software that Northwind uses had left the field Credit Rating on the customer records as optional, Northwind now wants it to be mandatory when adding new customers as part of their new and improved risk management strategy.

Now, these rules can be expressed in many ways. Imagine that you have written a shrink-wrapped sales system which you sell from your website. You really don’t want your 2 411 customers to call you on the phone while you’re busy playing the beta of Mass Effect 2 just because they want some optional field to suddenly become a required field, or because they want to restrict the discount level for sales staff on Mondays if there’s a full moon and the employee has been with the company for less than six months and wears jeans to work. Yes, I know! Customers really do get the strangest and often misguided “requirements” into their heads.

And let’s be honest, you couldn’t possibly have imagined that your newest customer would [one month later] want to restrict the discount level that sales employees that wear jeans to work are allowed to give on Mondays when there’s a full moon! Yet, there they are, at your doorstep, in their tastefully chalk-streaked $2000 suits, pulling you away from pruning your blog and monitoring the Google analysis charts, wanting this very feature implemented!

If only you had supplied the sales software with a simple rules engine so that customers could take care of these things on their own!

Constraining business objects using a natural-like language

Using a domain specific language, the above bulleted rules could be expressed like this:

RuleSet Northwind
  // Max discount of 7%
  Add rule for Order that requires Discount to be
    less than or equal to "0.07" unless
    SalesPerson.Supervisor is true

  // Price must be gt cost
  Add rule for Order that requires TotalPrice to be
    greater than TotalCost

  // Credit rating is now a required field
  Add rule for Customer that requires CreditRating
    to not be empty
End RuleSet

It’s enough like plain English, that whoever writes these rules won’t need to learn another language and syntax, only some basic rules. That, and somehow be provided with a list of what fields there are. You could provide a user interface that adds the “Add rule for” text at the beginning of each new line as the user presses the enter key, and have a ListBox that shows a list of business objects such as Customer, Order, Product that the user can double-click on to insert into the text at the current cursor position. Well, you get the idea.

Now, in your code, you may have business objects (Linq to SQL or Entities or whatever), like this:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string CreditRating { get; set; }
}

You could then implement an extension method called IsValid that will take care of all the heavy lifting for you (yes, very heavy…):

public static class RuleSet
{
    public static bool IsValid(this object value)
    {
        return evaluate(value);
    }
}

And invoke it like this:

// Read business rules from file or blob field in database
RuleSet.Add(resourceStream);

// Create a customer
Customer c = new Customer
{
    Id = 10,
    Name = "Contoso",
    CreditRating = null // <- Will cause validation to fail!
};

// Will print out: Customer valid: False
Console.WriteLine("Customer valid: {0}", c.IsValid());

c.CreditRating = "Excellent";

// Will print out: Customer valid: True
Console.WriteLine("Customer valid: {0}", c.IsValid());

What I do, is feed the business rules along with the DSL specification through MGrammar and project the result into a class library that uses reflection to validate the rules.

The MGrammar needed to parse and understand the above business rules is quite simple, actually:

module ObjectContraints
{
    export ObjectRules;

    language ObjectRules
    {
        // Basic tokens
        token Whitespace = (' ' | '\r' | '\n' );
        token Digit = ('0'..'9');
        token Identifier =
            ('A'..'Z' | 'a'..'z' | '.' | '_')+;
        token Linebreak = '\n' | '\r' | '\r\n';

        // Keywords
        @{Classification["Keyword"]}
        token RuleSet = 'RuleSet';
        @{Classification["Keyword"]}
        token End = 'End' | 'end';
        @{Classification["Keyword"]}
        token AddRuleFor = 'Add rule for';
        @{Classification["Keyword"]}
        token ThatRequires = 'that requires';
        @{Classification["Keyword"]}
        token ToBe = 'to be';
        @{Classification["Keyword"]}
        token ToNotBe = 'to not be';
        @{Classification["Keyword"]}
        token Is = 'is';
        @{Classification["Keyword"]}
        token IsNot = 'is not';
        @{Classification["Keyword"]}
        token When = 'when';
        @{Classification["Keyword"]}
        token Unless = 'unless';

        // Operators
        @{Classification["Keyword"]}
        token EqualTo = 'equal to';
        @{Classification["Keyword"]}
        token GreaterThan = 'greater than';
        @{Classification["Keyword"]}
        token GreaterThanOrEqualTo = 'greater than or equal to';
        @{Classification["Keyword"]}
        token LessThan = 'less than';
        @{Classification["Keyword"]}
        token LessThanOrEqualTo = 'less than or equal to';
        @{Classification["Keyword"]}
        token Empty = 'empty';
        @{Classification["Keyword"]}
        token True = 'true';
        @{Classification["Keyword"]}
        token False = 'false';

        // Comments
        @{Classification["Comment"]}
        token CommentToken = CommentDelimited | CommentLine;
        token CommentDelimited =
          "/*" CommentDelimitedContent* "*/";
        token CommentDelimitedContent = ^('*') | '*'  ^('/');  

        token CommentLine = "//" CommentLineContent*;
        token CommentLineContent
          = ^(
               '\u000A' // New Line
            |  '\u000D' // Carriage Return
            |  '\u0085' // Next Line
            |  '\u2028' // Line Separator
            |  '\u2029' // Paragraph Separator
            );  

        // Quoted values
        token QuoteDelimited =
          '"' c:QuoteDelimitedContent* '"' => c;
        token QuoteDelimitedContent = ^('"');              

        interleave Skippable = Whitespace | Comment;
        interleave Comment = CommentToken;

        // Main syntax
        syntax Main = RuleSet name:Identifier rules:Rule* End RuleSet
            => { Name => name, Rules => rules };

        // Single rule syntax
        syntax Rule = AddRuleFor className:Identifier ThatRequires
                expression:Expression1 condition:Condition?
            => { ClassName => className, Expression => expression,
                    Condition => condition };

        syntax Expression1 = propertyName:Identifier inverse:ToBeOrNotToBe
            operation:Operation =>
            { PropertyName => propertyName, Inverse => inverse,
              Comparison => operation };

        syntax Expression2 = propertyName:Identifier inverse:IsOrIsNot
            operation:Operation =>
            { PropertyName => propertyName, Inverse => inverse,
              Comparison => operation };

        syntax Operation =
                  EqualTo value:Value               =>
                        { Operator => "Eq", Value => value }
                | GreaterThanOrEqualTo value:Value  =>
                        { Operator => "GtEq", Value => value }
                | GreaterThan value:Value           =>
                        { Operator => "Gt", Value => value }
                | LessThanOrEqualTo value:Value     =>
                        { Operator => "LtEq", Value => value }
                | LessThan value:Value              =>
                        { Operator => "Lt", Value => value }
                | Empty                             =>
                        { Operator => "IsEmpty" }
                | True                              =>
                        { Operator => "IsTrue" }
                | False                             =>
                        { Operator => "IsFalse" };

        syntax Condition = c1:ConditionWhen => c1 | c2:ConditionUnless => c2;
        syntax ConditionWhen = When expression:Expression2 => expression;
        syntax ConditionUnless = Unless expression:Expression2 => expression;

        syntax ToBeOrNotToBe = ToNotBe => true | ToBe => false;
        syntax IsOrIsNot = IsNot => true | Is => false;

        syntax Value = value1:Identifier => Identifier { value1 }
                     | value2:QuoteDelimited => Literal { value2 };
    }
}

That’s it!

The class library that evaluates the business rules is also quite simple. The core method is Evaluate which is invoked by the IsValid extension method:

public bool Evaluate(object value)
{
    if (value == null)
        return false;

    if (PropertyInfo == null)
        return false;

    object propertyValue = PropertyInfo.GetValue(value, null);
    Type propertyType = PropertyInfo.PropertyType;

    bool result = false;
    switch (Operator)
    {
        case RuleOperator.Eq: result = (compare(propertyValue,
            getValue(value)) == 0); break;
        case RuleOperator.Gt: result = (compare(propertyValue,
            getValue(value)) == -1); break;
        case RuleOperator.GtEq: result = (compare(propertyValue,
            getValue(value)) >= 0); break;
        case RuleOperator.IsEmpty: result =
            (compare(propertyValue, null) == 0); break;
        case RuleOperator.IsFalse: result =
            (compare(propertyValue, false) == 0); break;
        case RuleOperator.IsTrue: result =
            (compare(propertyValue, true) == 0); break;
        case RuleOperator.Lt: result =
            (compare(propertyValue, getValue(value)) == 1); break;
        case RuleOperator.LtEq: result =
            (compare(propertyValue, getValue(value)) <= 0); break;
        default: return false;
    }
    return result ^ Inverse;
}

In fact, instead of projecting it into an expression tree-like class library and use reflection to validate the object, you could actually emit real IL code!

- I’ll leave that part as an exercise for the reader :)

Well, for now, here’s a natural-language-like business rules engine that validates objects using reflection.

Source code is for Visual Studio 2010 B1 with May 2009 CTP of Oslo.

Download source code:

Disclaimer: The source code has some limitations. It will not follow property paths (such as SalesPerson.Manager.IsSupervisor). It’s limited to simple literals and property names and the operators listed above. It is intended as a source of inspiration and not a third-party library that you can download and hook into your production code.

Technorati Tags: ,,,
  1. Peter Giles
    June 19th, 2010 at 02:16
    Reply | Quote | #1

    Excellent article.

    Any chance of getting this working with the November 2009 CTP and the RTM VS2010? I downloaded your solution and it doesn’t have references to any of the Oslo assemblies and I think DynamicParser is no longer in the API.