<CharlieDigital/> Programming, Politics, and uhh…pineapples

12Jun/09Off

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:

<WorkflowTemplate>
    <Actions>
        <Action Type="SimpleWorkflowEngine.Extensions.CustomActions.EmailAction, 
            SimpleWorkflowEngine.Extensions">
            <Parameters>
                <Parameter Name="ContinueOnError">true</Parameter>
                <Parameter Name="FriendlyName">Send Email</Parameter>
                <Parameter Name="RecipientAddress">cchen@domain.com</Parameter>
                <Parameter Name="Message">Hello, World! From SimpleWorkflowEngine!</Parameter>
                <Parameter Name="Subject">Test Email</Parameter>
            </Parameters>
        </Action>
    </Actions>
</WorkflowTemplate>

Here are the classes which this XML deserializes to:

using System;
using System.Collections.ObjectModel;
using System.Xml.Serialization;
 
namespace SimpleWorkflowEngine.Core.Common
{
    /// <summary>
    /// Models a workflow template.
    /// </summary>
    [Serializable]
    [XmlRoot("WorkflowTemplate")]
    public class WorkflowTemplate
    {
        private Collection<WorkflowTemplateAction> _actions;
        /// <summary>
        /// Gets or sets the actions for this workflow template.
        /// </summary>
        /// <value>The actions.</value>
        [XmlArray("Actions"), XmlArrayItem("Action", 
            typeof(WorkflowTemplateAction))]
        public Collection<WorkflowTemplateAction> Actions
        {
            get { return _actions; }
            set { _actions = value; }
        }
    }
    /// <summary>
    /// Represents an action in the workflow.
    /// </summary>
    [Serializable]
    public class WorkflowTemplateAction
    {
        private string _typeName;
        private Collection<WorkflowTemplateParameter> _parameters;
        /// <summary>
        /// Gets or sets the type name of the concrete 
        /// <see cref="WorkflowAction"/> class.
        /// </summary>
        /// <value>
        /// The type name of the concrete <see cref="WorkflowAction"/> class.
        /// </value>
        [XmlAttribute("Type")]
        public string TypeName
        {
            get { return _typeName; }
            set { _typeName = value; }
        }
        /// <summary>
        /// Gets or sets the parameters which map to property values on 
        /// the action.
        /// </summary>
        /// <value>The parameters.</value>
        [XmlArray("Parameters"), XmlArrayItem("Parameter", 
            typeof(WorkflowTemplateParameter))]
        public Collection<WorkflowTemplateParameter> Parameters
        {
            get { return _parameters; }
            set { _parameters = value; }
        }
    }
    /// <summary>
    /// Represents a property value on the <see cref="WorkflowAction"/>.
    /// </summary>
    [Serializable]
    public class WorkflowTemplateParameter
    {
        private string _name;
        private string _value;
        /// <summary>
        /// Gets or sets the name.  It must match the name of the property.
        /// </summary>
        /// <value>The name of the property.</value>
        [XmlAttribute("Name")]
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
        /// <summary>
        /// Gets or sets the value.
        /// </summary>
        /// <value>The value.</value>
        [XmlText(typeof(string))]
        public string Value
        {
            get { return _value; }
            set { _value = value; }
        }
    }
}

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:

using System;
using MbUnit.Framework;
using SimpleWorkflowEngine.Core.Common;
using SimpleWorkflowEngine.Core.Contracts;
using SimpleWorkflowEngine.Core.Runtime;
 
namespace SimpleWorkflowEngine.Tests.Fixtures
{
    [TestFixture]
    public class WorkflowFactoryTests
    {
        [Test]
        public void TestCreateInstance()
        {
            IWorkflowTemplateProvider provider =
                ApplicationContext.Instance.Resolve<IWorkflowTemplateProvider>();
            WorkflowTemplate template =
                provider.FromTemplateId(
                new Guid("1FE5526C-D0E7-4c58-8644-3B3144AEED0E"));
            Workflow instance = WorkflowFactory.Create(template);
            Assert.AreEqual(1, instance.Actions.Count);
        }
    }
}

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):

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using SimpleWorkflowEngine.Core.Common;
 
namespace SimpleWorkflowEngine.Core.Runtime
{
    /// <summary>
    /// Assembles a workflow instance based on an input template.
    /// </summary>
    public static class WorkflowFactory
    {
        private static string _nullPropertyError = 
            "The parameter \"{0}\" did not match a property on the class \"{1}\".";
        /// <summary>
        /// Assembles a workflow instance from a workflow template instance.
        /// </summary>
        /// <param name="template">The template.</param>
        /// <returns>A workflow instance based on the workflow template.</returns>
        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;
                    // Get the property matching the parameter name.
                    PropertyInfo property = actionType.GetProperty(
                        parameter.Name, flags);
                    if(property == null)
                    {
                        throw new NullReferenceException(string.Format(
                            _nullPropertyError, parameter.Name, actionType.Name));
                    }
                    // Convert the parameter value type.
                    object value = Convert.ChangeType(
                        parameter.Value, property.PropertyType);
                    // Create an assignment binding (set the property value).
                    MemberAssignment binding = Expression.Bind(
                        property, 
                        Expression.Constant(value, property.PropertyType));
                    bindings.Add(binding);
                }
                // Create the code expression and execute it to get a new instance.
                MemberInitExpression init = Expression.MemberInit(
                    Expression.New(actionType), bindings);
                WorkflowAction action = Expression.Lambda<Func<WorkflowAction>>(init)
                    .Compile().Invoke();
                // Add the action to the workflow.
                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:

using System;
using SimpleWorkflowEngine.Core.Attributes;
using SimpleWorkflowEngine.Core.Common;
using SimpleWorkflowEngine.Core.Constants;
using SimpleWorkflowEngine.Extensions.Components;

namespace SimpleWorkflowEngine.Extensions.CustomActions
{
    /// <summary>
    /// A worflow action which sends an email to a recipient.
    /// </summary>
    public class EmailAction : WorkflowAction
    {
        private MailClientService _mailClientService;
        private string _message;
        private string _recipientAddress;
        private string _subject;

        /// <summary>
        /// Sets the mail client service.
        /// </summary>
        /// <value>The mail client service.</value>
        public MailClientService MailClientService
        {
            set { _mailClientService = value; }
        }

        /// <summary>
        /// Gets or sets the recipient address.
        /// </summary>
        /// <value>The recipient address.</value>
        [Parameter(Usage.IsRequired, "Recipient e-mail")]
        public string RecipientAddress
        {
            get { return _recipientAddress; }
            set { _recipientAddress = value; }
        }

        /// <summary>
        /// Gets or sets the message.
        /// </summary>
        /// <value>The message.</value>
        [Parameter(Usage.IsRequired, "Message")]
        public string Message
        {
            get { return _message; }
            set { _message = value; }
        }

        /// <summary>
        /// Gets or sets the subject.
        /// </summary>
        /// <value>The subject.</value>
        [Parameter(Usage.IsRequired, "Subject")]
        public string Subject
        {
            get { return _subject; }
            set { _subject = value; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance 
        /// [requires input].
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance [requires input]; 
        /// otherwise, <c>false</c>.
        /// </value>
        public override bool RequiresInput
        {
            get { return false; }
        }

        /// <summary>
        /// Inheriting member should override this method to implement 
        /// custom execution logic.
        /// </summary>
        /// <param name="context">The context.</param>
        protected override void OnExecute(ExecutionContext context)
        {
            if (_mailClientService == null)
            {
                throw new NullReferenceException(
                    "No mail service client is configured for this runtime.");
            }

            _mailClientService.SendMail(RecipientAddress, Subject, Message);
        }
    }
}

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.

Posted by Charles Chen

Filed under: .Net, Dev Comments Off
Comments (1) Trackbacks (0)
  1. A fascinating approach.

    Good read over the morning Ice Coffee.

    (Found it via Dew Drop)


Trackbacks are disabled.