Using IronRuby To Implement Dynamic Business Rules

The is part three of my three part series on Domain Validation. In the previous entry in this series I showed you how to store rules in the database and then have those rules dynamically compiled so that we can run them against our domain entities (business objects). We also saw that there was some issues with this approach, and so we came to the conclusion that what we really needed was a scripting language which would interact with C# so that we could use to build our rules. Well, thanks to the IronRuby team we have just that.

So, the first thing that we need to do is get IronRuby setup in our project. We can do that by simply adding references to IronRuby.dll, IronRuby.Libraries.dll, Microsoft.Scripting.dll, and Microsoft.Scripting.Core.dll. These will all be found in your \IronRuby\trunk\build directory after you grab and build the latest version of IronRuby. If you don’t know how to do this, I wrote a post a while back on how to get IronRuby up and running.

So, now that you have built and referenced IronRuby, lets look at how we can get this running. I am going to first create a property to wrap the creation of the Microsoft.Scripting.ScriptEngine. This is the generic script engine that Microsoft is now using for the front-end of its two scripting languages (IronPython and IronRuby). This property looks like this:

private ScriptEngine scriptEngine;
public ScriptEngine ScriptEngine { 
    get
    {
        if (scriptEngine == null)
        {
            var setup = new ScriptRuntimeSetup();
            setup.LanguageSetups.Add(
                new LanguageSetup(
                    "IronRuby.Runtime.RubyContext, IronRuby",
                    "IronRuby 1.0",
                    new[] { "IronRuby", "Ruby", "rb" },
                    new[] { ".rb" }));
            var runtime = ScriptRuntime.CreateRemote(AppDomain.CurrentDomain, setup);
            scriptEngine = runtime.GetEngine("Ruby");        
        }
        return scriptEngine;
    }             
}

This is just a bit of boilerplate code for setting up IronRuby to run, and in fact, it can be done declaratively. Most of the info in this declaration we won’t even need, since we won’t be referencing any source files or anything, but we might as well do it correctly in case anyone decides to start copying and pasting. :-)

Now, just like before we are going to pull our list or RuleData classes out of the database. Our RuleData class just stores the info needed to build and report errors on our rules, and it looks like this:

internal class RuleData
{
    private readonly string[] property;
    private readonly string errorMessage;
    private readonly string rule;

    public RuleData(string rule, string errorMessage, params string[] property)
    {
        this.property = property;
        this.errorMessage = errorMessage;
        this.rule = rule;
    }

    public string Rule
    {
        get { return rule; }
    }

    public string ErrorMessage
    {
        get { return errorMessage; }
    }

    public IEnumerable<string> Properties
    {
        get { return property; }
    }
}

If you read the previous entry in this series you will have seen how much effort it took to read in and compile our rules in C# using the CodeDOM. Well, lets take a look at what it is going to take to get this working in Ruby. So, we first created our rules like this:

new RuleData("e.SomeValue > 1", "SomeValue Must Be Greater Than 1", "SomeValue");
new RuleData("e.SomeOtherValue < 1", "SomeOtherValue Must Be Less Than 1", "SomeOtherValue");

The first string is our rule, where “e” is the generic identifier for our entity. Our entity has two properties “SomeValue” and “SomeOtherValue” and the first one must be greater than 1 while the second one must be less than 1. We don’t even need to change the syntax for our Ruby code, since the boolean operators are the same.

One thing to note is that here we are manually instantiating these rules, but since they are just three strings, you can store them in the database with the full type name as your key. Something like this would do:

Type type = typeof(T);
string rulesKey = type.FullName + ruleType;

So, now all we have left to do is turn our rules into something that we can execute from C# code. Well, hate to disappoint, but here is the code that does this:

foreach (RuleData rule in ruleData)
{
    string source = String.Format("Proc.new {{ |e| {0} }}", rule.Rule);
    ScriptSource scriptSource = ScriptEngine.CreateScriptSourceFromString(source);                
    var proc = (Proc)scriptSource.Execute();
    Predicate<T> predicate = p => (bool)proc.Call(p);
    result.Add(new StaticRule<T>(predicate, rule.ErrorMessage, rule.Properties.ToArray()));
}

Almost too easy, huh? In the first line we are inserting our rule into a bit of wrapper code that creates a Proc, which is the equivalent of a delegate in C#. So, our rule would look like this: “Proc.new { |e| e.SomeValue > 1 }”. Notice we are using double curly braces since we have the code inside of a call to “String.Format”. Next we create our script source from this code, and then we execute it, which returns our IronRuby.BuiltIns.Proc class. Then we create a predicate delegate to wrap this Proc and pass the parameter from the Predicate delegate into the “Call” method on the proc object. The final line just uses this Predicate to create a new “StaticRule” class and insert it into our result list.

We don’t have any of the problems that we had with the dynamically compiled C# code, but since we are executing this code every time through we are taking a bit of a performance hit. We can speed this up a bit by calling “scriptSource.Compile()” instead of “Execute()” which would allow us to cache our compiled rules. The “Compile” method just returns a CompiledCode class which has its own “Execute()” method that we can use.

In this post we have seen how you can leverage IronRuby in order to save, load, and execute dynamic rules against business entities. We have not implemented anything close to a real rule engine here, but for many applications being able to save and load business rules on entities may be enough. The rules that you can construct in this manner can actually be quite complex. I hope that you find this useful!

Be Sociable, Share!

Leave a comment