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:
| Advantage | Description |
|---|---|
| Clear state transitions | Explicitly defines allowed state transitions, preventing illegal operations |
| Guard protection | Uses PermitIf to set conditional transitions, ensuring business logic correctness |
| UI integration friendly | CanFire property can be directly bound to MVVM Commands |
| Visualization support | Built-in DOT/UML diagram output for documentation |
| Lightweight, no dependencies | No 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
| Element | Convention | Example |
|---|---|---|
| State enum | {Feature}State | ConnectionState, SmbControllerState |
| Trigger enum | {Feature}Trigger | ConnectionTrigger, SmbTrigger |
| State machine class | {Feature}StateMachine | DeviceConnectionStateMachine |
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
- GitHub: https://github.com/dotnet-state-machine/stateless
- NuGet: https://www.nuget.org/packages/Stateless
7.2 Related Linear Issues
| Issue | Description |
|---|---|
| JOP-62 | Introduce Stateless state machine framework |
| JOP-63 | SmbController state machine refactoring |
| JOP-64 | Device connection state machine abstraction |
| JOP-65 | ViewModel CanFire binding |
| JOP-67 | WalBufferStateMachine (planned) |
7.3 In-Project Reference Files
| File | Full Path |
|---|---|
| ConnectionState | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionState.cs |
| ConnectionTrigger | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionTrigger.cs |
| DeviceConnectionStateMachine | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs |
| SmbTrigger | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbTrigger.cs |
| SmbControllerState | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\Models\SmbState.cs |
| SmbController | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs |
| SmbControlPanelViewModel | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs |
| Tests | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\tests\Jope.SMB.Core.Tests\Devices\DeviceConnectionStateMachineTests.cs |
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-01-20 | Initial creation |