Click here to Skip to main content
15,886,362 members
Articles / Programming Languages / C#

IronPython - A Configuration Language

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
9 Apr 2009CPOL4 min read 14.7K   14   1
IronPython - a configuration language

I had a requirement to create a state machine for a product at work. The product is a project management tool and therefore has the concept of a job which based on the users' actions moves from one state to another. There was also the additional requirement that the state machine should be configurable by/for different customers. Essentially, the state machine is a large flow diagram but the implementation needed to allow for completely ripping up the first customers flow diagram and replacing it with a completely different one for the next customer.

My first thought was to implement this by modelling the state machine as meta data in the database. However, even just considering the first customers' state machine, it was apparent that the number of edge cases would make this difficult and probably require code changes as soon as the product was sold to different customers, based on their own edge cases. I had already coded part of the state machine in C# so inspired by a Dot Net Rocks podcast featuring Oren Eini, I came up with the idea of coding the state machine in C#, storing the code in the database and then compiling it at runtime. However, I was further inspired by another Dot Net Rocks podcast featuring Michael Foord, talking about IronPython. Michael briefly mentioned embedding the IronPython interpreter in .NET code and on further investigation, the use cases for doing this are pretty much my requirements of being able to write different code for different customers. Michael has also written IronPython in Action which I have bought and can recommend - it helped me get my head round IronPython language and how to go about using it with C#.

From the C# prototype I created, I had a number of helper methods for doing things to manipulate my job business entity and even create a project in project server! I didn't really want to throw this away, so I didn't - I created an abstract class and then implemented the subclass in IronPython. The abstract class is below:

C#
namespace Sharpcoder.BusinessLogic
{
   public abstract class StateMachine
   {
       public StateMachine()
       {
            // I create various Dao's in here that are used by the helper methods
       }

       /// <summary>
       /// Moves a job through the stage gate process.
       /// </summary>
       /// <param name="job"></param>
       public abstract void Transition(Job job);

       // Various helper methods including CreateProject which 
       /creates a project in project server
}

As I said, the sub class is implemented in IronPython, to do this I needed to import the classes from my C# code which I will be using (such as Job) and then subclass my StateMachine abstract class and implement the Transition abstract method. You will notice the reference to self in the Transition methods parameter list which basically means that it is an instance method.

from Sharpcoder.BusinessEntities import (
   Job, JobTask
)
from Sharpcoder.BusinessLogic import StateMachine

class Customer1StateMachine(StateMachine):
   # Moves a job through the stage gate process.
   # param job 
   def Transition(self, job):
       return None
   # Transition - End

In order to be able to use the IronPython implementation of the state machine, I needed to read in the code, essentially compile it and then keep a handle to the state machine so that I could use it at some point in the future. There is a slight performance hit in doing all of this, so I decided to use a service locator, implemented as a singleton which due to what happens on start up of the application, gets created at start up. The interesting parts of this class - the bits that create the state machine object - are shown below. Because I need to reference some of the classes from the main part of my application (not least the StateMachine subclass), I need to load the assemblies where these are defined. I do this with the calls to runtime.LoadAssembly() - this code could definitely be refactored to be implemented in a nicer way. The code also assumes that the IronPython code will define a class that extends the StateMachine abstract class, instantiate an instance of this new class and then assign a reference to it, to a variable called machine - we can then use the machine variable to get a handle on the bespoke state machine.

C#
namespace Sharpcoder.BusinessLogic
{
   public class StateMachineLocator
   {
       // Thread-safe, lazy singleton

       /// <summary>
       /// Creates the state machine.
       /// </summary>
       private void InitFactory()
       {
           ScriptEngine engine = Python.CreateEngine();
           ScriptRuntime runtime = engine.Runtime;
           ScriptScope scope = engine.CreateScope();

           // Add required assemblies - this probably needs some tidying up
           runtime.LoadAssembly(typeof(Job).Assembly); // Sharpcoder.BusinessEntities
           runtime.LoadAssembly(GetType().Assembly); // Sharpcoder.BusinessLogic

           ScriptSource script =
               engine.CreateScriptSourceFromString
		(GetStateMachineSource(), SourceCodeKind.Statements);

           code.Execute(scope);

           scope.TryGetVariable<StateMachine>("machine", out _machine);
       }

       public StateMachine StateMachine
       {
           get { return _machine; }
       }
   }
}

GetStateMachineSource() can be implemented however you see fit. For my prototype, I put the IronPython code in a file which I set to be an embedded resource. Eventually, I will change this to read the code from the database therefore facilitating the requirement of being able to have different state machines for different customers.

C#
/// <summary>
/// Reads the stage gate machine written in IronPython.
/// </summary>
/// <returns>The source code.</returns>
private String GetStateMachineSource()
{
    Assembly assembly = Assembly.GetExecutingAssembly();
     using (Stream stream =
 assembly.GetManifestResourceStream("Sharpcoder.Job.Customer1StateMachine.py"))
    {
        using (StreamReader reader = new StreamReader(stream))
        {
            return reader.ReadToEnd();
        }
    }
}

At this point, we have all of the code required to create the state machine and use it in the application. The only thing left, is to flesh out the state machine so that it actually does something useful! Part of the code for my state machine is below (this is still work in progress, you will notice hard-coded user ids, etc.):

class Customer1StateMachine(StateMachine):
   # Various constants

   # Transitions job to state 1.007
   # param job
   def _Event_1007(self, job):
       self.UpdateJobStage(job, self.JOB_STAGE_1_ID)
       self.UpdateJobState(job, self.JOB_STATE_1007_ID)
       self.UpdateJobStatus(job, self.JOB_STATUS_OPEN_AWAIT_CUST_QUAL_ID)

       # Hardcoded to be steff for now, will eventually need to do 
       # some lookup to find the
       # ???
       self.CreateJobTask(job, self.JOB_TASK_TYPE_CUST_PRE_QUAL_ID, self.STEFF_USER_ID)
       self.CreateProject(job)
   # _Event_1007 - End

   # Transitions job to state 1.010
   # param job
   def _TransitionToState_1010(self, job):
       self.UpdateJobState(job, self.JOB_STATE_1010_ID)
       self.UpdateJobStatus(job, self.JOB_STATUS_OPEN_AWAIT_QUAL_ID)

       # Hardcoded to steff for now, will eventually need to do some lookup to find the
       # manager
       self.CreateJobTask(job, self.JOB_TASK_TYPE_CUST_INFO_ID, self.STEFF_USER_ID)
   # _TransitionToState_1010 - End

   # Transitions job to state 1.010
   # param job
   def _Event_1009(self, job):
       self.TransitionToState_1010(job)
   # _Event_1009 - End

   # Transitions job to state 1.010
   # param job
   def _Event_1010(self, job):
       self.UpdateJobStage(job, self.JOB_STAGE_1_ID)
       self._TransitionToState_1010(job)
       #self.CreateProject(job)
   # _Event_1009 - End

   # Performs the state transitions for a new job
   # param job - The new job.
   def _NewJobStateTransition(self, job):
       if job.JobType.Id is self.WINDFARMS_JOB_TYPE:
           self._Event_1007(job)
       else:
           self._Event_1010(job)
   # _NewJobStateTransition - End

   # Moves a job through the stage gate process.
   # param job 
   def Transition(self, job):
       if not isinstance(job, Job):
           raise Exception("Transition must be called with an object of type Job")

       if job.Id == 0:
           self._NewJobStateTransition(job)
       elif job.JobState == None:
           raise Exception("Job must have a state")
   # Transition - End

# Important!!! Stage Gate State Machine locator relies on the machine variable being set
machine = Customer1StateMachine()

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralThanks Pin
John C Wollner16-Jul-12 11:01
John C Wollner16-Jul-12 11:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.