In actual software development, especially industrial software, every device has complex states and functional requirements for switching between states. In such cases, managing states, state transitions, and function control under corresponding states becomes a very important issue. If not handled properly, these complicated states will become tangled like spaghetti, truly "cannot be cut, still messy in sorting." So how to solve this problem? Today, we will use a simple example to briefly illustrate how to use the Stateless component to manage and trigger states. This is for learning and sharing purposes only. Please point out any shortcomings.
What is the State Pattern?
Regarding the State Pattern and State Machine, as follows:
State Pattern: "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class." For this definition, it is a bit abstract. To understand it flexibly, it can be interpreted as: the state owner delegates behavior changes to the state object; the state owner itself only holds the state (it can also discard the state object), and the state object fulfills the change responsibility.
State Machine: "According to the specified state flowchart, based on the current executed action, change the current state to a new state under predefined conditions." A state machine has four elements: current state, condition, action, and next state. Among them, current state and condition are the "cause," and action and next state are the "effect."
- Current state - refers to the current state of the object
- Condition - when a condition is met, the current object triggers an action
- Action - the action performed after the condition is satisfied
- Next state - the new state of the object after the condition is satisfied. The next state is relative to the current state; once the next state is triggered, it becomes the current state
State Transition Diagram: "Often seen in UML modeling, used to describe all possible states of a specific object, as well as the transitions and changes between states caused by various events. It is also a prerequisite for configuring how the state machine behaves."
What is Stateless?
Stateless is a lightweight, high-performance state machine library based on .NET Standard. It can be used in both .NET Framework and .NET Core projects, making it easy to implement state transition logic.

Example Scenario Description
This article mainly uses the Stateless component to implement the management and triggering of a state as follows. There is a device software that has the following states:
- Idle state - indicates that there is currently no running task and it is in an idle state.
- Running state - indicates that it is currently working and is in a running state.
- Malfunction state - indicates that there is an abnormality and it is in a fault state.
- Recovery state - indicates that after repair, it has recovered from the fault and is in a recovery state.
It has the following triggers:
- Work - indicates starting work. It can enter from the Idle state or from the Recovery state, and the program will enter the Running state.
- Fatal - indicates encountering an exception. It can only enter from the Running state, and the program will enter the Malfunction state.
- Repair - indicates repairing the abnormality. It can only enter from the Malfunction state, and the program will enter the Recovery state.
- Finish - indicates completion. It can enter from the Running state or from the Recovery state, and the program will enter the Idle state.
The above states and triggers and their transitions are shown in the following diagram:

Create the Project
To demonstrate state changes, we create a WinForms application "Okcoder.Stateless.WinTool" and a common library "Okcoder.Stateless.Common", as shown below:

Install the Stateless Component
In Visual Studio 2022, you can install it via the NuGet Package Manager. Right-click on the solution, open the context menu, and click "Manage NuGet Packages for Solution". In the opened NuGet package for the solution, search for Stateless and install it. The current latest version is v5.18.0, as shown below:

Define States and Triggers
The state machine manages state changes and trigger events, so we define states and triggers as enum types. According to the example scenario description, define ToolState as follows:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// Tool State
/// </summary>
public enum ToolState
{
Idle=0, //Idle state
Running=1, //Running state
Malfunction = 2, // Fault state
Recovery=3, //Recovered state
}
}
Define ToolTrigger as follows:
namespace Okcoder.Stateless.Common
{
public enum ToolTrigger
{
Work = 0, // Start running
Fatal = 1, // Exception occurs
Finish = 2, // Complete
Repair=3, // Repair
}
}
Define Interfaces
Define the IToolStateService interface, which represents the capabilities of the state machine, such as triggering actions, state checking, exporting the state machine as DotGraph, etc., as follows:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// State machine interface
/// </summary>
public interface IToolStateService
{
/// <summary>
/// Initialize state
/// </summary>
void Init();
/// <summary>
/// Start working
/// </summary>
/// <returns></returns>
ToolResult Work();
/// <summary>
/// Encounter a fatal error
/// </summary>
/// <returns></returns>
ToolResult Fatal();
/// <summary>
/// Repair
/// </summary>
/// <returns></returns>
ToolResult Repair();
/// <summary>
/// Finish
/// </summary>
/// <returns></returns>
ToolResult Finish();
/// <summary>
/// Is it in Idle state
/// </summary>
/// <returns></returns>
bool IsIdleState();
/// <summary>
/// Is it in Running state
/// </summary>
/// <returns></returns>
bool IsRunningState();
/// <summary>
/// Is it in Malfunction state
/// </summary>
/// <returns></returns>
bool IsMalfunctionState();
/// <summary>
/// Is it in Recovery state
/// </summary>
/// <returns></returns>
bool IsRecoveryState();
/// <summary>
/// Get the current state
/// </summary>
/// <returns></returns>
ToolState GetToolState();
/// <summary>
/// Export state machine as DotGraph
/// </summary>
/// <param name="path"></param>
void ExportDotGraph(string path);
}
}
Define the device work interface IToolWorkService, which represents the device running work and operations, as follows:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// Device workflow interface
/// </summary>
public interface IToolWorkService
{
/// <summary>
/// Whether to create a fault
/// </summary>
bool IsMakeFatal { get; set; }
/// <summary>
/// Start working
/// </summary>
void DoWork();
/// <summary>
/// Repair
/// </summary>
void Repair();
}
}
Implement the Services
Implement the state machine service ToolStateService, which mainly uses the Stateless component to manage states and trigger actions for state transitions, as follows:
using Stateless;
using Stateless.Graph;
using System;
using System.IO;
using System.Text;
namespace Okcoder.Stateless.Common
{
/// <summary>
/// State machine service
/// </summary>
public class ToolStateService : IToolStateService
{
private Action<ToolState> toolAction;// Action
private StateMachine<ToolState, ToolTrigger> toolStateMachine;
public ToolStateService(Action<ToolState> toolAction)
{
// Define state machine
this.toolStateMachine = new StateMachine<ToolState, ToolTrigger>(ToolState.Idle);
// Configure allowed actions for each state
this.toolStateMachine.Configure(ToolState.Idle).Permit(ToolTrigger.Work, ToolState.Running);
this.toolStateMachine.Configure(ToolState.Running).Permit(ToolTrigger.Fatal, ToolState.Malfunction).Permit(ToolTrigger.Finish, ToolState.Idle);
this.toolStateMachine.Configure(ToolState.Malfunction).Permit(ToolTrigger.Repair, ToolState.Recovery);
this.toolStateMachine.Configure(ToolState.Recovery).Permit(ToolTrigger.Work, ToolState.Running).Permit(ToolTrigger.Finish, ToolState.Idle);
this.toolAction = toolAction;
}
/// <summary>
/// Initialize state
/// </summary>
public void Init()
{
this.toolAction(ToolState.Idle);
}
/// <summary>
/// Respond to trigger action
/// </summary>
/// <param name="trigger"></param>
private ToolResult Fire(ToolTrigger trigger)
{
ToolResult toolResult = new ToolResult();
try
{
if (!this.toolStateMachine.CanFire(trigger))
{
toolResult.IsOk = false;
toolResult.Desc = $"Current state is {this.toolStateMachine.State}, cannot perform {trigger} operation";
}
else
{
this.toolStateMachine.Fire(trigger);
toolResult.IsOk = true;
toolResult.Desc = "Ok";
DoAction();
}
}
catch (InvalidOperationException ex)
{
toolResult.IsOk = false;
toolResult.Desc = ex.Message;
}
return toolResult;
}
/// <summary>
/// Check if in state
/// </summary>
/// <param name="toolState"></param>
/// <returns></returns>
private bool IsInState(ToolState toolState)
{
return this.toolStateMachine.IsInState(toolState);
}
/// <summary>
/// Event notification
/// </summary>
private void DoAction()
{
if (this.toolAction != null)
{
this.toolAction(this.toolStateMachine.State);
}
}
/// <summary>
/// Start working
/// </summary>
public ToolResult Work()
{
return this.Fire(ToolTrigger.Work);
}
/// <summary>
/// Encounter a fatal error
/// </summary>
public ToolResult Fatal()
{
return this.Fire(ToolTrigger.Fatal);
}
/// <summary>
/// Repair
/// </summary>
public ToolResult Repair()
{
return this.Fire(ToolTrigger.Repair);
}
/// <summary>
/// Finish
/// </summary>
public ToolResult Finish()
{
return this.Fire(ToolTrigger.Finish);
}
/// <summary>
/// Is it in Idle state
/// </summary>
/// <returns></returns>
public bool IsIdleState()
{
return this.IsInState(ToolState.Idle);
}
/// <summary>
/// Is it in Running state
/// </summary>
/// <returns></returns>
public bool IsRunningState()
{
return this.IsInState(ToolState.Running);
}
/// <summary>
/// Is it in Malfunction state
/// </summary>
/// <returns></returns>
public bool IsMalfunctionState()
{
return this.IsInState(ToolState.Malfunction);
}
/// <summary>
/// Is it in Recovery state
/// </summary>
/// <returns></returns>
public bool IsRecoveryState()
{
return this.IsInState(ToolState.Recovery);
}
public ToolState GetToolState()
{
return this.toolStateMachine.State;
}
/// <summary>
/// Export state machine as DotGraph
/// </summary>
/// <param name="path"></param>
public void ExportDotGraph(string path)
{
var info = this.toolStateMachine.GetInfo();
string graph = UmlDotGraph.Format(info);
using (FileStream stream = File.OpenWrite(path))
{
using (StreamWriter writer = new StreamWriter(stream))
{
writer.Write(graph);
writer.Close();
}
stream.Close();
}
}
}
}
Among them, StateMachine is the core class of the state machine. It has two generic parameters representing ToolState and ToolTrigger. The constructor passes in the initial state. In this example, ToolState.Idle is passed in.
The StateMachine configures the allowed states through the Configure method of the object instance and configures the new states allowed to transition from the current state through the Permit method.
After configuration, the Fire method responds to the trigger action, and the IsInState method checks whether it is currently in the specified state.
In the constructor of the state machine service, a delegate method Action<string> is received to output the state to the UI layer. Of course, other decoupling methods such as event subscription and publishing can also be used.
The ToolResult return value indicates the result after the state machine execution. It is a class as follows:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// Tool execution result
/// </summary>
public class ToolResult
{
public bool IsOk { get; set; }
public string Desc { get; set; }
}
}
Implement the work service ToolWorkService, mainly to simulate device tool operation. When starting work, output text to the UI page. If there is an exception, stop; if no exception, the run completes, as follows:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// Device work service
/// </summary>
public class ToolWorkService:IToolWorkService
{
private Action<string> _progress;
private IToolStateService _toolStateService;
private bool _isMakeFatal=false;
public bool IsMakeFatal
{
get { return _isMakeFatal; }
set { _isMakeFatal = value; }
}
public ToolWorkService(IToolStateService toolStateService, Action<string> progress,bool isMakeFatal=false)
{
_progress = progress;
_toolStateService = toolStateService;
_isMakeFatal = isMakeFatal;
}
public void DoWork()
{
BeginWork();
Work();
Finish();
}
public void Repair()
{
var result = this._toolStateService.Repair();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} The fault has been repaired");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} The fault has not been repaired");
}
}
/// <summary>
/// Start working
/// </summary>
private void BeginWork()
{
var result = this._toolStateService.Work();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Started working");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Failed to start working");
}
}
/// <summary>
/// Work process
/// </summary>
private void Work()
{
for (int i = 0; i < 30; i++)
{
if (_isMakeFatal)
{
if (i > 10)
{
MarkFatal();// Run for a while, then report a fault
break;
}
}
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Currently working - {i}");
Thread.Sleep(300);
}
}
/// <summary>
/// Complete work
/// </summary>
private void Finish()
{
var result = this._toolStateService.Finish();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Work completed");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {result.Desc}");
}
}
private void MarkFatal()
{
var result = this._toolStateService.Fatal();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Oops, an error occurred?");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Failed to create a fault");
}
}
/// <summary>
/// Output information
/// </summary>
/// <param name="msg"></param>
private void OutPutInfo(string msg)
{
if (_progress != null)
{
_progress(msg);
}
}
}
}
The constructor receives a delegate Action<string> for outputting information to the UI. It can also be in other forms.
UI Call
In the Okcoder.Stateless.WinTool project, create the FrmMain form. In the constructor, define object instances of IToolStateService and IToolWorkService, and initialize them in the Load method.
When the user clicks Work, start calling the Work method of IToolWorkService. During execution, information will be output to the UI page.
There is a checkbox on the UI page to set whether to trigger an exception, simulating a scenario with an exception. If an exception occurs, click the Repair button to repair.
During execution of each action, different state changes can be observed. As shown below:
using Okcoder.Stateless.Common;
namespace Okcoder.Stateless.WinTool
{
public partial class FrmMain : Form
{
private IToolStateService toolStateService;
private IToolWorkService toolWorkService;
public FrmMain()
{
InitializeComponent();
this.toolStateService = new ToolStateService(ToolStateAction);
this.toolWorkService = new ToolWorkService(toolStateService, ToolWorkProcessAction, this.chkFatal.Checked);
}
private void FrmMain_Load(object sender, EventArgs e)
{
this.toolStateService.Init();
}
/// <summary>
/// Start working
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnWork_Click(object sender, EventArgs e)
{
this.txtInfo.Text = string.Empty;
this.toolWorkService.IsMakeFatal = this.chkFatal.Checked;
Task.Run(() =>
{
this.toolWorkService.DoWork();
});
}
/// <summary>
/// Repair
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRepair_Click(object sender, EventArgs e)
{
this.toolWorkService.Repair();
}
/// <summary>
/// Export state machine DotGraph file
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnExport_Click(object sender, EventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.DefaultExt = "txt";
saveFileDialog.Filter = "txt files|*.txt";
saveFileDialog.FileName = "Okcoder.txt";
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
var filePath = saveFileDialog.FileName;
this.toolStateService.ExportDotGraph(filePath);
MessageBox.Show("Export successful");
}
}
private void ToolStateAction(ToolState toolState)
{
this.Invoke(() =>
{
this.lblState.Text = toolState.ToString();
switch (toolState)
{
case ToolState.Idle:
case ToolState.Recovery:
this.btnWork.Enabled = true;
this.btnRepair.Enabled = false;
this.lblState.ForeColor = Color.Black;
break;
case ToolState.Running:
this.btnWork.Enabled = false;
this.btnRepair.Enabled = false;
this.lblState.ForeColor = Color.Goldenrod;
break;
case ToolState.Malfunction:
this.btnWork.Enabled = false;
this.btnRepair.Enabled = true;
this.lblState.ForeColor = Color.Red;
break;
}
this.Refresh();
});
}
private void ToolWorkProcessAction(string msg)
{
this.Invoke(() =>
{
this.txtInfo.AppendText(msg + "\r\n");
});
}
}
}
Example Demo
After the above steps, run the program and click the Work button. When there is no fault by default, it is as shown below:

When the fault checkbox is checked, simulating a fault, it is as shown below:

The above is all about "Recommend a High-Performance State Machine Management Solution." For more details, please refer to the official documentation. Hopefully, we can learn and progress together.
Learn programming, start by following [Old Code Knows the Way], and we will share more articles!!!