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);
}
}
}

Here is a sample ruleset: (Replace [ and ] with the obvious since xml does not embed well)
        const string XmlRule = @”
[RuleSet.Text Name=’Text Ruleset’ Description=’Test Ruleset’ ChainingBehavior=’None’]
[Rule Active=’true’ Name=’R1′ Description=’Checks Name’ Priority=’1′ ReevaluationBehavior=’Never’ ]
[Condition]this.Name == null[/Condition]
[ThenActions]
[Action]this.ValidationMessages.Add(“”Name should not be empty””)[/Action]
[/ThenActions]
[/Rule]
[/RuleSet.Text]
“;

One thought on “A better WF Rule Serializer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s