Stateless .NET Library

repository·dev·Indexed 27 days ago

https://github.com/dotnet-state-machine/stateless

A 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.

Tokens
3.5K
Snippets
16
Records
24
Agent score
42%

What's inside Stateless

  1. Advanced extensions in Stateless

    dev

    Beyond 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.
  2. Core features of Stateless

    dev

    Stateless 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.
  3. Quickstart with Stateless

    dev

    To 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.Ringing
  4. Create a basic state machine with Stateless

    dev

    To 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);
  5. Implement External State Storage

    dev

    To use Stateless with ORMs or UI frameworks that require state to be stored in specific properties, provide getter and setter delegates to the StateMachine constructor.

    var stateMachine = new StateMachine<State, Trigger>(
        () => myState.Value,
        s => myState.Value = s);
  6. Configure Hierarchical States

    dev

    You can define substates to create a hierarchy. A substate is considered part of its superstate. For example, if OnHold is a substate of Connected, calling IsInState(State.Connected) will return true when the machine is in the OnHold state. Use .SubstateOf() to establish this relationship.

    phoneCall.Configure(State.OnHold)
        .SubstateOf(State.Connected)
        .Permit(Trigger.TakenOffHold, State.Connected)
        .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);
  7. Configure Initial State Transitions

    dev

    A 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)
  8. Introspect State Machine Configuration

    dev

    Use 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.
  9. Use Dynamic State Transitions

    dev

    Use .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);