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.
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
14 thoughts on “A quick and dirty Rules Engine using Windows Workflow (Part 2)”
Nice post Rob, highlighting how the simple the Rules Engine can be used outside the bulk of WF. As you mention the appplications of using this technology are immense 🙂
Excellent Article, I am currently trying to implement a rules engine from scratch, this approach looks like a way better solution. Looking forward to the next part. Keep it coming. 😉
This may be of some use if you don’t want to work with the bloated XML that the WF rules engine stores in it’s data.
The rules engine serializes the dom code for the parsed rules. Mine stores what is displayed in the GUI.
Rob,
I know this is years later, but you didn’t link to this article in Part 1 (as you promised!), I had to use the search built in to find part 2. Thanks for putting this together – looks very promising!
Hi Jon,
Looks like I added a link at the very bottom, but didn’t remove/edit the last part of Part 1 to match! Updated now with the new(er) canonical formatted link..
Thanks for the feedback!
R
I apologize if this is too far off topic, I was wondering if you have heard of a way to programmatically check in individual rules to TFS. We use a lot of rules, and sometimes it would be nice to allow a user to check in a complicated rule after it has been written. We are saving the rules to a SQL Server table, but scripting out the whole rules table to save to TFS takes up over 6 megs of space currently (because of the serialized XML). This is less than ideal for us when working with a constantly growing set of rules. I’ve tried googling to find someone doing something similar what we need but I’ve come up empty so far.
Hi Christopher,
It’s certainly do-able via the TFS API 🙂 I’d need to know a little more about your scenario though, feel free to shoot me an email rob.sanders@gmail.com
R
Hi guys,
Could anyone rump me up with this project? I’m pretty new to C# and would need more information about the kind of project I have to select (do I get the application window when I use WorkFlowConsoleApplication?!)
I’m starting from scratch and I was trying to use this example to adjust it further for my purposes and ultimately to obtain the XML files which contain the rules.
I’d appreciate any information you could provide!
Thanks,
Thomas
Hi!
You can pick pretty much any UI-based project (Windows Form, Web, Console Application) and then add a project reference to the WorkFlowProvider project. Then you should be able to reference the classes within.
/Rob
HI,
I am currently exploring the XF rule engine capabilities, but i have a problem, when i load ruleset from file the rule engine doesn’t execute, it doesn’t throw any exception, but no rule is executed. I am using your load method but it doesn’t work. And I can see thet the ruleset object is loaded correctly so i don’t know what’s the problem. anyone have any idea?
Thanks,
HI,
I am currently exploring the WF rule engine capabilities, but i have a problem, when i load ruleset from file the rule engine doesn’t execute, it doesn’t throw any exception, but no rule is executed. I am using your load method but it doesn’t work. And I can see thet the ruleset object is loaded correctly so i don’t know what’s the problem. anyone have any idea?
Thanks,
I’d recommend using a tool like .NET reflector/ILSpy to investigate the plumbing of the WF inside the framework. There might be changes in the newer versions of the framework itself which has changed the execution behaviour.
Hi Robert! You wrote:” 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.” Do you have plans to help us with concurrent collections. I want to keep there already processed ids, so other rules would not be applied to those ids. Any idea how to do it?
It’s probably a bit beyond me these days, I haven’t touched .NET in a while. If I do dust off the gloves, this would be an interesting project.
Back when I wrote this article, it was prior to 4.x and everyone was rolling their own thread pool implementations (prior to async/await).
It’d be interesting to combine the power of the WF with the advances in state machine tech in the newer frameworks.