Discovering System.Linq.Expressions
I’m officially a fan: System.Linq.Expressions is one of the coolest namespaces.
I’ve been working on a simple workflow engine as an example codebase for exploring different aspects of object oriented programming (inheritance, encapsulation, polymorphism, design patterns, etc.) for training purposes.
As a side note, I’ve found that it’s actually very difficult to describe good object oriented code; I can just kind of feel it when I’m writing it and I know it when I see it…but it’s really, really hard to describe. I was asked by an associate consultant why it mattered. Why bother with good object oriented design? For me (at least), more than anything, it’s about organization of code, readability, maintainability, and usability. Good object oriented code makes it easy to think about the models and how the interactions between the different moving parts are executed. But that’s a bigger topic for a different post.
Back on topic 😀 The basic design scenario that I was trying to solve in this case is that the simple workflow engine (SWE) would have the capability of hydrating a workflow template instance from an XML definition file (much like WF does with .xoml files, but on a much more basic level, of course). I thought this would be a good exercise for teaching purposes as it would cover various aspects of reflection. Somewhere along the line, inspired by a comment by Richard Deeming (see bullet #4) on Rick Strahl’s DataContextFactory implementation, I decided to see if I could do it using expression trees instead.
Here is a sample XML template definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span style="font-family: Lucida Console;"><span style="color: #0000ff;"><</span><span style="color: #808000;">WorkflowTemplate</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Actions</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Action </span><span style="color: #800080;">Type</span><span style="color: #0000ff;">=</span><span style="color: #ff00ff;">"SimpleWorkflowEngine.Extensions.CustomActions.EmailAction, </span> <span style="color: #ff00ff;"> SimpleWorkflowEngine.Extensions"</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Parameters</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Parameter </span><span style="color: #800080;">Name</span><span style="color: #0000ff;">=</span><span style="color: #ff00ff;">"ContinueOnError"</span><span style="color: #0000ff;">></span><span style="color: #000000;">true</span><span style="color: #0000ff;"></</span><span style="color: #808000;">Parameter</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Parameter </span><span style="color: #800080;">Name</span><span style="color: #0000ff;">=</span><span style="color: #ff00ff;">"FriendlyName"</span><span style="color: #0000ff;">></span><span style="color: #000000;">Send Email</span><span style="color: #0000ff;"></</span><span style="color: #808000;">Parameter</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Parameter </span><span style="color: #800080;">Name</span><span style="color: #0000ff;">=</span><span style="color: #ff00ff;">"RecipientAddress"</span><span style="color: #0000ff;">></span><span style="color: #000000;">cchen@domain.com</span><span style="color: #0000ff;"></</span><span style="color: #808000;">Parameter</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Parameter </span><span style="color: #800080;">Name</span><span style="color: #0000ff;">=</span><span style="color: #ff00ff;">"Message"</span><span style="color: #0000ff;">></span><span style="color: #000000;">Hello, World! From SimpleWorkflowEngine!</span><span style="color: #0000ff;"></</span><span style="color: #808000;">Parameter</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"><</span><span style="color: #808000;">Parameter </span><span style="color: #800080;">Name</span><span style="color: #0000ff;">=</span><span style="color: #ff00ff;">"Subject"</span><span style="color: #0000ff;">></span><span style="color: #000000;">Test Email</span><span style="color: #0000ff;"></</span><span style="color: #808000;">Parameter</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"></</span><span style="color: #808000;">Parameters</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"></</span><span style="color: #808000;">Action</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"></</span><span style="color: #808000;">Actions</span><span style="color: #0000ff;">></span> <span style="color: #0000ff;"></</span><span style="color: #808000;">WorkflowTemplate</span><span style="color: #0000ff;">></span></span> |
Here are the classes which this XML deserializes to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
<span style="font-family: Lucida Console;"><span style="color: #0000ff;">using </span><span style="color: #008080;">System</span><span style="color: #000000;">;</span> <span style="color: #0000ff;">using </span><span style="color: #008080;">System</span><span style="color: #000000;">.</span><span style="color: #008080;">Collections</span><span style="color: #000000;">.</span><span style="color: #008080;">ObjectModel</span><span style="color: #000000;">;</span> <span style="color: #0000ff;">using </span><span style="color: #008080;">System</span><span style="color: #000000;">.</span><span style="color: #008080;">Xml</span><span style="color: #000000;">.</span><span style="color: #008080;">Serialization</span><span style="color: #000000;">;</span></span> <span style="font-family: Lucida Console;"><span style="color: #0000ff;">namespace </span><span style="color: #000000;">SimpleWorkflowEngine.Core.</span><span style="color: #008080;">Common</span> <span style="color: #000000;">{</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Models a workflow template.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #000000;">[Serializable]</span> <span style="color: #000000;">[XmlRoot(</span><span style="color: #ff00ff;">"WorkflowTemplate"</span><span style="color: #000000;">)]</span> <span style="color: #0000ff;">public class </span><span style="color: #000000;">WorkflowTemplate</span> <span style="color: #000000;">{</span> <span style="color: #0000ff;">private </span><span style="color: #808000;">Collection</span><span style="color: #000000;"><WorkflowTemplateAction> _actions;</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the actions for this workflow template.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The actions.</value></span> <span style="color: #000000;">[XmlArray(</span><span style="color: #ff00ff;">"Actions"</span><span style="color: #000000;">), XmlArrayItem(</span><span style="color: #ff00ff;">"Action"</span><span style="color: #000000;">, </span> <span style="color: #0000ff;">typeof</span><span style="color: #000000;">(WorkflowTemplateAction))]</span> <span style="color: #0000ff;">public </span><span style="color: #808000;">Collection</span><span style="color: #000000;"><WorkflowTemplateAction> Actions</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_actions; }</span> <span style="color: #000000;">set { _actions = value; }</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Represents an action in the workflow.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #000000;">[Serializable]</span> <span style="color: #0000ff;">public class </span><span style="color: #000000;">WorkflowTemplateAction</span> <span style="color: #000000;">{</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_typeName;</span> <span style="color: #0000ff;">private </span><span style="color: #808000;">Collection</span><span style="color: #000000;"><WorkflowTemplateParameter> _parameters;</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the type name of the concrete </span> <span style="color: #008000;">/// <see cref="WorkflowAction"/> class.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value></span> <span style="color: #008000;">/// The type name of the concrete <see cref="WorkflowAction"/> class.</span> <span style="color: #008000;">/// </value></span> <span style="color: #000000;">[</span><span style="color: #808000;">XmlAttribute</span><span style="color: #000000;">(</span><span style="color: #ff00ff;">"Type"</span><span style="color: #000000;">)]</span> <span style="color: #0000ff;">public string </span><span style="color: #000000;">TypeName</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_typeName; }</span> <span style="color: #000000;">set { _typeName = value; }</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the parameters which map to property values on </span> <span style="color: #008000;">/// the action.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The parameters.</value></span> <span style="color: #000000;">[XmlArray(</span><span style="color: #ff00ff;">"Parameters"</span><span style="color: #000000;">), XmlArrayItem(</span><span style="color: #ff00ff;">"Parameter"</span><span style="color: #000000;">, </span> <span style="color: #0000ff;">typeof</span><span style="color: #000000;">(WorkflowTemplateParameter))]</span> <span style="color: #0000ff;">public </span><span style="color: #808000;">Collection</span><span style="color: #000000;"><WorkflowTemplateParameter> Parameters</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_parameters; }</span> <span style="color: #000000;">set { _parameters = value; }</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Represents a property value on the <see cref="WorkflowAction"/>.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #000000;">[Serializable]</span> <span style="color: #0000ff;">public class </span><span style="color: #000000;">WorkflowTemplateParameter</span> <span style="color: #000000;">{</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_name;</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_value;</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the name. It must match the name of the property.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The name of the property.</value></span> <span style="color: #000000;">[</span><span style="color: #808000;">XmlAttribute</span><span style="color: #000000;">(</span><span style="color: #ff00ff;">"Name"</span><span style="color: #000000;">)]</span> <span style="color: #0000ff;">public string </span><span style="color: #000000;">Name</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_name; }</span> <span style="color: #000000;">set { _name = value; }</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the value.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The value.</value></span> <span style="color: #000000;">[</span><span style="color: #808000;">XmlText</span><span style="color: #000000;">(</span><span style="color: #0000ff;">typeof</span><span style="color: #000000;">(</span><span style="color: #0000ff;">string</span><span style="color: #000000;">))]</span> <span style="color: #0000ff;">public string </span><span style="color: #000000;">Value</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_value; }</span> <span style="color: #000000;">set { _value = value; }</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span></span> |
I’ve done something similar in a workflow engine that I put together for Zorch Software before the days of WF. Back then I used reflection to assemble a workflow instance from an XML template as well. (In that case, the engine supported state machine workflows; for this training sample, I thought that just sequential was good enough)
Here is the invocation code to get a new instance of a workflow from a workflow template definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<span style="font-family: Lucida Console;"><span style="color: #0000ff;">using </span><span style="color: #008080;">System</span><span style="color: #000000;">;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">MbUnit.Framework;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">SimpleWorkflowEngine.Core.</span><span style="color: #008080;">Common</span><span style="color: #000000;">;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">SimpleWorkflowEngine.Core.Contracts;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">SimpleWorkflowEngine.Core.</span><span style="color: #008080;">Runtime</span><span style="color: #000000;">;</span></span> <span style="font-family: Lucida Console;"><span style="color: #0000ff;">namespace </span><span style="color: #000000;">SimpleWorkflowEngine.Tests.Fixtures</span> <span style="color: #000000;">{</span> <span style="color: #000000;">[TestFixture]</span> <span style="color: #0000ff;">public class </span><span style="color: #000000;">WorkflowFactoryTests</span> <span style="color: #000000;">{</span> <span style="color: #000000;">[Test]</span> <span style="color: #0000ff;">public void </span><span style="color: #000000;">TestCreateInstance()</span> <span style="color: #000000;">{</span> <span style="color: #000000;">IWorkflowTemplateProvider provider =</span> <span style="color: #808000;">ApplicationContext</span><span style="color: #000000;">.Instance.Resolve<IWorkflowTemplateProvider>();</span></span> <span style="font-family: Lucida Console;"> <span style="color: #000000;">WorkflowTemplate template =</span> <span style="color: #000000;">provider.FromTemplateId(</span> <span style="color: #0000ff;">new </span><span style="color: #000000;">Guid(</span><span style="color: #ff00ff;">"1FE5526C-D0E7-4c58-8644-3B3144AEED0E"</span><span style="color: #000000;">));</span></span> <span style="font-family: Lucida Console;"> <span style="color: #000000;">Workflow instance = WorkflowFactory.Create(template);</span></span> <span style="font-family: Lucida Console;"> <span style="color: #000000;">Assert.AreEqual(</span><span style="color: #800080;">1</span><span style="color: #000000;">, instance.Actions.Count);</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span></span> |
The basics are that the factory has to create an instance by iterating through each WorkflowTemplateAction and creating a concrete WorkflowAction instance and set property values using the WorkflowTemplateParameter associated with the WorkflowTemplateAction.
Here is the implementation of the factory method (cool stuff in bold):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using SimpleWorkflowEngine.Core.Common; namespace SimpleWorkflowEngine.Core.Runtime { public static class WorkflowFactory { private static string _nullPropertyError = "The parameter \"{0}\" did not match a property on the class \"{1}\"."; public static Workflow Create(WorkflowTemplate template) { Workflow instance = new Workflow(); foreach(WorkflowTemplateAction templateAction in template.Actions) { Type actionType = Type.GetType(templateAction.TypeName); List < MemberBinding > bindings = new List < MemberBinding > (); foreach(WorkflowTemplateParameter parameter in templateAction.Parameters) { BindingFlags flags = BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public; PropertyInfo property = actionType.GetProperty(parameter.Name, flags); if (property == null) { throw new NullReferenceException(string.Format(_nullPropertyError, parameter.Name, actionType.Name)); } object value = Convert.ChangeType(parameter.Value, property.PropertyType); MemberAssignment binding = Expression.Bind(property, Expression.Constant(value, property.PropertyType)); bindings.Add(binding); } MemberInitExpression init = Expression.MemberInit(Expression.New(actionType), bindings); WorkflowAction action = Expression.Lambda < Func < WorkflowAction >> (init).Compile().Invoke(); instance.Actions.Add(action); } return instance; } } } |
The outer for loop iterates the actions to create and the inner for loop collects the properties to set on the action. Object creation and initializaiton is done in one shot using Expression.Lambda.
For reference, here is the target class for the XML in the sample above:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
<span style="font-family: Lucida Console;"><span style="color: #0000ff;">using </span><span style="color: #008080;">System</span><span style="color: #000000;">;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">SimpleWorkflowEngine.Core.Attributes;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">SimpleWorkflowEngine.Core.</span><span style="color: #008080;">Common</span><span style="color: #000000;">;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">SimpleWorkflowEngine.Core.Constants;</span> <span style="color: #0000ff;">using </span><span style="color: #000000;">SimpleWorkflowEngine.Extensions.Components;</span> <span style="color: #0000ff;">namespace </span><span style="color: #000000;">SimpleWorkflowEngine.Extensions.CustomActions</span> <span style="color: #000000;">{</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// A worflow action which sends an email to a recipient.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #0000ff;">public class </span><span style="color: #000000;">EmailAction : WorkflowAction</span> <span style="color: #000000;">{</span> <span style="color: #0000ff;">private </span><span style="color: #000000;">MailClientService _mailClientService;</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_message;</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_recipientAddress;</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_subject;</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Sets the mail client service.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The mail client service.</value></span> <span style="color: #0000ff;">public </span><span style="color: #000000;">MailClientService MailClientService</span> <span style="color: #000000;">{</span> <span style="color: #000000;">set { _mailClientService = value; }</span> <span style="color: #000000;">}</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the recipient address.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The recipient address.</value></span> <span style="color: #000000;">[</span><span style="color: #808000;">Parameter</span><span style="color: #000000;">(Usage.IsRequired, </span><span style="color: #ff00ff;">"Recipient e-mail"</span><span style="color: #000000;">)]</span> <span style="color: #0000ff;">public string </span><span style="color: #000000;">RecipientAddress</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_recipientAddress; }</span> <span style="color: #000000;">set { _recipientAddress = value; }</span> <span style="color: #000000;">}</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the message.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The message.</value></span> <span style="color: #000000;">[</span><span style="color: #808000;">Parameter</span><span style="color: #000000;">(Usage.IsRequired, </span><span style="color: #ff00ff;">"Message"</span><span style="color: #000000;">)]</span> <span style="color: #0000ff;">public string </span><span style="color: #808000;">Message</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_message; }</span> <span style="color: #000000;">set { _message = value; }</span> <span style="color: #000000;">}</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets or sets the subject.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value>The subject.</value></span> <span style="color: #000000;">[</span><span style="color: #808000;">Parameter</span><span style="color: #000000;">(Usage.IsRequired, </span><span style="color: #ff00ff;">"Subject"</span><span style="color: #000000;">)]</span> <span style="color: #0000ff;">public string </span><span style="color: #000000;">Subject</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_subject; }</span> <span style="color: #000000;">set { _subject = value; }</span> <span style="color: #000000;">}</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Gets a value indicating whether this instance </span> <span style="color: #008000;">/// [requires input].</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <value></span> <span style="color: #008000;">/// <c>true</c> if this instance [requires input]; </span> <span style="color: #008000;">/// otherwise, <c>false</c>.</span> <span style="color: #008000;">/// </value></span> <span style="color: #0000ff;">public override bool </span><span style="color: #000000;">RequiresInput</span> <span style="color: #000000;">{</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return false</span><span style="color: #000000;">; }</span> <span style="color: #000000;">}</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Inheriting member should override this method to implement </span> <span style="color: #008000;">/// custom execution logic.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <param name="context">The context.</param></span> <span style="color: #0000ff;">protected override void </span><span style="color: #000000;">OnExecute(</span><span style="color: #808000;">ExecutionContext </span><span style="color: #000000;">context)</span> <span style="color: #000000;">{</span> <span style="color: #0000ff;">if </span><span style="color: #000000;">(_mailClientService == </span><span style="color: #0000ff;">null</span><span style="color: #000000;">)</span> <span style="color: #000000;">{</span> <span style="color: #0000ff;">throw new </span><span style="color: #808000;">NullReferenceException</span><span style="color: #000000;">(</span> <span style="color: #ff00ff;">"No mail service client is configured for this runtime."</span><span style="color: #000000;">);</span> <span style="color: #000000;">}</span> <span style="color: #000000;">_mailClientService.SendMail(RecipientAddress, Subject, </span><span style="color: #808000;">Message</span><span style="color: #000000;">);</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span></span> |
Admittedly, I didn’t expect this to work…I had a hell of a time figuring out how to create the expression to set the property values.
A fascinating approach.
Good read over the morning Ice Coffee.
(Found it via Dew Drop)