Skip to main content

GST Stateless State Machine Usage Guide

Target Audience: GST team developers Prerequisites: C# fundamentals, MVVM pattern Estimated Reading Time: 20 minutes


1. Overview

1.1 Why Use Stateless

Stateless is a lightweight .NET state machine framework. The GST team chose it for the following reasons:

AdvantageDescription
Clear state transitionsExplicitly defines allowed state transitions, preventing illegal operations
Guard protectionUses PermitIf to set conditional transitions, ensuring business logic correctness
UI integration friendlyCanFire property can be directly bound to MVVM Commands
Visualization supportBuilt-in DOT/UML diagram output for documentation
Lightweight, no dependenciesNo complex configuration needed, use directly

1.2 Applicable Scenarios

  • Device connection management: Disconnected → Connecting → Connected → Error
  • Control flow: NotInitialized → Ready → Running → Paused → Stopped
  • Data processing: Idle → Processing → Completed → Error

1.3 Package Information

<PackageReference Include="Stateless" Version="5.20.0" />

2. Basic Patterns

2.1 Naming Conventions

ElementConventionExample
State enum{Feature}StateConnectionState, SmbControllerState
Trigger enum{Feature}TriggerConnectionTrigger, SmbTrigger
State machine class{Feature}StateMachineDeviceConnectionStateMachine

2.2 State Enum Example

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionState.cs

namespace Jope.SMB.Core.Devices;

/// <summary>
/// Unified connection state for all device types
/// </summary>
public enum ConnectionState
{
/// <summary>Device is not connected</summary>
Disconnected = 0,

/// <summary>Device is attempting to connect</summary>
Connecting = 1,

/// <summary>Device is connected and ready</summary>
Connected = 2,

/// <summary>Device is attempting to reconnect after connection loss</summary>
Reconnecting = 3,

/// <summary>Device has a connection error</summary>
Error = 4
}

2.3 Trigger Enum Example

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionTrigger.cs

namespace Jope.SMB.Core.Devices;

/// <summary>
/// Triggers for device connection state machine
/// </summary>
public enum ConnectionTrigger
{
/// <summary>Initiate connection</summary>
Connect,

/// <summary>Connection succeeded</summary>
Success,

/// <summary>Connection failed</summary>
Failure,

/// <summary>Disconnect actively</summary>
Disconnect,

/// <summary>Connection lost unexpectedly</summary>
LostConnection,

/// <summary>Retry connection</summary>
Retry
}

2.4 Class Structure

A standard state machine class should contain the following structure:

public class {Feature}StateMachine
{
// 1. Private fields
private readonly StateMachine<{State}, {Trigger}> _machine;

// 2. Public properties - State
public {State} State => _machine.State;

// 3. Public properties - CanFire
public bool CanXxx => _machine.CanFire({Trigger}.Xxx);

// 4. Events
public event EventHandler<StateChangedEventArgs>? StateChanged;

// 5. Constructor
public {Feature}StateMachine()
{
_machine = new StateMachine<{State}, {Trigger}>({State}.Initial);
ConfigureStateMachine();
}

// 6. ConfigureStateMachine method
private void ConfigureStateMachine() { ... }

// 7. Fire methods
public void Fire({Trigger} trigger) { ... }
}

2.5 ConfigureStateMachine() Pattern

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs:62-95

private void ConfigureStateMachine()
{
// Disconnected state
_machine.Configure(ConnectionState.Disconnected)
.Permit(ConnectionTrigger.Connect, ConnectionState.Connecting)
.Ignore(ConnectionTrigger.Disconnect);

// Connecting state
_machine.Configure(ConnectionState.Connecting)
.Permit(ConnectionTrigger.Success, ConnectionState.Connected)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected);

// Connected state
_machine.Configure(ConnectionState.Connected)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected)
.Permit(ConnectionTrigger.LostConnection, ConnectionState.Reconnecting)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error);

// Reconnecting state
_machine.Configure(ConnectionState.Reconnecting)
.Permit(ConnectionTrigger.Success, ConnectionState.Connected)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected);

// Error state
_machine.Configure(ConnectionState.Error)
.Permit(ConnectionTrigger.Retry, ConnectionState.Connecting)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected)
.OnEntry(() => { /* Error state entered */ });

// Register transition callback
_machine.OnTransitioned(OnStateMachineTransitioned);
}

3. Advanced Features

3.1 OnEntry / OnExit

Execute actions when entering or leaving a state:

_machine.Configure(ConnectionState.Error)
.Permit(ConnectionTrigger.Retry, ConnectionState.Connecting)
.OnEntry(() => { /* Execute when entering Error state */ })
.OnExit(() => { /* Execute when leaving Error state */ });

3.2 OnEntryAsync (Asynchronous)

Use OnEntryAsync for asynchronous operations:

_machine.Configure(ConnectionState.Connecting)
.OnEntryAsync(async () =>
{
await ConnectToDeviceAsync();
})
.Permit(ConnectionTrigger.Success, ConnectionState.Connected)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error);

Note: When using async, you need to use FireAsync to trigger:

await _machine.FireAsync(ConnectionTrigger.Connect);

3.3 PermitIf (Conditional Transition)

Set transition conditions (Guards):

_machine.Configure(SmbControllerState.Ready)
.PermitIf(SmbTrigger.Start, SmbControllerState.Running,
() => _configuration != null,
"Configuration must be set before starting");

3.4 PermitReentry (Reentry)

Allow reentry into the same state:

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs:133-137

// Ready state
_machine.Configure(SmbControllerState.Ready)
.Permit(SmbTrigger.Start, SmbControllerState.Running)
.PermitReentry(SmbTrigger.Configure) // Allow repeated configuration
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

3.5 Ignore (Ignore Trigger)

Ignore a specific trigger (does not throw an exception):

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs:65-67

// Disconnected state
_machine.Configure(ConnectionState.Disconnected)
.Permit(ConnectionTrigger.Connect, ConnectionState.Connecting)
.Ignore(ConnectionTrigger.Disconnect); // Do nothing when already disconnected

3.6 OnTransitioned (Transition Callback)

Register a global transition callback:

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs:97-106

_machine.OnTransitioned(OnStateMachineTransitioned);

private void OnStateMachineTransitioned(
StateMachine<ConnectionState, ConnectionTrigger>.Transition transition)
{
StateChanged?.Invoke(this, new ConnectionStateChangedEventArgs
{
PreviousState = transition.Source,
CurrentState = transition.Destination,
Trigger = transition.Trigger,
ErrorMessage = transition.Destination == ConnectionState.Error
? _lastErrorMessage : null
});
}

4. UI Integration (WPF MVVM)

4.1 CanFire Property Design

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs:57-94

Expose CanFire properties at the Controller/Service layer:

#region CanFire Properties

/// <summary>
/// Whether the controller can be configured
/// </summary>
public bool CanConfigure => _machine.CanFire(SmbTrigger.Configure);

/// <summary>
/// Whether the controller can start
/// </summary>
public bool CanStart => _machine.CanFire(SmbTrigger.Start);

/// <summary>
/// Whether the controller can pause
/// </summary>
public bool CanPause => _machine.CanFire(SmbTrigger.Pause);

/// <summary>
/// Whether the controller can resume
/// </summary>
public bool CanResume => _machine.CanFire(SmbTrigger.Resume);

/// <summary>
/// Whether the controller can stop
/// </summary>
public bool CanStop => _machine.CanFire(SmbTrigger.Stop);

/// <summary>
/// Whether the controller can reset
/// </summary>
public bool CanReset => _machine.CanFire(SmbTrigger.Reset);

/// <summary>
/// Whether the controller can acknowledge an error
/// </summary>
public bool CanAcknowledge => _machine.CanFire(SmbTrigger.Acknowledge);

#endregion

4.2 ViewModel Binding

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs:52-89

Forward CanFire properties in the ViewModel:

#region CanFire Properties (from JOP-63 State Machine)

/// <summary>
/// Whether the Start button should be enabled
/// </summary>
public bool CanStart => _controller.CanStart;

/// <summary>
/// Whether the Pause button should be enabled
/// </summary>
public bool CanPause => _controller.CanPause;

/// <summary>
/// Whether the Resume button should be enabled
/// </summary>
public bool CanResume => _controller.CanResume;

/// <summary>
/// Whether the Stop button should be enabled
/// </summary>
public bool CanStop => _controller.CanStop;

/// <summary>
/// Whether the Reset button should be enabled
/// </summary>
public bool CanReset => _controller.CanReset;

/// <summary>
/// Whether the Acknowledge button should be enabled
/// </summary>
public bool CanAcknowledge => _controller.CanAcknowledge;

/// <summary>
/// Whether configuration changes are allowed
/// </summary>
public bool CanConfigure => _controller.CanConfigure;

#endregion

4.3 StateChanged Event Handling

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs:190-210

Notify the UI to update when state changes:

private void OnControllerStateChanged(object? sender, StateChangedEventArgs e)
{
// Notify UI of all state-dependent properties
System.Windows.Application.Current?.Dispatcher.Invoke(() =>
{
OnPropertyChanged(nameof(CurrentState));
OnPropertyChanged(nameof(StateDisplayText));
OnPropertyChanged(nameof(StateColor));
OnPropertyChanged(nameof(IsInError));
OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsPaused));

// Notify CanFire properties
OnPropertyChanged(nameof(CanStart));
OnPropertyChanged(nameof(CanPause));
OnPropertyChanged(nameof(CanResume));
OnPropertyChanged(nameof(CanStop));
OnPropertyChanged(nameof(CanReset));
OnPropertyChanged(nameof(CanAcknowledge));
OnPropertyChanged(nameof(CanConfigure));
});
}

4.4 XAML Binding Example

<!-- Use CanFire to control button IsEnabled -->
<Button Content="Start"
Command="{Binding StartCommand}"
IsEnabled="{Binding CanStart}" />

<Button Content="Pause"
Command="{Binding PauseCommand}"
IsEnabled="{Binding CanPause}" />

<Button Content="Stop"
Command="{Binding StopCommand}"
IsEnabled="{Binding CanStop}" />

<Button Content="Reset"
Command="{Binding ResetCommand}"
IsEnabled="{Binding CanReset}" />

<!-- Error acknowledge button only visible in Error state -->
<Button Content="Acknowledge Error"
Command="{Binding AcknowledgeCommand}"
Visibility="{Binding CanAcknowledge, Converter={StaticResource BoolToVisibility}}" />

5. Implementation Examples

5.1 DeviceConnectionStateMachine (Complete Example)

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs

This is a generic device connection state machine that can be used by all device types:

using Stateless;
using Stateless.Graph;

namespace Jope.SMB.Core.Devices;

/// <summary>
/// State machine for managing device connection states.
/// Provides unified connection management for all device types.
/// </summary>
public class DeviceConnectionStateMachine
{
private readonly StateMachine<ConnectionState, ConnectionTrigger> _machine;
private string? _lastErrorMessage;

public ConnectionState State => _machine.State;
public bool CanConnect => _machine.CanFire(ConnectionTrigger.Connect);
public bool CanDisconnect => _machine.CanFire(ConnectionTrigger.Disconnect);
public bool CanRetry => _machine.CanFire(ConnectionTrigger.Retry);
public string? LastErrorMessage => _lastErrorMessage;

public event EventHandler<ConnectionStateChangedEventArgs>? StateChanged;

public DeviceConnectionStateMachine()
{
_machine = new StateMachine<ConnectionState, ConnectionTrigger>(
ConnectionState.Disconnected);
ConfigureStateMachine();
}

public void Fire(ConnectionTrigger trigger)
{
if (!_machine.CanFire(trigger))
{
throw new InvalidOperationException(
$"Cannot fire trigger '{trigger}' from state '{_machine.State}'");
}
_machine.Fire(trigger);
}

public void Fire(ConnectionTrigger trigger, string? errorMessage)
{
if (trigger == ConnectionTrigger.Failure)
{
_lastErrorMessage = errorMessage;
}
Fire(trigger);
}

public bool TryFire(ConnectionTrigger trigger)
{
if (!_machine.CanFire(trigger))
return false;

_machine.Fire(trigger);
return true;
}

public string GetStateDiagram()
{
return UmlDotGraph.Format(_machine.GetInfo());
}

// ConfigureStateMachine - see previous section
}

5.2 SmbController State Machine Integration

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs

Integrating the state machine in a Controller:

public class SmbController : ISmbController
{
private readonly StateMachine<SmbControllerState, SmbTrigger> _machine;

public SmbController()
{
// Initialize state machine with initial state NotInitialized
_machine = new StateMachine<SmbControllerState, SmbTrigger>(
SmbControllerState.NotInitialized);
ConfigureStateMachine();
}

private void ConfigureStateMachine()
{
// NotInitialized state
_machine.Configure(SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Configure, SmbControllerState.Ready)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Ready state
_machine.Configure(SmbControllerState.Ready)
.Permit(SmbTrigger.Start, SmbControllerState.Running)
.PermitReentry(SmbTrigger.Configure)
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Running state
_machine.Configure(SmbControllerState.Running)
.Permit(SmbTrigger.Pause, SmbControllerState.Paused)
.Permit(SmbTrigger.Stop, SmbControllerState.Stopped)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Paused state
_machine.Configure(SmbControllerState.Paused)
.Permit(SmbTrigger.Resume, SmbControllerState.Running)
.Permit(SmbTrigger.Stop, SmbControllerState.Stopped)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Stopped state
_machine.Configure(SmbControllerState.Stopped)
.Permit(SmbTrigger.Start, SmbControllerState.Running)
.Permit(SmbTrigger.Configure, SmbControllerState.Ready)
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Error state
_machine.Configure(SmbControllerState.Error)
.Permit(SmbTrigger.Acknowledge, SmbControllerState.Ready)
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized);

// Register transition callback
_machine.OnTransitioned(OnStateMachineTransitioned);
}

// Operation methods use CanFire checks
public void Configure(SmbConfiguration configuration)
{
if (!CanConfigure)
throw new InvalidOperationException(
$"Cannot configure in state: {_machine.State}");

// Execute configuration logic...
_machine.Fire(SmbTrigger.Configure);
}
}

5.3 State Diagram

SmbController state machine transition diagram:

                         ┌─────────────┐
│NotInitialized│
└──────┬──────┘
│ Configure

┌──────────────────►┌─────┐◄──────────────────┐
│ │Ready│ │
│ └──┬──┘ │
│ │ Start │ Acknowledge
│ ┌─────────────┼─────────────┐ │
│ │ ▼ │ │
│ │ ┌───────┐ │ │
│ Reset │ Pause ◄─┤Running├─► Stop │ │
│ │ └───┬───┘ │ │
│ │ │ │ │
│ ▼ │ Resume ▼ │
│ ┌──────┐ │ ┌───────┐ │
│ │Paused├──────────┘ │Stopped├────┘
│ └──┬───┘ └───┬───┘
│ │ Stop │
│ └────────────────────────────┘
│ │
└────────────────┬───────────────────┘
│ Error (from any state)

┌─────┐
│Error│
└─────┘

6. Best Practices

6.1 Error Handling

Every state machine should have an Error state and a corresponding recovery mechanism:

// 1. Error state design
_machine.Configure(SmbControllerState.Error)
.Permit(SmbTrigger.Acknowledge, SmbControllerState.Ready) // Return to Ready after acknowledgement
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized); // Or full reset

// 2. Any state can transition to Error
_machine.Configure(SmbControllerState.Running)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// 3. Method to trigger Error
private void SetError(string message, Exception? exception = null)
{
_lastError = message;

if (_machine.CanFire(SmbTrigger.Error))
{
_machine.Fire(SmbTrigger.Error);
}

ErrorOccurred?.Invoke(this, new ErrorOccurredEventArgs
{
Message = message,
Exception = exception
});
}

6.2 Asynchronous Operations

Pattern for handling asynchronous operations:

public async Task StartAsync(CancellationToken cancellationToken = default)
{
// 1. Check CanFire first
if (!CanStart)
{
throw new InvalidOperationException(
$"Cannot start from state: {_machine.State}");
}

// 2. Execute async operations (before Fire)
await InitializeResourcesAsync(cancellationToken);

// 3. Trigger state transition
_machine.Fire(SmbTrigger.Start);

// 4. Async operations after state transition
if (_configuration.ShouldAutoAdvance)
{
StartControlLoop();
}
}

6.3 Thread Safety

If the state machine is accessed by multiple threads, locking is required:

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs:34-43

private readonly object _lock = new();

public SmbControllerState ControllerState
{
get
{
lock (_lock)
{
return _machine.State;
}
}
}

public void Configure(SmbConfiguration configuration)
{
lock (_lock)
{
if (!CanConfigure)
throw new InvalidOperationException(...);

// Configuration logic...
_machine.Fire(SmbTrigger.Configure);
}
}

6.4 Testing Approach

File Location: D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\tests\Jope.SMB.Core.Tests\Devices\DeviceConnectionStateMachineTests.cs

State Transition Tests

[Fact]
public void Fire_Connect_FromDisconnected_ShouldTransitionToConnecting()
{
var machine = new DeviceConnectionStateMachine();

machine.Fire(ConnectionTrigger.Connect);

Assert.Equal(ConnectionState.Connecting, machine.State);
}

[Fact]
public void Fire_Connect_FromConnecting_ShouldThrow()
{
var machine = new DeviceConnectionStateMachine();
machine.Fire(ConnectionTrigger.Connect);

Assert.Throws<InvalidOperationException>(() =>
machine.Fire(ConnectionTrigger.Connect));
}

CanFire Tests

[Fact]
public void InitialState_CanConnect_ShouldBeTrue()
{
var machine = new DeviceConnectionStateMachine();

Assert.True(machine.CanConnect);
}

[Fact]
public void Connected_CanConnect_ShouldBeFalse()
{
var machine = new DeviceConnectionStateMachine();
machine.Fire(ConnectionTrigger.Connect);
machine.Fire(ConnectionTrigger.Success);

Assert.False(machine.CanConnect);
}

Event Tests

[Fact]
public void StateChanged_ShouldBeRaisedOnTransition()
{
var machine = new DeviceConnectionStateMachine();
ConnectionStateChangedEventArgs? eventArgs = null;
machine.StateChanged += (s, e) => eventArgs = e;

machine.Fire(ConnectionTrigger.Connect);

Assert.NotNull(eventArgs);
Assert.Equal(ConnectionState.Disconnected, eventArgs.PreviousState);
Assert.Equal(ConnectionState.Connecting, eventArgs.CurrentState);
Assert.Equal(ConnectionTrigger.Connect, eventArgs.Trigger);
}

Full Cycle Tests

[Fact]
public void FullCycle_ConnectFailRetryConnect_ShouldWork()
{
var machine = new DeviceConnectionStateMachine();

machine.Fire(ConnectionTrigger.Connect);
machine.Fire(ConnectionTrigger.Failure, "First attempt failed");
Assert.Equal(ConnectionState.Error, machine.State);

machine.Fire(ConnectionTrigger.Retry);
Assert.Equal(ConnectionState.Connecting, machine.State);

machine.Fire(ConnectionTrigger.Success);
Assert.Equal(ConnectionState.Connected, machine.State);
}

7. References

7.1 Stateless Official Resources

IssueDescription
JOP-62Introduce Stateless state machine framework
JOP-63SmbController state machine refactoring
JOP-64Device connection state machine abstraction
JOP-65ViewModel CanFire binding
JOP-67WalBufferStateMachine (planned)

7.3 In-Project Reference Files

FileFull Path
ConnectionStateD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionState.cs
ConnectionTriggerD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionTrigger.cs
DeviceConnectionStateMachineD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs
SmbTriggerD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbTrigger.cs
SmbControllerStateD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\Models\SmbState.cs
SmbControllerD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs
SmbControlPanelViewModelD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs
TestsD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\tests\Jope.SMB.Core.Tests\Devices\DeviceConnectionStateMachineTests.cs

Version History

VersionDateChanges
1.02026-01-20Initial creation