Creating a dynamic state machine with C# and NHibernate, Part 2: Adding business rules.

This the second part of a series started in an earlier post; Creating a dynamic state machine with C# and NHibernate 

In my first post I showed you how to create a state machine, attach it to an entity and then save it using NHibernate. In this post we’ll extend the state machine with the capability of adding business rules that must be fulfilled to allow transitions. These rules will be dynamically added to each state and persisted to the database using NHibernate.

 

Extend the model with business rules using strategy pattern

The first step will be to use an implementation of the Strategy Pattern to ensure that our business rules engine is open for extension (thus following the open-closed principle). First we’ll define an interface to use for our business rules, listing 1 shows the definition;

 

public interface IRule
{
    bool IsMetBy(Template entity, State state);
}

listing 1, the IRule interface

The IsMetBy method accepts the entity that the state is attached to which we’ll be using as data for our rules later. Next we add a list of business rules to the state class;

 

public class State
{
    ...

    private IList<IRule> _transitionRules = new List<IRule>();
    public virtual IList<IRule> TransitionRules
    {
        get { return _transitionRules; }
        set { _transitionRules = value; }
    }

    public virtual bool HasAllTransitionRulesMetBy(Template entity)
    {
        var transitionIsAllowed = true;
        foreach (var rule in TransitionRules)
            transitionIsAllowed &= rule.IsMetBy(entity, this);

        return transitionIsAllowed;
    }
}

listing 2, list of rules on the state class

With this addition every state now holds a list of rules that need to be fulfilled before the state accept being changed into it. We’ve added a method that runs through all the business rules for the state and validate that all of them are met. This a call to HasAllTransitionRulesMetBy is added to the templates ChangeStateTo method;

public void ChangeStateTo(State newState)
{
    if (State.CanBeChangedTo(newState) && newState.HasAllTransitionRulesMetBy(this))
        State = newState;
    else
        throw new InvalidStateTransitionException();
}

listing 3, changes to the ChangeState method

At this point changing state will run through the list of allowed transitions, functionality we added in the first part, and the list of rules to make sure the transition is allowed. We honor the open – closed principle by allowing rules to be added in a simple fashion, thus not relying on a lot of refactoring when rules change or get’s added.

Implementing a rule

To make this state machine meaningful we need to start creating business rules. For convenience I’ve added a base class Rule that implements some properties needed later but in essence it’s the same as our interface. First rule will ensure that an entity from the template has a ScheduledHours property with a minimum of time. Listing 4 shows our first rule,

 

public class IsScheduledForAtLeast : Rule
{
    public virtual int ScheduledHours { get; set; }

    protected IsScheduledForAtLeast() {}

    public IsScheduledForAtLeast(int scheduledHours)
    {
        this.ScheduledHours = scheduledHours;
    }

    public override bool IsMetBy(Template entity, State state)
    {
       if ( entity.ScheduledHours >= ScheduledHours && state == "Closed" )
           return true;

        return false;
    }
}

listing 4, an example rule

In a typical scenario these rules might be a bit more complex and in part three of this series we’ll look into rules that need more then just the entity state to get it’s work done. Figure 1 displays the model we’ve built so far;

image

Figure 1, our model so far

A test to validate this looks something like listing 5;

public static class States
{
    public static State ClosedState = new State("Closed")
    { TransitionRules = new List<IRule>
    { new MinimumAttendanceRule(8), new IsScheduledForAtLeast(4)} };

    public static State OnGoingState = new State("OnGoing");

    public static State OpenState = new State("Open")
    { AllowedTransitions = new List<State> { "Paused", ClosedState } };
}

[Test]
public void It_will_not_allow_state_transition_from_closed_to_open()
{
    var entity = new Entity(States.ClosedState);

    Assert.Throws(typeof(InvalidStateTransitionException),
        () => entity.ChangeStateTo(States.OpenState));
}

 

Using NHibernate to persist a template with state and business rules.

So far we can build a state machine that is setup with transition rules and business rules for each state, but only in memory. For this to be meaningful we actually need to persist it as well. For our scenario we want to persist the template, it’s current state, all allowed state transitions and the rules added to each transition.

An example setup that we need to persist looks like listing 6;

[Test]
public void Save_a_state_with_transition_rules_added()
{
    var savedState = new State("Open")
                         {
                             TransitionRules =
                             new List<IRule> {
                                new MinimumAttendanceRule(8),
                                new IsScheduledForAtLeast(5)}
                         };

    stateRepository.Save(savedState);
    var fetchedState = validationRepository.Get(savedState.Id);

    Assert.That(fetchedState.TransitionRules.Count, Is.EqualTo(2));

   Assert.That(fetchedState.TransitionRules.FirstOrDefault(
    rule => rule is MinimumAttendanceRule), Is.Not.Null);

    Assert.That(fetchedState.TransitionRules.FirstOrDefault(
    rule => rule is IsScheduledForAtLeast), Is.Not.Null);
}

So how do you save something this dynamic to the database? There is no way of telling what rules will be added and certainly not a table structure that will fit. Can we do it? Yes we can. Using inheritance mapping in NHibernate this is very possible. For our scenario we’re using the inheritance type “Discriminator column” and a many-to-many relationship between state and rule. The database tables for this will look like figure 2;

imagefigure 2, Table structure 

As figure 2 shows it is now possible to store every state with it’s transitions, their rules and any configured value needed (we serialize all values into one column at the moment). We need to update our NHibernate mapping to include the list of rules and all implemented rule types. Listing 7 shows the mapping files for this scenario;

 

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="State" table="States">    

     ...

    <bag name="TransitionRules" cascade="all" table="TransitionRules">
      <key column="StateId" />
      <many-to-many column="RuleId"  class="Rule" />
    </bag>
  </class>
</hibernate-mapping>

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="Rule" table="Rules" abstract="true">
    <id name="Id" type="int">
      <generator class="native" />
    </id>
    <discriminator column="Name" />

    <subclass discriminator-value="IsScheduledForAtLeast"
                     name="IsScheduledForAtLeast">
      <property name="ScheduledHours" column="Value" />
    </subclass>
  </class>
</hibernate-mapping>

listing 7, NHibernate mapping

 

 

Summary

With the usage of interfaces and the open-closed principle, we get a flexible way to add rules to our state machine. These rules can easily be added in a user interface and several templates with different sets of states and business rules. Using some powerful mapping techniques in NHibernate this is persisted easily as well.

This was part 2 of a three part series. In the last part we’ll be using dependency injection in our rules to enable more advanced scenarios. I’ll also provide you with a complete end-to-end sample solution.

13 thoughts on “Creating a dynamic state machine with C# and NHibernate, Part 2: Adding business rules.

      • Hey, any chance we could get a rough cut of part 3’s source code if there was no time to do the tutorial?

        Really interested in where this was going; great article and a good read :D

        Thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>