Stateless .NET Library
repository·dev·Indexed 27 days ago
https://github.com/dotnet-state-machine/statelessA lightweight, fluent .NET library for implementing state machines and state-driven workflows. It supports hierarchical states, entry/exit actions, guard clauses, and parameterized triggers. Stateless allows for external state storage for ORM persistence and provides capabilities to export state machine configurations to DOT and Mermaid graph formats for visualization.
What's inside Stateless
- Stateless is a .NET library used to create state machines and lightweight state machine-based workflows directly in your code. It allows you to define states, triggers, transitions, and associated actions (entry/exit/internal) using a fluent API.
Advanced extensions in Stateless
devBeyond standard constructs, Stateless supports:
- External State Storage: Store the state in an external property (e.g., for ORM persistence).
- Parameterized Triggers: Pass data along with a trigger.
- Reentrant States: Support for states that can be re-entered.
- Graph Export: Export the state machine configuration to DOT or Mermaid formats for visualization.
Core features of Stateless
devStateless provides several standard state machine constructs:
- Generic Support: States and triggers can be any .NET type (enums, strings, numbers, etc.).
- Hierarchical States: Support for nested state structures.
- Entry/Exit Actions: Execute code automatically when entering or exiting a state.
- Guard Clauses: Add conditional logic to transitions.
- Introspection: Inspect the current state and configuration of the machine.
Quickstart with Stateless
devTo create a state machine in .NET, instantiate a
StateMachine<TState, TTrigger>with an initial state. You then use the.Configure(state)method to define transitions using.Permit(trigger, targetState)or define internal transitions and entry/exit actions. To move the machine from one state to another, use the.Fire(trigger)method.var phoneCall = new StateMachine<State, Trigger>(State.OffHook); phoneCall.Configure(State.OffHook) .Permit(Trigger.CallDialled, State.Ringing); phoneCall.Configure(State.Connected) .OnEntry(t => StartCallTimer()) .OnExit(t => StopCallTimer()) .InternalTransition(Trigger.MuteMicrophone, t => OnMute()) .InternalTransition(Trigger.UnmuteMicrophone, t => OnUnmute()) .InternalTransition<int>(_setVolumeTrigger, (volume, t) => OnSetVolume(volume)) .Permit(Trigger.LeftMessage, State.OffHook) .Permit(Trigger.PlacedOnHold, State.OnHold); // ... phoneCall.Fire(Trigger.CallDialled); // phoneCall.State is now State.RingingCreate a basic state machine with Stateless
devTo use Stateless, instantiate a
StateMachine<TState, TTrigger>with an initial state. Use the.Configure(State)method to define behavior for specific states, such as permitting transitions via.Permit(Trigger, State), defining entry/exit actions via.OnEntry()and.OnExit(), or handling internal transitions via.InternalTransition(). Trigger events using the.Fire(Trigger)method.var phoneCall = new StateMachine<State, Trigger>(State.OffHook); phoneCall.Configure(State.OffHook) .Permit(Trigger.CallDialled, State.Ringing); phoneCall.Configure(State.Connected) .OnEntry(t => StartCallTimer()) .OnExit(t => StopCallTimer()) .InternalTransition(Trigger.MuteMicrophone, t => OnMute()) .InternalTransition(Trigger.UnmuteMicrophone, t => OnUnmute()) .InternalTransition<int>(_setVolumeTrigger, (volume, t) => OnSetVolume(volume)) .Permit(Trigger.LeftMessage, State.OffHook) .Permit(Trigger.PlacedOnHold, State.OnHold); // ... phoneCall.Fire(Trigger.CallDialled); Assert.AreEqual(State.Ringing, phoneCall.State);Implement External State Storage
devTo use Stateless with ORMs or UI frameworks that require state to be stored in specific properties, provide getter and setter delegates to the
StateMachineconstructor.var stateMachine = new StateMachine<State, Trigger>( () => myState.Value, s => myState.Value = s);Configure Hierarchical States
devYou can define substates to create a hierarchy. A substate is considered part of its superstate. For example, if
OnHoldis a substate ofConnected, callingIsInState(State.Connected)will returntruewhen the machine is in theOnHoldstate. Use.SubstateOf()to establish this relationship.phoneCall.Configure(State.OnHold) .SubstateOf(State.Connected) .Permit(Trigger.TakenOffHold, State.Connected) .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);Configure Initial State Transitions
devA substate can be marked as the initial state of a superstate using
.InitialTransition(). When the machine enters the superstate, it automatically enters the designated substate.Note: Because Stateless does not track when it is 'started', you can implement a manual start by using a dummy initial state and calling
.Activate()to fire the first transition.// Setting an initial substate sm.Configure(State.B) .InitialTransition(State.C); sm.Configure(State.C) .SubstateOf(State.B); // Workaround for starting the machine sm.Configure(InitialState) .OnActivate(() => sm.Fire(LetsGo)) .Permit(LetsGo, StateA)Introspect State Machine Configuration
devUse the following properties and methods to inspect the machine:
StateMachine.PermittedTriggers: Returns a list of triggers that can be successfully fired in the current state.StateMachine.GetInfo(): Retrieves information about the state configuration.
Use Dynamic State Transitions
devUse
.PermitDynamic()to determine the destination state at runtime based on logic or trigger parameters. If a dynamic transition results in the same state as the current one, it acts as a reentrant transition.stateMachine.Configure(State.Start) .PermitDynamic(Trigger.CheckScore, () => score < 10 ? State.LowScore : State.HighScore);Use Activation and Deactivation
devUse.Activate()and.Deactivate()to perform logic when restoring or preparing to store the state machine.Activateshould be called once before normal operation starts and once before state storage.Export State Machine to Mermaid Graph
devGenerate Mermaid diagrams for use in GitHub markdown or Obsidian using
MermaidGraph.Format(machine.GetInfo()).string graph = MermaidGraph.Format(phoneCall.GetInfo());