Introduction
The current trunk (as of august 28, 2008) allows users to override the default file persistence storage. This is a small howto to demonstrate the use of Active Record to persist workflow definitions to a database.

Persistence
In workflow scenario's that have longer life cycle it's wise to store the workflow state somewhere that survives system failures, reboots or other unexpected things rather then keeping it in memory. This is where the persistence services of the Simple State Machine project comes in.

When saving a workflow we need to store enough information so that we can later recreate a statemachine and continue to run as if nothing ever happened.

With that in mind we can determine that there are a couple of things that need to be stored;
  • the workflow identity
  • it's state
  • the machine definition
  • the link with the application domain (the domain content)

Storing the identity and the current state is not so difficult. Each statemachine contains a GUID that we can use and the current state has a name.
The machine definition, as you probably know, is stored inside your boo files, so if we store the name of the file that was used to create the workflow we should have enough information to restart the machine later.

The domain context is the part that reflects the state of the workflow inside your application domain. Each time we ask the Workflow Service (WS) to handle an event it can also update the state in your domain model. We'll look at this link later on.

The case
In this howto we will be building a workflow service that can handle an account opening request at our company. Customers that apply for an account are required to send in a signed contact and make a deposit of 100 Euro to our bank account.

When these two requirements are met an account manager can activate the account and the customer can start using the provided services. A final requirement is that account opening requests can be denied as long as they have not been accepted.

This gives us the following workflow definition:

workflow "Account Opening Request workflow"

#Event Definitions
#--------------------------------------------------------
trigger @AccountRequestSubmitted
trigger @AccountRequestDenied
trigger @AccountRequestAccepted
trigger @SignedContractReceived
trigger @DepositReceived
trigger @AccountClosed

#State & Transition Definitions
#--------------------------------------------------------
state @AccountRequestPending:
	when @AccountRequestDenied		>> @AccountRequestDenied
	when @SignedContractReceived		>> @AwaitingDeposit

state @AwaitingDeposit:
	when @AccountRequestDenied		>> @AccountRequestDenied
	when @DepositReceived			>> @AwaitingAccountActivation

state @AwaitingAccountActivation:
	when @AccountRequestDenied		>> @AccountRequestDenied
	when @AccountRequestAccepted		>> @AccountActivated

state @AccountActivated:
	when @AccountClosed			>> @AccountClosed

state @AccountClosed
state @AccountRequestDenied


The domain model
Our domain model currently only contains one account class that represents the account opening request for the customer. As you can see we are using ActiveRecord as a persistence engine. The ModelBase class is nothing more the an abstract class that sits on top of our domain model that provides an Id.

[ActiveRecord]
public class Account : ModelBase
{
	private string _customerName;
	private AccountState _accountState;

	[Property]
	public string CustomerName
	{
		get { return _customerName; }
		set { _customerName = value; }
	}

	[Property]
	public AccountState AccountState
	{
		get { return _accountState; }
		set { _accountState = value; }
	}
}


Active record binding
Out of the box Simple State Machine (SSM) provides support for saving workflow definitions (WFD) to disk using the .Net XmlSerializer class. When storing a WFD in a database we need a different structure. So the first thing we need to do is to create an Active Record entity that can store such a WFD.

This is done by implementing the IWorkflowEntity interface and it looks like this:

[ActiveRecord]
public class ARWorkflowEntity : IWorkflowEntity {
	private Guid _workflowId;
	private string _machineConfiguration;
	private string _currentState;
	private string _domainContextTypeDescription;
	private object _domainContextId;
	private string _domainContextStatusProperty;

	/// <summary>
	/// Get or set the Id of this entity
	/// </summary>
	[PrimaryKey(Generator = PrimaryKeyType.Assigned)]
	public Guid Id {
		get { return _workflowId; }
		set { _workflowId = value; }
	}

	public Guid WorkflowId {
		get { return _workflowId; }
		set { _workflowId = value; }
	}

	[Property]
	public string MachineConfiguration {
		get { return _machineConfiguration; }
		set { _machineConfiguration = value; }
	}

	[Property]
	public string CurrentState {
		get { return _currentState; }
		set { _currentState = value; }
	}

	[Property]
	public string DomainContextTypeDescription {
		get { return _domainContextTypeDescription; }
		set { _domainContextTypeDescription = value; }
	}

	public object DomainContextId {
		get { return _domainContextId; }
		set { _domainContextId = value; }
	}

	[Property]
	public int PersistedDomainContextID {
		get { return _domainContextId is int ? (int)_domainContextId : 0; }
		set { _domainContextId = value; }
	}

	[Property]
	public string DomainContextStatusProperty {
		get { return _domainContextStatusProperty; }
		set { _domainContextStatusProperty = value; }
	}
}

Since Active Record doesn't know how to persist the anonymous DomainContextId property, we use a little trick to unbox it via the PersistedDomainContextID property.

The saving and loading of WFD is accomplished via the IWorkflowPersister interface. So the next thing is to implement a WorkflowPersister that uses Active Record.

/// <summary>
/// Active record workflow persister
/// </summary>
public class ARWorkflowPersister : IWorkflowPersister
{
	/// <summary>
	/// Saves the specified workflow entity.
	/// </summary>
	/// <param name="workflowEntity">The workflow entity.</param>
	public void Save(IWorkflowEntity workflowEntity)
	{
		ActiveRecordMediator.CreateAndFlush(workflowEntity);
	}

	/// <summary>
	/// Updates the specified workflow entity.
	/// </summary>
	/// <param name="workflowEntity">The workflow entity.</param>
	public void Update(IWorkflowEntity workflowEntity)
	{
		ActiveRecordMediator.UpdateAndFlush(workflowEntity);
	}

	/// <summary>
	/// Loads the specified workflow id.
	/// </summary>
	/// <param name="workflowId">The workflow id.</param>
	/// <returns></returns>
	public IWorkflowEntity Load(Guid workflowId)
	{
		return (IWorkflowEntity) ActiveRecordMediator.FindByPrimaryKey(typeof(ARWorkflowEntity), workflowId, true);
	}

	/// <summary>
	/// Completes the specified workflow entity.
	/// </summary>
	/// <param name="workflowEntity">The workflow entity.</param>
	public void Complete(IWorkflowEntity workflowEntity)
	{
		ActiveRecordMediator.UpdateAndFlush(workflowEntity);
	}

	/// <summary>
	/// Creates the empty workflow entity.
	/// </summary>
	/// <param name="workflowId">The workflow id.</param>
	/// <returns></returns>
	public IWorkflowEntity CreateEmptyWorkflowEntity(Guid workflowId)
	{
		return new ARWorkflowEntity { WorkflowId = workflowId };
	}


Domain binding
We are almost ready to see our workflow persisting service in action. There is one last binding that we need to realize and that's the link between our application domain and the workflow. As you'll remember from the start of this article, we have a class called Account that we want to associate with a running workflow. This can be accomplished by passing an Account instance when we start a workflow.

// first we create a new account and set the initial state
var myAccount = new Account
                {
                	CustomerName = "Ernst",
                	AccountState = AccountState.AccountRequestPending
                };
		
// this is where we pass the account instance to make a link between the workflow and the account.
var machineContext = _workflowService.Start("accountopeningworkflow", myAccount, "AccountState");


The binding between the workflow instance and the domain entity is kept by the DomainContextRepository. Classes that implement such an interface must be able to create a string that allows us to identify the domain object that the workflow is linked to. We need this because the Workflow Service has no idea what kind object you are passing as a reference our how you would like to restore it in the case of a new event that needs to be handled. In this example we'll be using the Type name and the object's namespace.

This is the code for the Active Record DomainContextRepository:

public class ARDomainContextRepository : IDomainContextRepository {
	/// <summary>
	/// Gets the id.
	/// </summary>
	/// <param name="instance">The instance.</param>
	/// <returns></returns>
	public object GetId(object instance) {
		return ((ModelBase)instance).Id;
	}

	/// <summary>
	/// Gets the type description.
	/// </summary>
	/// <param name="instance">The instance.</param>
	/// <returns></returns>
	public string GetTypeDescription(object instance) {
		var type = instance.GetType();
		return string.Format("{0}, {1}", type.FullName, type.Assembly.GetName().Name);
	}

	/// <summary>
	/// Loads the specified type description.
	/// </summary>
	/// <param name="typeDescription">The type description.</param>
	/// <param name="id">The id.</param>
	/// <returns></returns>
	public object Load(string typeDescription, object id) {
		var type = Type.GetType(typeDescription, true);
		return ActiveRecordMediator.FindByPrimaryKey(type, id);
	}

	/// <summary>
	/// Saves the specified instance.
	/// </summary>
	/// <param name="instance">The instance.</param>
	public void Save(object instance) {
		ActiveRecordMediator.SaveAndFlush(instance);
	}
}


The application
Now that we've implemented all our bindings we can start writing a sample application that create a new account and starts the workflow. You can step through it and keep an eye on your database to see it updating the values.

Here is our sample application:

// first we create a new account and set the initial state
var myAccount = new Account
                {
                	CustomerName = "Ernst",
                	AccountState = AccountState.AccountRequestPending
                };

// then we start the workflow for the new account.
var machineContext = _workflowService.Start("accountopeningworkflow", myAccount, "AccountState");

// we received a signed contact and would like to update the workflow state.
machineContext.HandleEvent(AccountEvent.SignedContractReceived);

// the next state should be that we are waiting for the deposit
Assert.That(myAccount.AccountState, Is.EqualTo(AccountState.AwaitingDeposit));

// this should also be stored in the database
var entity = (ARWorkflowEntity) ActiveRecordMediator.FindFirst(typeof (ARWorkflowEntity));
Assert.That(entity.CurrentState, Is.EqualTo(Convert.ToString(AccountState.AwaitingDeposit)));

// walk through the rest of the workflow, you can place breakpoints on these line to see the WFD updating in the DB.
machineContext.HandleEvent(AccountEvent.DepositReceived);
machineContext.HandleEvent(AccountEvent.AccountRequestAccepted);
machineContext.HandleEvent(AccountEvent.AccountClosed);

// and it should be completed here
Assert.That(machineContext.IsComplete);


Attached is a zip file that contains a Visual Studio solution with all the code, libraries and the sample application as a unit test so you can see it running. Download it here.

As a final note:
  • Don't forget to create a database named 'ActiveRecordWorkflowSample'.
  • It assumes a local copy of SQLExpress, check the setup function for connection details.
  • The sample application is licensed under the Apache License, Version 2.0 so you're free to use it as you like.

Last edited Aug 28, 2008 at 2:35 PM by ernstnaezer, version 5

Comments

No comments yet.