Vagif Abilov's blog on .NET


WYSIWYG rule editor: create and test rules for any .NET type

In my previous post I showed a simple WYSIWYG rule editor (that internally uses Windows Workflow Foundation rule engine) that does not serialize rules using CodeDom notation. I wrote this editor simply to demonstrate the concept, and its code combined both rule editing, rule serialization and sample rule classes. Since I expect rule authoring and management to be hot topic in the projects I am involved, I decided to dedicate some time to clean up the rule editor and make it generic rule authoring tool that can be used with almost any class that can be loaded from an arbitrary .NET assembly.

Let me start presenting the new editor with a few screen shots that give a pretty good picture of how it can be used.

Simple rule editor in action

When you start the editor, first thing you need to do is load an assembly that contains types used to build rules.

SimpleRuleEditor1

I’ve chosen here one of Microsoft Enterprise Library assemblies, Microsoft.Practices.EnterpriseLibrary.Logging.dll. Next is to choose one of its type. Let it be LogEntry.

Now you can start defining and testing rules, but to make a rule definition task easier, the editor has a helper page that can be displayed by clicking an “Info” button:

InfoPage

The Rule Information page is displayed side by side with the main editor form, and it even supports drag and drop, so you can simply drag properties into the “Condition”, “Then” or “Else” text boxes. It can save you from typos, and I was able to quickly define a LogEntry rule: “If Severity == TraceEventType.Error and Message.Contains(“Space mission”) Then Priority = 5”:

LogEntryRule

Every rule should be tested, and our editor has an “Apply” button that displays a rule object data input page. The “Value” column is editable, so to test the rules we can enter some data there. Of course, the most interesting input is the one that should trigger the rule:

LogEntryObject1

And the rules are invoked by clicking the “Execute” button.

LogEntryObject2

As you can see, defining and testing a rule on an arbitraty .NET class is a matter of seconds, and the rules can be saved for later use. Now let’s inspect how this was done.

Loading assembly for type instantiation

While the task of assembly selection and loading is trivial, certain extra steps need to be performed to prevent subsequent call to Activator.CreateInstance to fail with FileNotFoundException. And this exception will be thrown if the instantiated types depend on types from other assemblies referenced by the assembly loaded with Assembly.LoadFile. Successful instantiation of types from this assembly requires two steps:

  • Loading referenced assemblies
  • Handling AssemblyResolve event that is fired by the current domain.
private Assembly LoadAssembly(string assemblyPath)
{
    Assembly assembly = Assembly.LoadFile(assemblyPath);
    foreach (AssemblyName assemblyRef in assembly.GetReferencedAssemblies())
    {
        Evidence evidence = new Evidence(
            new object[] { new Zone(SecurityZone.MyComputer) },
            new object[] { });

        string path = Path.Combine(Path.GetDirectoryName(assemblyPath), assemblyRef.Name) + ".dll";
        if (File.Exists(path))
        {
            AssemblyName asmName = new AssemblyName();
            asmName.Name = assemblyRef.Name;
            asmName.CodeBase = Path.GetDirectoryName(assemblyPath);
            Assembly asm = Assembly.LoadFrom(path, evidence);
        }
    }
    return assembly;
}

Code to handle domain-specific events can be added to the application Program class, and the event handlers can be registered right on program startup.

[STAThread]
static void Main()
{
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
    AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(CurrentDomain_AssemblyLoad);

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new MainForm());
}

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    if (Program.Assemblies.Keys.Contains(args.Name))
    {
        return Program.Assemblies[args.Name];
    }
    return null;
}

static void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
{
    if (!Program.Assemblies.Keys.Contains(args.LoadedAssembly.FullName))
    {
        Program.Assemblies.Add(args.LoadedAssembly.FullName, args.LoadedAssembly);
    }
}

Rule serialization

Comparing to my previous post, I moved rule serialization code to a dedicated RuleSetSerializer class but kept it minimalistic. It stores rules in a plain tab-separated text file, and I did this on purpose. As I wrote earlier, I see a great value in keeping serialized rules in a human readable format. Would you have to spend more time on management of SQL queries if they all were converted into CodeDom trees? Would configuration files become more complex to work with if they were stored in binary form? Definitely. This is why I mean it is worth storing a rule “if a == 1 then b = 2” exactly as it looks. A simple serializer streaming the rules as tab-separated lines is just an example for this rule editor. When storing rules in a database conditions and action lists will most likely be written in different table columns but I would still keep their representation compact in case of non-complicated rule sets.

Populating property and method lists

There the fun began. I had to flatten property trees and build a list of all properties and methods for the top class and all nested classes. Imagine we create rules on a class called FirstLevelClass that include properties pointing to instances of SecondLevelClass that in turn has a property pointing to an instance of ThirdLevelClass:

public class FirstLevelClass
{
    public string Name { get; set; }
    public string Description { get; set; }
    public SecondLevelClass SecondLevel { get; set; }

    public bool FirstMethod() { return true; }
}

public class SecondLevelClass
{
    public string Name { get; set; }
    public string Description { get; set; }
    public ThirdLevelClass ThirdLevel { get; set; }

    public bool SecondMethod() { return true; }
}

public class ThirdLevelClass
{
    public string Name { get; set; }
    public string Description { get; set; }

    public bool ThirdMethod() { return true; }
}

For such class topology the following set of properties and methods should be available for rule assignment:

FirstLevelClass

To support recursive property resolution, I defined an auxilliary class RuleProperty:

public class RuleProperty
{
    public RuleProperty OwnerProperty { get; set; }
    public Type DeclaringType { get; set; }
    public Type Type { get { return this.PropertyInfo.PropertyType; } }
    public string Name { get { return this.PropertyInfo.Name; } }
    public PropertyInfo PropertyInfo { get; set; }

    public string FullName
    {
        get
        {
            string fullName = this.Name;
            var ownerProperty = this.OwnerProperty;
            while (ownerProperty != null)
            {
                fullName = ownerProperty.Name + "." + fullName;
                ownerProperty = ownerProperty.OwnerProperty;
            }
            return fullName;
        }
    }
}

Then populating property list can be implemented using recursive algorithm:

private List GetProperties(Type type, RuleProperty ownerProperty)
{
    var properties = new List();

    foreach (var property in from item in type.GetProperties() orderby item.Name select item)
    {
        var ruleProperty = new RuleProperty
        {
            DeclaringType = type,
            OwnerProperty = ownerProperty,
            PropertyInfo = property
        };
        if (property.PropertyType.GetProperties().Count() == 0 ||
            property.PropertyType.FullName.StartsWith("System."))
        {
            properties.Add(ruleProperty);
        }
        else if (!property.PropertyType.FullName.StartsWith("System."))
        {
            properties.AddRange(GetProperties(property.PropertyType, ruleProperty));
        }
    }

    return properties;
}

As you can see, I filtered properties based on system classes to avoid expansion of string properties that otherwise would have been treated as compound properties.

Instantiating rule object classes

This is something that is only needed to test rules in the editor – when working with rules using domain specific code you will typically just make a call to an object constructor: “myObject = new MyObject()”. But the rule editor does not have a convenience of knowledge of what objects it should create. It can only call Activator.CreateInstance, and in case of nested objects it will not be enough – it will need to traverse the whole object hierarchy, identify properties that requires instantiation of nested objects and call Activator.CreateInstance on them. I placed the code that does it in a method CreateObjectInstance:

public RuleObject CreateObjectInstance()
{
    RuleObject ruleObject = new RuleObject(this.Type);
    Dictionary ownerObjects = new Dictionary();

    for (int index = 0; index < this.Properties.Count; index++)
    {
        object propertyObject;
        if (this.Properties[index].OwnerProperty == null)
        {
            propertyObject = ruleObject.Instance;
        }
        else
        {
            if (!ownerObjects.ContainsKey(this.Properties[index].OwnerProperty))
            {
                object baseObject;
                if (this.Properties[index].OwnerProperty.OwnerProperty == null)
                {
                    baseObject = ruleObject.Instance;
                }
                else
                {
                    baseObject = ownerObjects[this.Properties[index].OwnerProperty.OwnerProperty];
                }
                propertyObject = this.Properties[index].OwnerProperty.PropertyInfo.GetValue(baseObject, null);
                if (propertyObject == null)
                {
                    propertyObject = Activator.CreateInstance(this.Properties[index].DeclaringType);
                }
                ownerObjects.Add(this.Properties[index].OwnerProperty, propertyObject);
                this.Properties[index].OwnerProperty.PropertyInfo.SetValue(baseObject, propertyObject, null);
            }
            else
            {
                propertyObject = ownerObjects[this.Properties[index].OwnerProperty];
            }
        }
        ruleObject.PropertyObjects.Add(this.Properties[index], propertyObject);
    }

    return ruleObject;
}

Assigning and retrieving property values

After all these preparations there is only one step left: manage property values. When testing rules, a user of the rule editor should be able assign values to a rule object, execute rules and display property values after rule execution. Code to get and set property values is quite simple:

public void SetPropertyValue(RuleObject ruleObject, int index, object value)
{
    object propertyValue;
    if (this.Properties[index].Type.IsEnum)
    {
        propertyValue = Enum.Parse(this.Properties[index].Type, value.ToString());
    }
    else
    {
        propertyValue = Convert.ChangeType(value, this.Properties[index].Type);
    }
    this.Properties[index].PropertyInfo.SetValue(ruleObject.PropertyObjects[this.Properties[index]], propertyValue, null);
}

public object GetPropertyValue(RuleObject ruleObject, int index)
{
    return this.Properties[index].PropertyInfo.GetValue(ruleObject.PropertyObjects[this.Properties[index]], null);
}

The rule editor is now completed and can be used to define and test rules on types from any .NET assembly. You can download the rule editor source code together with a few sample test classes (placed in a different assembly) and a few rules for them stored in text files.

Comments

Sudhanshu said:

Hi Vagif,

I completely agree with your thoughts on the unreadability and verbose nature of XAML.

Can you let me know if you faced any performance impact due to use of reflection?

# May 11, 2009 5:44 AM

Vagif Abilov said:

I haven't compared performance differences between CodeDOM and WYSIWYG versions. But I plan to look at peformance aspects of rule engine in general, because I can definitely feel the slowness of unit tests that use WF rule engine.

# May 11, 2009 5:44 PM

rolls said:

Hey Vagif

Nice article,This implemenatation should supercede microsofts one.....Simplicity is very difficult to achieve with software,you have passed with flying colours with this project.Respect to you sir.

Regards Rolls

# October 14, 2009 12:14 AM

Vagif Abilov said:

Hi Rolls,

Thank you for the kind words! Glad my little editor is useful for others.

Vagif

# October 14, 2009 6:19 AM

Vagif Abilov's blog on .NET said:

About a year ago I wrote a blog post where I described a simple WYSIWIG rule editor that can be used

# May 20, 2010 10:09 AM

Jones said:

Hello

I have had a look at your sample application on bitbucket, thank you for your contribution.

I would like to add a Generic list type to your SampleClasses.There is no Generic list in your samples

and i am having a problem When your code trys to Convert it's Type.  I added  public  List<Factor> Factors

to FirstLevelClass.

When i try to set the Propertyvalue.

   this.RuleType.SetPropertyValue(objectDetails, index, propertyValue);

   I keep getting coversion errors as this line    propertyValue = Convert.ChangeType(value, this.Properties[index].Type);  

    in function

       "public void SetPropertyValue(RuleObject ruleObject, int index, object value)"

Please can you advise a way to do this with your toolset.

Many Thanks

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace SampleClasses

{

   public class ClassWithoutDefaultPublicConstructor

   {

       private ClassWithoutDefaultPublicConstructor()

       {

       }

       public string Name { get; set; }

       public string Description { get; set; }

   }

   public class BaseClass

   {

       public string Name { get; set; }

   }

   public class DerivedClass : BaseClass

   {

       public string Description { get; set; }

   }

   public class FirstLevelClass

   {

       public string Name { get; set; }

       public string Description { get; set; }

       public SecondLevelClass SecondLevel { get; set; }

       public int IntValue { get; set; }

       public int? NullableIntValue { get; set; }

       public decimal DecimalValue { get; set; }

       public double DoubleValue { get; set; }

       public bool FirstMethod() { return true; }

        private  List<Factor> _Factors = new List<Factor>();

       public  List<Factor> Factors

       {

           get { return _Factors; }

           set { _Factors = value; }

       }

       public void AddFactor(Factor f)

       {

           Factors.Add(f);

       }

       public void RemoveFactor(Factor f)

       {

           try

           {

               int idx = Factors.IndexOf(f);

               Factors.RemoveAt(idx);

           }

           catch

           {

           }

       }

   }

   public class Factor

   {

       public double value1{ get; set; }

       public double value2{ get; set; }

   }

   public class SecondLevelClass

   {

       public string Name { get; set; }

       public string Description { get; set; }

       public ThirdLevelClass ThirdLevel { get; set; }

       public bool SecondMethod() { return true; }

   }

   public class ThirdLevelClass

   {

       public string Name { get; set; }

       public string Description { get; set; }

       public bool ThirdMethod() { return true; }

   }

   public class ClassWithEmbeddedClass

   {

       public class EmbeddedClass

       {

           public string Name { get; set; }

           public string Description { get; set; }

       }

       public string Name { get; set; }

       public string Description { get; set; }

       public string EmbeddedName { get { return Link.Name; } set { Link.Name = value; } }

       public string EmbeddedDescription { get { return Link.Description; } set { Link.Name = value; } }

       public EmbeddedClass Link { get; set; }

       public ClassWithEmbeddedClass()

       {

           this.Link = new EmbeddedClass();

       }

   }

}

# October 27, 2010 8:17 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 


Please add 3 and 6 and type the answer here: