Building a better .NET State Machine
[Note: Updated version on Nuget has slightly different API, see latest blog post.]
There are several state machine implementations for .NET out there but, sadly, none of them met all of the requirements I have for a state machine. These are:-
- Well written using encapsulation and other good practices
- Able to be easily serialized to disk
- Able to handle temporal events easily (After ... At ... Every ...)
- Serialized form must expose a property saying when it next needs to be fetched from disk to run
- Implements hierarchical states with entry and exit actions
So I built one, and have made the source code available on Nuget so you can add it to any project easily without any extra DLLs.
Look for "AboditStateMachine" on Nuget to download it. The download includes a sample state machine documented to show off some of its capabilities.
Defining states is easy, just give them a name and specify their parent state if any:-
public static readonly State UnVerified =
AddState("UnVerified");
public static readonly State Verified =
AddState("Verified");
States are hierarchical. If you are in state VerifiedRecently
you are also in its parent state Verified
.
public static readonly State VerifiedRecently =
AddState("Verified recently", parent: Verified);
public static readonly State VerifiedAWhileAgo =
AddState("Verified a while ago", parent: Verified);
You can use any type that's IEquatable
as an Event
type or you
can use the provided Event
class:
private static Event eUserVerifiedEmail =
new Event("User verified email");
private static Event eScheduledCheck =
new Event("Scheduled Check");
private static Event eBeenHereAWhile =
new Event("Been here a while");
The state machine itself is specified in a static constructor so it runs just once no matter how many instances of the state machine you create.
Each method is provided with an instance of the state machine m
as
well as the state s
and the event e
as appropriate:
static DemoStatemachine()
{
UnVerified
.OnEnter((m, s, e) => {
// States can execute code when they are entered or when they are left
// In this case we start a timer to bug the user until they confirm their email
m.Every(new TimeSpan(hours: 10, minutes:0, seconds:0), eScheduledCheck);
// You can also set a reminder to happen at a specific time, or after a given
// interval just once
m.At(new DateTime(DateTime.Now.Year+1, 1, 1), eScheduledCheck);
m.After(new TimeSpan(hours: 24, minutes: 0, seconds: 0), eScheduledCheck);
})
// All necessary timing information is serialized with the state machine
// The serialized state machine also exposes a property showing when it
// next needs to be woken up
// External code will need to call the Tick(utc) method at that time to trigger
// the next temporal event
.When(eScheduledCheck, (m, s, e) =>
{
// Send a message to the user asking them to verify their email
Trace.WriteLine("Send verify email message");
// We return the current state 's' rather than 'UnVerified' in case
// we are in a child state of 'Unverified'
// This makes it easy to handle hierarchical states and to either change
// to a different state or stay in the same state
return s; })
.When(eUserVerifiedEmail, (m, s, e) =>
{
Trace.WriteLine("The user has verified their email address, we are done (almost)");
// Kill the scheduled check event, we no longer need it
m.CancelScheduledEvent(eScheduledCheck);
// Start a timer for one last transition
m.After(new TimeSpan(hours:24, minutes:0, seconds:0), eBeenHereAWhile);
return VerifiedRecently;
});
VerifiedRecently
.When(eBeenHereAWhile, (m, s, e) =>
{
Trace.WriteLine("User has now been a member for over 24 hours");
Trace.WriteLine("Give them additional priviledges for example");
// No need to cancel the 'eBeenHereAWhile' event because it wasn't auto-repeating
//m.CancelScheduledEvent(eBeenHereAWhile);
return VerifiedAWhileAgo;
});
Verified
.OnEnter((m, s, e) =>
{
Trace.WriteLine("The user is now fully verified");
});
VerifiedAWhileAgo.OnEnter((m, s, e) =>
{
Trace.WriteLine("The user has been verified for over 24 hours");
});
}
With your state machine defined you can now create instances of it, trigger events on them, serialize them to disk, fetch them back, carry on eventing on them, ...
DemoStatemachine demoStateMachine =
new DemoStatemachine(DemoStatemachine.UnVerified);
// At the time specified in demoStateMachine.NextTimedEventAt you reload
// the state machine from disk and call
demoStateMachine.Tick(DateTime.UtcNow);
// When the user verifies their email address you call ...
demoStateMachine.VerifiesEmail();
// At any other time you can examine the current state, or
// act on the state changed event, ...
I hope you find this new state machine implementation useful, and if you have any feedback, do please send it my way.