A quick and dirty Rules Engine using Windows Workflow (Part 2)

Hi there and happy new year.  2011 promises to be quite an interesting year, and I hope that I can continue to contribute here at Sanders Technology.  To kick off the new year, I decided to revisit the Windows Workflow article I started late last year.

A little while ago I wrote a post at entitled “A quick and dirty Rules Engine using Windows Workflow (Part 1)” which has evidently been fairly popular.  Unfortunately, it seems that I forgot to follow it up with a part 2!  Now, welcoming in the new year, I’m putting together the second part.

Honestly though, folks, this could easily be a multi part mini project, because the uses of this Windows Workflow Foundation (WF) rules engine are immense!

I’ve managed to extend the scope of the code displayed in part 1 to include some dummy data items and I’ve crafted some more reusable and general purpose code (for example purposes), but you really ought to be able to see for yourselves how powerful and multi-purpose this really is.

I was going to write a quick and dirty WinForms UI, but I ended up ditching it in favour of a bunch of unit tests instead.  You really should be able to see the potential here, I don’t want to spoil the magic by adding an inept user interface.

Let’s take a look at the sample solution.  I’ve added some terribly (and perhaps insultingly) simple “objects” which, of course, you would substitute for your own DTOs/Entities/BusinessObjects.  It’s a basic class with some public properties, nothing terribly complex (it’s a demo after all).  You can see it uses an Enum just for fun on one of the properties.  I’ve also included a screenshot of the Solution structure – nothing too scary here.

imageimage

The Class View for the “BusinessObjects” | The Solution Structure

Basically the entire solution consists of two class libraries and a Unit Test project.  I’m trying to keep this very simple.  You could plug a WinForms UI or a website or a WCF Web Service Application underneath this very easily!

The main fun is in the “RuleManager” class, which is basically just a wrapper for the main WF workflow engine parts.  I’ve put in an extremely vanilla implementation which allows a few interesting parts of functionality.  I think if you use your imagination, you’ll be able to come up with some much more interesting ways to play with the options.

So why don’t we have a look at the RuleManager class?  It is defined to take a generic type, so you can work upon different source object types.

For the purpose of this post, we have just the one main object defined “Employee”.  It also doesn’t so anything to ensure the rules loaded are explicit for the data type – I’ll do an expanded implementation later to show how we can account for this.

My sincere apologies for the crappiness of the format of the posted code!  I’m having a bit of a fight with my copy of Live Writer and the plugins for inserting code snippets are not working very well with the site layout theme.  I’ll try and get it looking right.. soon.

#region Using Directives
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Workflow.Activities.Rules.Design;
using System.Workflow.Activities.Rules;
using System.Windows.Forms;
using System.Workflow.ComponentModel.Serialization;
using System.Xml;
using System.IO;
using BusinessObjects;
using System.Collections.ObjectModel;
#endregion

namespace WorkFlowProvider
{
    /// Implements a wrapper around the Windows Workflow Foundation Rules Engine
    /// A Data Object type to process
    public static class RulesManager<T> where T : new()
    {
        #region Rules Editor Support

        /// Launch the Rules Form to create a new rule
        public static RuleSet LaunchNewRulesDialog(string ruleName, string outputPath)
        {
            return LaunchRulesDialog(null, ruleName, outputPath);
        }

        /// Launch the Rules Editor with an existing rule (for editing),        
        /// or to create a new rule (pass NULL to create a new rule)
        /// The rule name (for the file name)
        /// The path to save rules to
        /// A rule (if one is saved/edited)
        public static RuleSet LaunchRulesDialog(RuleSet ruleSet, string ruleName, string outputPath)
        {
            // You could pass in an existing ruleset object for editing if you 

// wanted to, we're creating a new rule, so it's set to null RuleSetDialog ruleSetDialog = new RuleSetDialog(typeof(T), null, ruleSet); if (ruleSetDialog.ShowDialog() == DialogResult.OK) { // grab the ruleset ruleSet = ruleSetDialog.RuleSet; // We're going to serialize it to disk so it can be reloaded

WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer(); string fileName = String.Format("{0}.rules", ruleName); string fullName = Path.Combine(outputPath, fileName); if (File.Exists(fullName)) { File.Delete(fullName); //delete existing rule } using (XmlWriter rulesWriter = XmlWriter.Create(fullName)) { serializer.Serialize(rulesWriter, ruleSet); rulesWriter.Close(); } } return ruleSet; } #endregion #region Rule Processing /// Applies a set of rules to a specified data object public static T ProcessRules(T objectToProcess, ReadOnlyCollection rules) { RuleValidation validation = new RuleValidation(typeof(T), null); RuleExecution execution = new RuleExecution(validation, objectToProcess); foreach (RuleSet rule in rules) { rule.Execute(execution); } return objectToProcess; } /// Execute a single rule on a single data object public static T ProcessRule(T objectToProcess, RuleSet rule) { RuleValidation validation = new RuleValidation(typeof(T), null); RuleExecution execution = new RuleExecution(validation, objectToProcess); rule.Execute(execution); return objectToProcess; } #endregion #region Rules Management /// Loads a single rule given a path and file name public static RuleSet LoadRule(string rulesLocation, string fileName) { RuleSet ruleSet = null; // Deserialize from a .rules file. using (XmlTextReader rulesReader = new XmlTextReader(Path.Combine(rulesLocation, fileName))) { WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer(); ruleSet = (RuleSet)serializer.Deserialize(rulesReader); } return ruleSet; } /// Loads a set of rules from disk public static ReadOnlyCollection LoadRules(string rulesLocation) { RuleSet ruleSet = null; List rules = new List(); foreach (string fileName in Directory.GetFiles(rulesLocation, "*.rules")) { // Deserialize from a .rules file. using (XmlTextReader rulesReader = new XmlTextReader(fileName)) { WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer(); ruleSet = (RuleSet)serializer.Deserialize(rulesReader); rules.Add(ruleSet); rulesReader.Close(); } } return rules.AsReadOnly(); } #endregion } }

This one class pretty much gives you all you need to create, load and save rules.  It’s a bit basic at this point in time, I will try to create a more robust and tolerant class in subsequent posts on this topic.  For now though, I think it adequately demonstrates the sort of functionality which can be gleaned from the Rules Engine.

You can create or edit a rule by using the LaunchNewRulesDialog or LaunchRulesDialog methods with minimal user input.  I’ve written a very basic Unit Test which proves how efficient this can be, but I’m sure you’ll be able to have some fun with it.

Next up, there are some functions to load existing rule files from disk, the aptly named LoadRule and LoadRules methods.  They are pretty self explanatory, I don’t think we need to go into too much detail about the loading of rules files.

Finally, there are some functions which can be called to execute rules against data objects.  At this stage I’m supporting the execution of a single rule against a single data object, or a collection of rules against a single data object.  Obviously you could easily expand upon this.  You may wish to consider a multi-threaded approach, I may be persuaded to implement a more robust solution which allows for concurrent multiple item/multiple rule processing if you leave a comment for me.

Finally, here’s the Unit Test which allows you to create a new rule and apply it to the test data defined in the test:

[TestMethod]
public void CreateNewRule()
{
    Employee testEmployee = new Employee();    
    testEmployee.FirstName = "Joe";    
    testEmployee.Surname = "Smith";
    testEmployee.Location = StateEnum.ACT;
    testEmployee.Manager = null;
    testEmployee.DateHired = DateTime.Now.AddYears(-1);
    testEmployee.EmployeeNumber = 99;

    string ruleName = String.Format("{0}UnitTestRule", DateTime.Now.Millisecond);
    string path = Assembly.GetExecutingAssembly().Location.Replace(Assembly.GetExecutingAssembly().ManifestModule.Name, String.Empty);

    RuleSet newRule = RulesManager.LaunchNewRulesDialog(ruleName, path);
    testEmployee = RulesManager.ProcessRule(testEmployee, newRule);

    Trace.WriteLine(testEmployee.FirstName);
    Trace.WriteLine(testEmployee.Surname);
    Trace.WriteLine(testEmployee.DateHired);
}

So, in this post we’ve had a look at a very basic solution structure which demonstrates a reusable rules design.  At the moment it is as close to useless as a demo usually starts off looking like.  I’m only getting started, once you are familiar with the ‘RulesManager’ wrapper concept, we’ll be ready to expand upon it significantly.

This is part 2 of a multi-part series.  I’ll be expanding upon the concepts shown here in subsequent posts.

Check back soon!

Solution Files


Leave a comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

14 thoughts on “A quick and dirty Rules Engine using Windows Workflow (Part 2)”