|
|
This project, as well as the example below, was inspired by Simple State Machine.
var phoneCall = new StateMachine<State, Trigger>(State.OffHook);
phoneCall.Configure(State.OffHook)
.Allow(Trigger.CallDialed, State.Ringing);
phoneCall.Configure(State.Ringing)
.Allow(Trigger.HungUp, State.OffHook)
.Allow(Trigger.CallConnected, State.Connected);
phoneCall.Configure(State.Connected)
.OnEntry(t => StartCallTimer())
.OnExit(t => StopCallTimer())
.Allow(Trigger.LeftMessage, State.OffHook)
.Allow(Trigger.HungUp, State.OffHook)
.Allow(Trigger.PlacedOnHold, State.OnHold);
phoneCall.Configure(State.OnHold)
.SubstateOf(State.Connected)
.Allow(Trigger.TakenOffHold, State.Connected)
.Allow(Trigger.HungUp, State.OffHook)
.Allow(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);Features
Some fairly standard state machine constructs are supported:
- Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
- Hierarchical states
- Entry/exit events for states
- Guard clauses to support conditional transitions
- Ability to store state externally (for example, in a property tracked by Linq to SQL)
- Minimal introspection
Hierarchical States
In the example above, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.
In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.
Entry/Exit Events
In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)
The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.
Entry/Exit event handlers are supplied with a parameter of type Transition that describes the trigger, source and destination states.
Guard Clauses
The state machine will choose between multiple transitions based on guard clauses, e.g.:
phoneCall.Configure(State.OffHook)
.AllowIf(Trigger.CallDialled, State.Ringing, () => IsValidNumber)
.AllowIf(Trigger.CallDialled, State.Beeping, () => !IsValidNumber);Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.
Ignored Transitions
Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.
To ignore triggers within certain states, use the Ignore(TTrigger) directive:
phoneCall.Configure(State.Connected)
.Ignore(Trigger.CallDialled);External State Storage
Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:
var stateMachine = new StateMachine<State, Trigger>(
() => myState.Value,
s => myState.Value = s);In this example the state machine will use the myState object for state storage.
Introspection
The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.
Project Goals
The state machine component is intended as a base for exploration of the possible combination of generic and functional programming to build a minimalistic .NET workflow engine.
The project is largely experimental, but the code is unit tested and should be suitable to build upon if it meets your needs.
Some improvements to the DSL are definitely possible and might get made if there is interest in this.
Please use the issue tracker or the contact details on the linked blog if you'd like to report problems or discuss features.
