Some time ago I was experimenting with simple fluent APIs. One of the things I produced while doing so is a small library for building simple state engines which I call AbstractState. This library is available of GitHub under an Apache 2.0 licence.
There are many cases where a full-blown workflow engine is impractical or undesirable but a simple state engine will nicely address the problem at hand. AbstractState addresses many of these cases by providing a simple mechanism to define and execute the permitted state transitions for a given type. The following example configures the permissible transitions for a type:
StateManager.Initialise(initialise => initialise.Add<Simple, SimpleState>()
.GetStateWith(instance => instance.State)
.SetStateWith((instance, state) => instance.SetState(state))
.AllowTransition(SimpleState.First, new[] {SimpleState.Second, SimpleState.Fourth})
.AllowTransition(SimpleState.Second, new[] {SimpleState.First, SimpleState.Third})
.AllowTransition(SimpleState.Third, new[] {SimpleState.First, SimpleState.Third}));
This configuration starts with the Initialise method on the static StateManager class. StateManager is the primary gateway to the library. It is static because the expected use cases have the library embedded in domain classes and other locations where use of a DI container is awkward or impractical.
Initialise takes a delegate of type Action<IConfigurationExpression> that is used to define all the state transitions. For simplicity currently Initialise clears any existing configuration when invoked. IConfigurationExpession contains a single method Add<Stateful, State> which is used to define the state transitions for a single type.
The example above shows the configuration of a type Simple. The current state of a Simple instance is defined by the state type SimpleState. This type may be anything that can be reliably used as a key for a collection. Strings and enums are typical although this is not mandatory. Caution should be exercised with reference types such that distinct instances that represent the same value are considered equal. In this example SimpleState is an enum.
The Add method returns IStatefulConfigurationExpression<TStateful, TState> that defines a number of methods used to define the permitted transitions. This interface also defines the GetStateWith and SetStateWith methods. These methods are the mechanism through which the retrieval and update of state against an instance are defined. This means that the stateful type does not need to implement any particular interface or mechanism in order to be integrated with the state engine. In this case the state is retrieved directly from the state property and is set using a helper method (SetState) that is defined on the stateful class.
Once we have defined how the state engine may interact with the stateful type’s state we specify the supported transactions. AbstractState works on a whitelist model where only state transitions that are explicitly valid are permitted. Here we specify six possible transitions using the AllowTransition, two each for each of the First, Second and Third states. What we see here is that is possible to transition into a state (Fourth) for which there is no defined outgoing transitions. This is valid for end states which may never be exited.
There are two overloads provided for AllowTransition:
IStatefulConfigurationExpression<TStateful, TState> AllowTransition(TState initial, params TState[] finalStates);
IStatefulConfigurationExpression<TStateful, TState> AllowTransition(TState initial, TState final, Func<TStateful, bool> checkFunction);
The first overload sets up simple transitions where the transition from the initial state to the final state(s) are always allowed. The second overload supports cases where the state transition may only be permitted under some circumstances. It takes a delegate of type Func<TStateful, bool> to make this determination. If the return value of this delegate is false when passed an instance of the stateful type then the desired transition will not occur.
Once the state manager has been configured it is simple to invoke:
StateManager.Transition(instance, SimpleState.Third);
This code will perform one of two actions. If the configuration currently allows instance to be put into the Third state then the SetStateWith delegate will be invoked to assign instance to this state. If this is not permitted then the Transition method will throw InvalidOperationException.
Anyone familiar with the StructureMap configuration API may realise that it is one of the primary influences on this API. This is unsurprising as the configuration API is one of my favourite StructureMap features.
I’m reasonably happy with how this turned out as an experiment in fluent APIs. A potential future improvement is to allow the behaviour on an invalid transition to be configurable rather than simply throw InvalidOperationException. I may look at this if there’s any interest in me doing so.