Stateless 3.0
Workflow. If you’ve built line-of-business apps in a government department, or complex “enterprise” environment, you’ll immediately recognize the kind of application I’m talking about. States, flowcharts, arcane rules… If you haven’t had this experience: modelling bugs in a bug tracker is a close approximation, but generally orders of magnitude less complex than the horrors that large organizations can cook up.
Unfortunately, the tools for building complex workflows tend to themselves be complex and unwieldy. Not wanting to burden ourselves with Windows Workflow Foundation, back then in its XAML-designer-driven first version, a team I was working on decided to take the “POCO” route and model documents in the workflow as entities mapped to SQL Server, accepting we’d need to do the heavy-lifting ourselves.
Things went well, but over time, the logic that decided which actions were allowed in each state, and what the state resulting from an action should be, grew into a tangle of if
and switch
. Inspired by Simple State Machine, I eventually refactored this out into a little state machine class that was configured declaratively: in this state, allow this trigger, transition to this other state, and so-on.
Declarative state machines handle scaling to greater complexity better than procedural code does. Not only is the state machine definition self-documenting, but it’s easier to manage cross-cutting concerns, enforce invariants, and even generate diagrams showing the various states and relationships between them.
The interesting part was that, because the actual _state
field was mapped to a column by our ORM, the state machine didn’t actually store the current state. Instead, when constructing it, you could provide a pair of delegates to get the current state and set the new state after a transition:
public class Bug
{
enum State { Open, Assigned, Deferred, Resolved, Closed }
enum Trigger { Assign, Defer, Upvote, Resolve, Close }
State _state = State.Open;
readonly StateMachine<State, Trigger> _machine;
public Bug()
{
_machine = new StateMachine<State, Trigger>(() => _state, s => _state = s);
_machine.Configure(State.Open)
.Permit(Trigger.Assign, State.Assigned)
.Permit(Trigger.Close, State.Closed);
_machine.Configure(State.Assigned)
.OnEntry(() => NotifyAssignee())
// ...
}
public void Close()
{
_machine.Fire(Trigger.Close);
}
// ...
This separation of the transition rules from the state storage turned out nicely: you could attach state machine logic onto all kinds of things in an unintrusive manner. Later on a sleepless night I decided to re-implement the idea as a generic library, and thus Stateless was born.
Stateless has retained its simple API, but over time it has been improved substantially. It’s been tweaked for all kinds of scenarios, not just enterprise workflow: people have used it to implement user interfaces, IoT devices, actors in concurrent systems, and more in between.
Version 2.0 acquired parameterized triggers, and version 2.5 got DOT graph visualization.
The most recent version, 3.0, just came off the press yesterday. Here’s what’s new!
.NET Core support
At version 3.0, Stateless supports .NET Core through the magic of .NET Standard. Through the .NET Standard 1.0 target, the package is portable across the .NET Framework, .NET Core, Windows Store apps and Windows Phone. As part of this change, project.json’s multi-targeting support let us bring back .NET 4.0 support as well.
Artur Karbone landed the PR for this. I think it really got the ball rolling for the whole release - thanks Artur!
Asynchronous actions
Task<T>
and async
are inescapable in modern .NET development. To call async
methods in OnEntry()
and OnExit()
actions, deep support in Stateless was required.
_machine.Configure(State.Assigned)
.OnEntryAsync(async () => await SendEmailToAssignee());
await _machine.FireAsync(Trigger.Assign);
Stateless 3.0 adds OnEntryAsync()
and so-on alongside the existing synchronous counterparts. To interact with a StateMachine
instance that uses asynchronous actions, FireAsync()
must be called in place of the synchronous Fire()
.
Yevhen Bobrov encountered the need for async
support in Stateless during the course of his work with Orleans, and took the initiative in sending the comprehensive PR for the feature.
Reentrant Fire()
One of the longest-lived bugs on the Stateless issue tracker was to support reentrant Fire()
.
While it’s always been possible to call Fire()
within an entry or exit action during a state transition, the semantics were “foggy” (to put it mildly), with handlers firing at different times and in different orders.
_machine.Configure(State.Resolved)
.OnEntry(() => {
if (autoClose)
{
// While handling one transition, kick off another
_machine.Fire(Trigger.Close);
}
});
In Stateless 3.0 this code works reliably: the entry and exit actions for the Resolved
state will finish firing before the machine will move on to processing the second Fire()
call.
Ian Wall raised the original issue and provided the corresponding PR that’s shipping in this version.
Internal transitions
Henning added support for internal transitions with this PR. Internal transitions are actions taken upon a trigger that do not result in a visible state change:
_machine.Configure(State.Deferred)
.Permit(Trigger.Upvote, State.Open);
_machine.Configure(State.Open)
.OnEntry(() => _votes = 1)
.InternalTransition(Trigger.Upvote, () => _votes++);
Internal transitions make it easier to encapsulate the effects of a trigger. In the Deferred
state, an Upvote
trigger may result in a transtiion back to Open
. Within the Open
state the trigger is still accepted, but only the _upvotes
side-effect occurs.
Activation/deactivation events
Also from Yevhen Bobrov, the new Activate()
, OnActivate()
and corresponding de-activation events allow state machines to re-establish conditions particular to a state after peristence events.
For example, if the state machine is managing an IoT audio speaker, it may invoke the hardware switch to turn “on” when it transitions into the On
state:
machine.Configure(State.On)
.OnEntry(() => TurnOn());
When the speaker reloads after a power outage however, we want the hardware switch “on” even though the state machine is not transitioning into the On
state, just loading up. That’s the purpose of OnActivate()
:
machine.Configure(State.On)
.OnActivate(() => TurnOn());
// After re-loading:
machine.Activate();
Activation events will fire when transitioning into the state, and when the new Activate()
method is called.
CLSCompliantAttribute
Suraj Gupta provided a PR to mark Stateless as being CLS-compliant, which allows it to be referenced from other CLS-compliant projects.
The future
Stateless has seen a pretty huge burst of activity recently, and appears to be ramping up. It’s great to see the future looking bright for this happy little chunk of code. If you’re using Stateless it’d be awesome to hear from you! Oh, and enjoy the new version :-)