Monday, March 23, 2009

A better WF Rule Serializer

The Serialaizer supplied with the Microsoft rules engine stores the CodeDom expressions into the XML.
This is less than readable (can you imaging performing differences on this in version control - and you do keep your rules in version control don’t you?) and very wordy (100 lines per line of rule text).

Here is a greatly improved version.  It stores the rule in a format as close to that the user entered.  It parses the code at serialization time.  It uses the same code as the rule engine to perform the parsing, requiring reflection to get at the code…

=== TextRuleSetSerializer ===

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Workflow.Activities.Rules;
using System.Xml;

namespace PerfectStorm.Rules
{
    /// <summary>
    /// This is a far more compact serializer than the one supplied with the WF rules engine.
    /// </summary>
    public class TextRuleSetSerializer
    {
        /// <summary>
        /// This is a proxy to the internal Parser class in the System.Workflow.Activities assembly.
        /// </summary>
        private class ParserProxy
        {
            private object _parser;
            private Type _parserType;

            /// <summary>
            ///
            /// </summary>
            /// <param name=”factType”></param>
            public ParserProxy(Type factType)
            {
                //This is a bit dangerous - it may require a rewrite with each new version of the framework.
                Assembly a = Assembly.Load(“System.Workflow.Activities, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35″);
                _parserType = a.GetType(“System.Workflow.Activities.Rules.Parser”);
                Type[] constructorParam = { typeof(RuleValidation) };
                RuleValidation validation = new RuleValidation(factType, null);
                object[] callParams = { validation };
                ConstructorInfo ci = _parserType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,
                    null, constructorParam, null);
                _parser = ci.Invoke(callParams);
            }

            /// <summary>
            ///
            /// </summary>
            /// <param name=”fragment”></param>
            /// <returns></returns>
            public RuleExpressionCondition ParseCondition(string fragment)
            {
                object[] parserParameters = { fragment };
                return (RuleExpressionCondition)_parserType.InvokeMember(“ParseCondition”,
                    BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
                    null, _parser, parserParameters);
            }

            /// <summary>
            ///
            /// </summary>
            /// <param name=”statements”></param>
            /// <returns></returns>
            public List<RuleAction> ParseStatementList(string statements)
            {
                object[] parserParameters = { statements };
                return (List<RuleAction>)_parserType.InvokeMember(“ParseStatementList”,
                    BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
                    null, _parser, parserParameters);
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name=”writer”></param>
        /// <param name=”rs”></param>
        public void Serialize(XmlWriter writer, RuleSet rs)
        {
            writer.WriteStartDocument();
            writer.WriteStartElement(“RuleSet.Text”);
            writer.WriteAttributeString(“Name”, rs.Name);
            writer.WriteAttributeString(“Description”, rs.Description);
            writer.WriteAttributeString(“ChainingBehavior”, rs.ChainingBehavior.ToString());

            foreach (var rule in rs.Rules)
            {
                writer.WriteStartElement(“Rule”);
                writer.WriteAttributeString(“Active”, rule.Active.ToString());
                writer.WriteAttributeString(“Description”, rule.Description);
                writer.WriteAttributeString(“Name”, rule.Name);
                writer.WriteAttributeString(“Priority”, rule.Priority.ToString());
                writer.WriteAttributeString(“ReevaluationBehavior”, rule.ReevaluationBehavior.ToString());

                writer.WriteStartElement(“Condition”);
                writer.WriteValue(rule.Condition.ToString());
                writer.WriteEndElement();

                writer.WriteStartElement(“ThenActions”);

                foreach (var action in rule.ThenActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();

                writer.WriteStartElement(“ElseActions”);

                foreach (var action in rule.ElseActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
                writer.WriteEndElement();
            }

            writer.Flush();
        }

        /// <summary>
        ///
        /// </summary>
        /// <typeparam name=”T”></typeparam>
        /// <param name=”value”></param>
        /// <returns></returns>
        private T GetEnumValue<T>(string value)
        {
            if (!typeof(T).IsSubclassOf(typeof(Enum)))
                throw new Exception(“Must be an Enum”);

            int[] allValues = (int[])Enum.GetValues(typeof(T));

            foreach (int i in allValues)
            {
                T newValue = (T)Enum.ToObject(typeof(T), i);
                if (newValue.ToString() == value)
                {
                    return newValue;
                }
            }
            throw new Exception(value + ” is not of Type ” + typeof(T).Name);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name=”factType”></param>
        /// <param name=”reader”></param>
        /// <returns></returns>
        public RuleSet Deserialize(Type factType, XmlReader reader)
        {
            RuleSet result = new RuleSet();

            ParserProxy parser = new ParserProxy(factType);

            //string allXml = reader.ReadOuterXml();
            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(reader);
            result.Name = xDoc.SelectSingleNode(“//RuleSet.Text/@Name”).InnerText;
            result.Description = xDoc.SelectSingleNode(“//RuleSet.Text/@Description”).InnerText;
            result.ChainingBehavior = GetEnumValue<RuleChainingBehavior>(xDoc.SelectSingleNode(“//RuleSet.Text/@ChainingBehavior”).InnerText);

            foreach (XmlNode ruleNode in xDoc.SelectNodes(“//Rule”))
            {
                Rule rule = new Rule();
                rule.Active = ruleNode.SelectSingleNode(“@Active”).InnerText == “true”;
                rule.Description = ruleNode.SelectSingleNode(“@Description”).InnerText;
                rule.Name = ruleNode.SelectSingleNode(“@Name”).InnerText;
                rule.Priority = int.Parse(ruleNode.SelectSingleNode(“@Priority”).InnerText);
                rule.ReevaluationBehavior = GetEnumValue<RuleReevaluationBehavior>(ruleNode.SelectSingleNode(“@ReevaluationBehavior”).InnerText);

                // I have problem identifying rule condition…
                rule.Condition = parser.ParseCondition(ruleNode.SelectSingleNode(“Condition”).InnerText);

                foreach (XmlNode thenNode in ruleNode.SelectNodes(“ThenActions/Action”))
                {
                    List<RuleAction> actions = parser.ParseStatementList(thenNode.InnerText);
                    foreach (RuleAction action in actions)
                    {
                        rule.ThenActions.Add(action);
                    }
                }

                foreach (XmlNode elseNode in ruleNode.SelectNodes(“ThenActions”))
                {
                    List<RuleAction> actions = parser.ParseStatementList(elseNode.InnerText);
                    foreach (RuleAction action in actions)
                    {
                        rule.ElseActions.Add(action);
                    }
                }

                result.Rules.Add(rule);
            }

            return result;
        }
    }
}

=== Program ===

using System;
using System.Workflow.Activities.Rules;
using System.Workflow.Activities.Rules.Design;
using System.Xml;

namespace PerfectStorm.Rules
{
    public class Order
    {
        private string _comment;
        public string Comment { get { return _comment; } set { _comment = value; } }
        public void setComment(string s) {this.Comment = s;}
    }
   
    class Program
    {
        // My key point here is that the workflow serializer creates illegible rules.
        // I want to write something that stores the rules in something closer to the native form.
        static void Main(string[] args)
        {

            // Create a RuleSet that waorks with Orders (just another .net Object)

            RuleSetDialog ruleSetDialog = new RuleSetDialog(typeof(Order), null, null);

            // Show the RuleSet Editor
            ruleSetDialog.ShowDialog();
            // Get the RuleSet after editing

            RuleSet ruleSet = ruleSetDialog.RuleSet;

            Order o = new Order();
            RuleValidation rv = new RuleValidation(typeof(Order),null);
            ruleSet.Validate(rv);
            foreach (var err in rv.Errors)
            {
                Console.WriteLine(err.ErrorText);
            }
            ruleSet.Name = “Test”;

           // Console.WriteLine(Pickler.Pickle<RuleSet>(ruleSet));

           

            RuleEngine re = new RuleEngine(ruleSet, typeof(Order));
            re.Execute(o);
            Console.WriteLine(o.Comment);
 
            TextRuleSetSerializer serializer = new TextRuleSetSerializer();

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            XmlWriter rulesWriter = XmlWriter.Create(“test.rules”, settings);
           

            serializer.Serialize(rulesWriter, ruleSet);
            rulesWriter.Close();

            // Deserialize from a .rules file.

            XmlTextReader rulesReader = new XmlTextReader(“test.rules”);

            TextRuleSetSerializer serializer2 = new TextRuleSetSerializer();

            ruleSet = (RuleSet)serializer2.Deserialize(typeof(Order), rulesReader);
            o.Comment = “”;
            re.Execute(o);
            Console.WriteLine(o.Comment);
        }
    }
}

Posted by chriseyre2000 at 20:27:56 | Permalink | Comments (1) »

Saturday, March 21, 2009

Simpler WF Rules Serializer

I have been looking at the Windows WF Rules serialization.  The default version creates 100 lines of illegible xml for each line of entered rules.  My version creates 10 lines per line of rule.  I am tempted to incorporate this into the stand-alone ruleset editor. My version would initially store the readable version alongside the compiled version.

Here is a version that creates cleaner XML:

    public class TextRuleSetSerializer
    {
        public void Serialize(XmlWriter writer, RuleSet rs)
        {
            writer.WriteStartDocument();
            writer.WriteStartElement(“RuleSet.Text”);
            writer.WriteAttributeString(“Name”, rs.Name);
            writer.WriteAttributeString(“Description”, rs.Description);
            writer.WriteAttributeString(“ChainingBehaviour”, rs.ChainingBehavior.ToString());
                       
            foreach (var rule in rs.Rules)
            {
                writer.WriteStartElement(“Rule”);
                writer.WriteAttributeString(“Active”, rule.Active.ToString());
                writer.WriteAttributeString(“Description”, rule.Description);
                writer.WriteAttributeString(“Name”, rule.Name);
                writer.WriteAttributeString(“Priority”, rule.Priority.ToString());
                writer.WriteAttributeString(“ReevaluationBehavior”, rule.ReevaluationBehavior.ToString());

                writer.WriteStartElement(“Condition”);
                writer.WriteAttributeString(“ConditionType”, rule.Condition.GetType().FullName);
                writer.WriteAttributeString(“ConditionAssembly”, rule.Condition.GetType().AssemblyQualifiedName);
                writer.WriteValue(rule.Condition.ToString());
                writer.WriteEndElement();

                writer.WriteStartElement(“ThenActions”);
               
                foreach(var action in rule.ThenActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteAttributeString(“ActionType”, action.GetType().FullName);
                    writer.WriteAttributeString(“ActionAssembly”, action.GetType().AssemblyQualifiedName);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }
               
                writer.WriteEndElement();
               
                writer.WriteStartElement(“ElseActions”);

                foreach (var action in rule.ElseActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteAttributeString(“ActionType”, action.GetType().FullName);
                    writer.WriteAttributeString(“ActionAssembly”, action.GetType().AssemblyQualifiedName);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }
               
                writer.WriteEndElement();
                writer.WriteEndElement();
            }
     
            writer.Flush();
        }
}

I am still working on the deserialization version.  This is more difficult since the code that is used in the RuleSetDialog to compile the entered text into CodeDOM is an internal class.  This leaves me with two options: 

  1. Use reflector to create a non-internal version of the same code.
  2. Puppeteer the RuleSetDialog to perform the compilation for me.
Posted by chriseyre2000 at 22:12:45 | Permalink | No Comments »

Thursday, January 29, 2009

Use the wf editior on you own objects

Here is a great article on using the wf editor on your own objects.
Posted by chriseyre2000 at 20:39:50 | Permalink | No Comments »

Useful MSDN documentation on WF

Here is a useful example of MSDN documentation on workflow.

The example could do with some more narrative but at least it shows more thought than most of the api (example).  The example gives no more than you can get from the name.

Posted by chriseyre2000 at 20:16:06 | Permalink | No Comments »

WF Rulesets without Workflow

Here is a great article on using workflow rules for validation.

I am little concerned at how verbose and unreadable the rules file is…

Posted by chriseyre2000 at 19:27:38 | Permalink | No Comments »

Sunday, January 18, 2009

Useful Articlles on Workflow

Here is a set of useful articles on workflow.
One of the important ones is a demo of using the  rules engine outside of the workflow.  Given the existence of the stand alone ruleset editor the sky is the limit.
Posted by chriseyre2000 at 06:46:21 | Permalink | No Comments »

Sunday, January 11, 2009

Microsoft WF contains a rule engine

Here are the details.

Here is an external ruleset editor for it.

This does seem simplistic, but does work.

Posted by chriseyre2000 at 08:11:48 | Permalink | No Comments »

Sunday, April 20, 2008

Introduction to workflow

This is a great introductory article to windows workflow.

Here is a series of articles that explain the hosting of the workflow designer:

Part 1

Part 2

Part 3

Part 4

Part 5

Part 6

Saving

Summary

Posted by chriseyre2000 at 06:42:18 | Permalink | No Comments »

Tuesday, April 15, 2008

Boo and WF

This is based upon an example that has been floating around the blogs.
I have removed the Python accent from it (hint you don’t need to use self in a Boo class):

The following is wf.boo

import System.Workflow.Activities
import System.Workflow.Runtime
import System
class MyWorkflow(SequentialWorkflowActivity):
_codeActivity as CodeActivity
def constructor():
super()
_codeActivity = CodeActivity()
_codeActivity.ExecuteCode += SayHello
_codeActivity.Name = “Hello”
Activities.Add(_codeActivity)
def SayHello(sender, args):
print “Hello”

def Started(sender as object, args as EventArgs):
print “Startedn”
def Completed(sender as object, args as EventArgs):
print “Completed”

tf = MyWorkflow()
rt = WorkflowRuntime()
rt.WorkflowStarted += Started
rt.WorkflowCompleted += Completed
type = tf.GetType()
instance = rt.CreateWorkflow(type)
instance.Start()
Console.ReadKey()

The following is default.build:

<?xml version=”1.0″ ?>

<project name=”wpfdemo” default=”build”>
<property name=”boo.dir” value=”C:/boo/bin” />
<target name=”build” depends=”wpfdemo” />
    <target name=”wpfdemo”>
        <loadtasks assembly=”${boo.dir}/Boo.NAnt.Tasks.dll” />
            <booc output=”wf.exe” target=”exe”>
                <sources>
                    <include name=”wf.boo” />
                </sources>
            </booc>
    </target>
</project>

Posted by chriseyre2000 at 19:55:16 | Permalink | No Comments »