diff --git a/.editorconfig b/.editorconfig
index 26350ca..fe2d82f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -197,7 +197,7 @@ csharp_preferred_modifier_order = public,private,protected,internal,static,exter
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
# file_header_template = unset
-file_header_template = Copyright Xeno Innovations, Inc. 2025\nSee the LICENSE file in the project root for more information.
+file_header_template = Copyright Xeno Innovations, Inc. 2026\nSee the LICENSE file in the project root for more information.
# this. and Me. preferences
dotnet_style_qualification_for_event = false
diff --git a/Directory.Build.props b/Directory.Build.props
index 0805d27..3a3a01b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
latest
True
- 2.3.1
+ 2.4.1
../../output/$(MSBuildProjectName)
true
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2eb4914..b6c8bc4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,6 +4,7 @@
+
diff --git a/Lite.StateMachine.slnx b/Lite.StateMachine.slnx
index 3e7a3ea..a57be99 100644
--- a/Lite.StateMachine.slnx
+++ b/Lite.StateMachine.slnx
@@ -1,7 +1,12 @@
-
-
+
+
+
+
+
+
+
diff --git a/docs/icon-128x128.png b/docs/icon-128x128.png
index 64db45f..165574b 100644
Binary files a/docs/icon-128x128.png and b/docs/icon-128x128.png differ
diff --git a/docs/Sample-ExportUml-Level1-Legend.svg b/docs/images/Sample-ExportUml-Level1-Legend.svg
similarity index 100%
rename from docs/Sample-ExportUml-Level1-Legend.svg
rename to docs/images/Sample-ExportUml-Level1-Legend.svg
diff --git a/docs/Sample-ExportUml-Level3.svg b/docs/images/Sample-ExportUml-Level3.svg
similarity index 100%
rename from docs/Sample-ExportUml-Level3.svg
rename to docs/images/Sample-ExportUml-Level3.svg
diff --git a/docs/icon-128x128.pdn b/docs/images/icon-128x128.pdn
similarity index 100%
rename from docs/icon-128x128.pdn
rename to docs/images/icon-128x128.pdn
diff --git a/docs/images/icon-128x128_MK2.png b/docs/images/icon-128x128_MK2.png
new file mode 100644
index 0000000..64db45f
Binary files /dev/null and b/docs/images/icon-128x128_MK2.png differ
diff --git a/docs/icon-poc-1.jpg b/docs/images/icon-poc-1.jpg
similarity index 100%
rename from docs/icon-poc-1.jpg
rename to docs/images/icon-poc-1.jpg
diff --git a/docs/icon-poc-2.jpg b/docs/images/icon-poc-2.jpg
similarity index 100%
rename from docs/icon-poc-2.jpg
rename to docs/images/icon-poc-2.jpg
diff --git a/docs/icon-poc-3.png b/docs/images/icon-poc-3.png
similarity index 100%
rename from docs/icon-poc-3.png
rename to docs/images/icon-poc-3.png
diff --git a/docs/icon.pdn b/docs/images/icon.pdn
similarity index 100%
rename from docs/icon.pdn
rename to docs/images/icon.pdn
diff --git a/docs/images/nuget-state-machine-icon-badge.png b/docs/images/nuget-state-machine-icon-badge.png
new file mode 100644
index 0000000..697a622
Binary files /dev/null and b/docs/images/nuget-state-machine-icon-badge.png differ
diff --git a/docs/images/nuget-state-machine-icon-chip.png b/docs/images/nuget-state-machine-icon-chip.png
new file mode 100644
index 0000000..165574b
Binary files /dev/null and b/docs/images/nuget-state-machine-icon-chip.png differ
diff --git a/docs/images/nuget-state-machine-icon-isometric.png b/docs/images/nuget-state-machine-icon-isometric.png
new file mode 100644
index 0000000..e412ab5
Binary files /dev/null and b/docs/images/nuget-state-machine-icon-isometric.png differ
diff --git a/docs/images/nuget-state-machine-icon-minimal.png b/docs/images/nuget-state-machine-icon-minimal.png
new file mode 100644
index 0000000..9c7bed0
Binary files /dev/null and b/docs/images/nuget-state-machine-icon-minimal.png differ
diff --git a/docs/images/nuget-state-machine-icon.png b/docs/images/nuget-state-machine-icon.png
new file mode 100644
index 0000000..0c040a7
Binary files /dev/null and b/docs/images/nuget-state-machine-icon.png differ
diff --git a/samples/Sample.Basics/DiStates/DemoDiMachine.cs b/docs/old-concepts/DiStates/DemoDiMachine.cs
similarity index 100%
rename from samples/Sample.Basics/DiStates/DemoDiMachine.cs
rename to docs/old-concepts/DiStates/DemoDiMachine.cs
diff --git a/samples/Sample.Basics/DiStates/DemoDiStates.cs b/docs/old-concepts/DiStates/DemoDiStates.cs
similarity index 100%
rename from samples/Sample.Basics/DiStates/DemoDiStates.cs
rename to docs/old-concepts/DiStates/DemoDiStates.cs
diff --git a/samples/Sample.Basics/DiStates/DiStateBase.cs b/docs/old-concepts/DiStates/DiStateBase.cs
similarity index 100%
rename from samples/Sample.Basics/DiStates/DiStateBase.cs
rename to docs/old-concepts/DiStates/DiStateBase.cs
diff --git a/samples/Sample.Mk4/Sample4AMachine.cs b/docs/old-concepts/Sample4AMachine.cs
similarity index 100%
rename from samples/Sample.Mk4/Sample4AMachine.cs
rename to docs/old-concepts/Sample4AMachine.cs
diff --git a/samples/Sample.Mk4/Sample4BMachine.cs b/docs/old-concepts/Sample4BMachine.cs
similarity index 100%
rename from samples/Sample.Mk4/Sample4BMachine.cs
rename to docs/old-concepts/Sample4BMachine.cs
diff --git a/readme.md b/readme.md
index 5aaa6b4..bfe8b7f 100644
--- a/readme.md
+++ b/readme.md
@@ -20,6 +20,60 @@ The Lite State Machine is designed for vertical scaling. Meaning, it can be used
|-|-|-|
| Lite.StateMachine | [](https://www.nuget.org/packages/Lite.StateMachine/) | [](https://www.nuget.org/packages/Lite.StateMachine/)
+## Benchmarks
+
+Not only is [Lite.StateMachine](https://github.com/SuessLabs/Lite.StateMachine) smaller, easier to read, manage, and maintain, _**it faster too**_!
+
+The following table is an output of local [benchmark results](https://github.com/DamianSuess/Lite.StateMachine.Benchmarks) using state-transition operations across multiple states:
+
+| Method | Version | Mean | Allocated |
+|-|-|-|-|
+| **Lite.StateMachine** | v2.3.0 | **10.17 us** | **8.02 KB** |
+| Stateless | v5.20.1 | 10.72 us | 10.62 KB |
+
+_Lite.StateMachine is the fastest and lowest allocation_
+
+
+## Features
+
+_Thread safe and most customizations can be set on-the-fly!_
+
+* AOT Friendly - Ahead-of-Time compilation, _No Reflection, no Linq, etc._
+* Passing parameters between state transitions via `Context`
+* Dependency Injection (DI) friendly
+* Asynchronous states
+* Types of States:
+ * **Basic Linear State** (`BaseState`)
+ * **Composite** States (`CompositeState`)
+ * Hieratical / Nested Sub-states
+ * Similar to Actor/Director model
+ * **Command States** with optional Timeout (`CommandState`)
+ * Uses internal Event Aggregator for sending/receiving messages
+ * Allows users to hook to external messaging services (TCP/IP, RabbitMQ, DBus, etc.)
+* State Transition Triggers:
+ * Transitions are triggered by setting the context's next state result:
+ * On Success: `context.NextState(Result.Ok);`
+ * On Error: `context.NextState(Result.Error);`
+ * On Failure: : `context.NextState(Result.Failure);`
+* State Handlers:
+ * `OnEntering` - Initial entry of the state
+ * `OnEnter` - Resting (idle) place for state.
+ * `OnExit` - (Optional) Thrown during transitioning. Used for housekeeping or exiting activity.
+ * `OnMessage` (Optional)
+ * Must ensure that code has exited `OnMessage` before going to the next state.
+ * `OnTimeout` - (Optional) Thrown when the state is auto-transitioning due to timeout exceeded
+* Transition has knowledge of the `PreviousState`, `CurrentStateId`, and `NextState`
+* Shared **Context**:
+ * `Parameters` - _For passing data between states._
+ * `Errors` - _For passing error information between states._
+ * `CurrentStateId`
+ * `NextStates`
+ * `EventAggregator`
+ * `LastChildResult`, `LastChildStateId` _(for composite states)_
+* Customizable (on-the-fly):
+ * State Timeout (_per state or default for all states_)
+ * Overridable Next States! `OnSuccess`, `OnError`, or `OnFailure`
+
## Usage
Create a _state machine_ by defining the states, transitions, and shared context.
@@ -28,6 +82,9 @@ You can define the state machine using either the fluent design pattern or stand
### Basic State
+
+The basic state exapmle transitions from `State1 -> State2 -> State3`.
+
```cs
// That's it! Just create the state machine, register states, and run it.
var machine = await new StateMachine()
@@ -57,7 +114,7 @@ public class BasicState1() : BaseState
{
public async Task OnEnter(Context context)
{
- await Task.Yield(); // Some async work here...
+ await Task.Yield(); // Your async work here...
context.NextState(Result.Ok);
}
}
@@ -66,8 +123,9 @@ public class BasicState2() : BaseState
{
public Task OnEnter(Context context)
{
+ // Notice, we did not async/await this method
context.NextState(Result.Ok);
- return Task.CompletedTask; // Notice, we did not async/await this method
+ return Task.CompletedTask;
}
}
@@ -91,6 +149,8 @@ var uml = machine.ExportUml(includeSubmachines: true);
### Composite States
+The following uses the fluent design pattern style, stacking the `.RegisterXXX(...)` methonds ontop of each other with the `RunAsync(...)` method occurring at the end.
+
```cs
using Lite.StateMachine;
@@ -183,30 +243,4 @@ public class Composite_State3() : BaseState
}
```
-## Features
-
-* AOT Friendly - _No Reflection, no Linq, etc._
-* Passing parameters between state transitions via `Context`
-* Types of States
- * **Basic Linear State** (`BaseState`)
- * **Composite** States (`CompositeState`)
- * Hieratical / Nested Sub-states
- * Similar to Actor/Director model
- * **Command States** with optional Timeout (`CommandState`)
- * Uses internal Event Aggregator for sending/receiving messages
- * Allows users to hook to external messaging services (TCP/IP, RabbitMQ, DBus, etc.)
-* State Transition Triggers
- * Transitions are triggered by setting the context's next state result:
- * On Success: `context.NextState(Result.Ok);`
- * On Error: `context.NextState(Result.Error);`
- * On Failure: : `context.NextState(Result.Failure);`
-* State Handlers
- * `OnEntering` - Initial entry of the state
- * `OnEnter` - Resting (idle) place for state.
- * `OnExit` - (Optional) Thrown during transitioning. Used for housekeeping or exiting activity.
- * `OnMessage` (Optional)
- * Must ensure that code has exited `OnMessage` before going to the next state.
- * `OnTimeout` - (Optional) Thrown when the state is auto-transitioning due to timeout exceeded
-* Transition has knowledge of the `PreviousState` and `NextState`
-
## References
diff --git a/samples/Sample.Mk4/Program.cs b/samples/Sample.Mk4/Program.cs
deleted file mode 100644
index 57986ff..0000000
--- a/samples/Sample.Mk4/Program.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright Xeno Innovations, Inc. 2025
-// See the LICENSE file in the project root for more information.
-
-namespace Sample.Mk4a;
-
-internal class Program
-{
- private static void Main()
- {
- ////Mk4.SampleA.TestApp.Run();
- ////
- ////Mk4.SampleB.TestApp.Run();
- }
-}
diff --git a/samples/Sample01.BasicStates/Program.cs b/samples/Sample01.BasicStates/Program.cs
new file mode 100644
index 0000000..c6445e5
--- /dev/null
+++ b/samples/Sample01.BasicStates/Program.cs
@@ -0,0 +1,27 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+
+namespace Sample01.BasicStates;
+
+internal class Program
+{
+ private static async Task Main(string[] args)
+ {
+ Console.WriteLine("Regular State Sync: Starting...");
+ SampleBasic.BasicStateMachine.Run();
+ Console.WriteLine("Regular State Sync: DONE!");
+
+ Console.WriteLine("\n\nRegular State Async: Starting...");
+ await SampleBasic.BasicStateMachine.RunAsync();
+ Console.WriteLine("Regular State Async: DONE!");
+
+ Console.WriteLine("\n\n-=-=-=-=-=-\n\n");
+
+ Console.WriteLine("Composite Sync: Starting...");
+ SampleComposite.CompositeStateMachine.Run();
+ Console.WriteLine("Composite Sync: DONE!");
+ }
+}
diff --git a/samples/Sample.Mk4/Sample.Mk4.csproj b/samples/Sample01.BasicStates/Sample01.BasicStates.csproj
similarity index 98%
rename from samples/Sample.Mk4/Sample.Mk4.csproj
rename to samples/Sample01.BasicStates/Sample01.BasicStates.csproj
index 0afa75f..68b478d 100644
--- a/samples/Sample.Mk4/Sample.Mk4.csproj
+++ b/samples/Sample01.BasicStates/Sample01.BasicStates.csproj
@@ -5,8 +5,9 @@
net10.0
+
-
+
diff --git a/samples/Sample01.BasicStates/SampleBasic/BasicStateMachine.cs b/samples/Sample01.BasicStates/SampleBasic/BasicStateMachine.cs
new file mode 100644
index 0000000..ecdfa8a
--- /dev/null
+++ b/samples/Sample01.BasicStates/SampleBasic/BasicStateMachine.cs
@@ -0,0 +1,126 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using Lite.StateMachine;
+
+namespace Sample01.BasicStates.SampleBasic;
+
+/// State definitions.
+public enum BasicStateId
+{
+ State1,
+ State2,
+ State3,
+}
+
+public class BasicStateMachine
+{
+ /// Example synchronous run method.
+ public static void Run()
+ {
+ var machine = new StateMachine();
+ machine.RegisterState(BasicStateId.State1, BasicStateId.State2);
+ machine.RegisterState(BasicStateId.State2, BasicStateId.State3);
+ machine.RegisterState(BasicStateId.State3);
+
+ // Non-async Start your engine!
+ var task = machine.RunAsync(BasicStateId.State1);
+ task.GetAwaiter().GetResult();
+ }
+
+ /// Example asynchronous run method.
+ /// Task.
+ public static async Task RunAsync()
+ {
+ var machine = new StateMachine();
+ machine.RegisterState(BasicStateId.State1, BasicStateId.State2);
+ machine.RegisterState(BasicStateId.State2, BasicStateId.State3);
+ machine.RegisterState(BasicStateId.State3);
+
+ // Async Example!
+ await machine.RunAsync(BasicStateId.State1);
+ }
+}
+
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "ignore")]
+public class State1 : IState
+{
+ public Task OnEntering(Context context)
+ {
+ Console.WriteLine($"[BasicState1][OnEntering]'");
+ return Task.CompletedTask;
+ }
+
+ public Task OnEnter(Context context)
+ {
+ // Set success from OnEnter to transition to the next state, State2.
+ context.NextState(Result.Success);
+ Console.WriteLine($"[BasicState1][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'");
+ Console.WriteLine($"[BasicState1][OnEnter].OnError goto: '{context.NextStates.OnError}'");
+ Console.WriteLine($"[BasicState1][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'");
+
+ return Task.CompletedTask;
+ }
+
+ public Task OnExit(Context context)
+ {
+ Console.WriteLine($"[BasicState1][OnExit]'");
+ return Task.CompletedTask;
+ }
+}
+
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "ignore")]
+public class State2 : IState
+{
+ public Task OnEntering(Context context)
+ {
+ Console.WriteLine($"[BasicState2][OnEntering]'");
+ return Task.CompletedTask;
+ }
+
+ public Task OnEnter(Context context)
+ {
+ // Set success from OnEnter to transition to the next state, State2.
+ context.NextState(Result.Success);
+ Console.WriteLine($"[BasicState2][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'");
+ Console.WriteLine($"[BasicState2][OnEnter].OnError goto: '{context.NextStates.OnError}'");
+ Console.WriteLine($"[BasicState2][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'");
+
+ return Task.CompletedTask;
+ }
+
+ public Task OnExit(Context context)
+ {
+ Console.WriteLine($"[BasicState2][OnExit]'");
+ return Task.CompletedTask;
+ }
+}
+
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "ignore")]
+public class State3 : IState
+{
+ public Task OnEntering(Context context)
+ {
+ Console.WriteLine($"[BasicState3][OnEntering]'");
+ return Task.CompletedTask;
+ }
+
+ public Task OnEnter(Context context)
+ {
+ // Set success from OnEnter to transition to the next state, State2.
+ context.NextState(Result.Success);
+ Console.WriteLine($"[BasicState3][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'");
+ Console.WriteLine($"[BasicState3][OnEnter].OnError goto: '{context.NextStates.OnError}'");
+ Console.WriteLine($"[BasicState3][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'");
+
+ return Task.CompletedTask;
+ }
+
+ public Task OnExit(Context context)
+ {
+ Console.WriteLine($"[BasicState3][OnExit]'");
+ return Task.CompletedTask;
+ }
+}
diff --git a/samples/Sample01.BasicStates/SampleComposite/CompositeStateMachine.cs b/samples/Sample01.BasicStates/SampleComposite/CompositeStateMachine.cs
new file mode 100644
index 0000000..0ea8ca2
--- /dev/null
+++ b/samples/Sample01.BasicStates/SampleComposite/CompositeStateMachine.cs
@@ -0,0 +1,220 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using Lite.StateMachine;
+
+namespace Sample01.BasicStates.SampleComposite;
+
+public enum CompositeL3
+{
+ State1,
+ State2,
+ State2_Sub1,
+ State2_Sub2,
+ State2_Sub2_Sub1,
+ State2_Sub2_Sub2,
+ State2_Sub2_Sub3,
+ State2_Sub3,
+ State3,
+}
+
+public class CompositeStateMachine
+{
+ /// Example synchronous run method.
+ public static void Run()
+ {
+ // Non-async Start your engine!
+ var machine = GenerateStateMachineL3(new StateMachine());
+ var task = machine.RunAsync(CompositeL3.State1);
+ task.GetAwaiter().GetResult();
+ }
+
+ /// Example asynchronous run method.
+ /// Task.
+ public static async Task RunAsync()
+ {
+ // Async Example!
+ var machine = GenerateStateMachineL3(new StateMachine());
+ await machine.RunAsync(CompositeL3.State1);
+ }
+
+ private static StateMachine GenerateStateMachineL3(StateMachine machine)
+ {
+ machine
+ .RegisterState(CompositeL3.State1, CompositeL3.State2)
+ .RegisterComposite(CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub1, onSuccess: CompositeL3.State3)
+ .RegisterSubState(CompositeL3.State2_Sub1, parentStateId: CompositeL3.State2, onSuccess: CompositeL3.State2_Sub2)
+ .RegisterSubComposite(CompositeL3.State2_Sub2, parentStateId: CompositeL3.State2, initialChildStateId: CompositeL3.State2_Sub2_Sub1, onSuccess: CompositeL3.State2_Sub3)
+ .RegisterSubState(CompositeL3.State2_Sub2_Sub1, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub2)
+ .RegisterSubState(CompositeL3.State2_Sub2_Sub2, parentStateId: CompositeL3.State2_Sub2, onSuccess: CompositeL3.State2_Sub2_Sub3)
+ .RegisterSubState(CompositeL3.State2_Sub2_Sub3, parentStateId: CompositeL3.State2_Sub2, onSuccess: null)
+ .RegisterSubState(CompositeL3.State2_Sub3, parentStateId: CompositeL3.State2, onSuccess: null)
+ .RegisterState(CompositeL3.State3, onSuccess: null);
+
+ return machine;
+ }
+}
+
+#pragma warning disable SA1124 // Do not use regions
+#pragma warning disable SA1649 // File name should match first type name
+#pragma warning disable SA1402 // File may only contain a single type
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+
+public class State1()
+ : StateBase()
+{
+ public override Task OnEnter(Context context)
+ {
+ Console.WriteLine($"[State1][OnEnter]");
+ return base.OnEnter(context);
+ }
+}
+
+/// Level-1: Composite.
+public class State2()
+ : StateBase()
+{
+ #region CodeMaid - DoNotReorder
+
+ public override Task OnEntering(Context context)
+ {
+ Console.WriteLine($"[State2][OnEntering]");
+ return base.OnEntering(context);
+ }
+
+ #endregion CodeMaid - DoNotReorder
+
+ public override Task OnEnter(Context context)
+ {
+ // NOTE:
+ // We're a parent composite state. The 'context.NextState' is
+ // not used here as we will call it in the OnExit after all child
+ // states have completed.
+ Console.WriteLine($"[State2][OnEnter]**");
+ return Task.CompletedTask;
+ }
+
+ public override Task OnExit(Context context)
+ {
+ Console.WriteLine($"[State2][OnExit]**");
+
+ // NOTE:
+ // As this is a parent Composite state, we MUST call NextState to trigger
+ // the parent state to move to the next state to signify that the child states have completed.
+ context.NextState(Result.Success);
+ return base.OnExit(context);
+ }
+}
+
+/// Sublevel-2: State.
+public class State2_Sub1()
+ : StateBase()
+{
+ public override Task OnEnter(Context context)
+ {
+ Console.WriteLine($"[State2_Sub1][OnEnter]");
+ return base.OnEnter(context);
+ }
+}
+
+/// Sublevel-2: Composite.
+public class State2_Sub2()
+ : StateBase()
+{
+ #region CodeMaid - DoNotReorder
+
+ public override Task OnEntering(Context context)
+ {
+ Console.WriteLine($"[State2_Sub2][OnEntering]");
+ return base.OnEntering(context);
+ }
+
+ #endregion CodeMaid - DoNotReorder
+
+ public override Task OnEnter(Context context)
+ {
+ Console.WriteLine($"[State2_Sub2][OnEnter]**");
+ Console.WriteLine($"[State2_Sub2][OnEnter] CurrentStateId: {context.CurrentStateId} ({CompositeL3.State2_Sub2})");
+ Console.WriteLine($"[State2_Sub2][OnEnter] PreviousStateId: {context.PreviousStateId} ({CompositeL3.State2_Sub1})");
+ Console.WriteLine($"[State2_Sub2][OnEnter] LastChildStateId: {context.LastChildStateId} ({CompositeL3.State2_Sub2_Sub3})");
+
+ // NOTE:
+ // We're a parent composite state. The 'context.NextState' is
+ // not used here as we will call it in the OnExit after all child
+ // states have completed.
+ ////return base.OnEnter(context);
+ return Task.CompletedTask;
+ }
+
+ public override Task OnExit(Context context)
+ {
+ Console.WriteLine($"[State2_Sub2][OnExit]");
+ Console.WriteLine($"[State2_Sub2][OnExit] CurrentStateId: {context.CurrentStateId} ({CompositeL3.State2_Sub2})");
+ Console.WriteLine($"[State2_Sub2][OnExit] PreviousStateId: {context.PreviousStateId} ({CompositeL3.State2_Sub1})");
+ Console.WriteLine($"[State2_Sub2][OnExit] LastChildStateId: {context.LastChildStateId} ({CompositeL3.State2_Sub2_Sub3})");
+
+ // NOTE:
+ // As this is a parent Composite state, we MUST call NextState to trigger
+ // the parent state to move to the next state to signify that the child states have completed.
+ context.NextState(Result.Success);
+ return base.OnExit(context);
+ }
+}
+
+/// Sublevel-3: State.
+public class State2_Sub2_Sub1()
+ : StateBase()
+{
+ public override Task OnEnter(Context context)
+ {
+ Console.WriteLine($"[State2_Sub2_Sub1][OnEnter] (success)");
+ return base.OnEnter(context);
+ }
+}
+
+/// Sublevel-3: State.
+/// NOTE: We are auto-succeeding and not populating OnEnter.
+public class State2_Sub2_Sub2()
+ : StateBase()
+{
+}
+
+/// Sublevel-3: Last State.
+public class State2_Sub2_Sub3()
+ : StateBase()
+{
+ public override Task OnEnter(Context context)
+ {
+ Console.WriteLine($"[State2_Sub2_Sub3][OnEnter] (Success)");
+ return base.OnEnter(context);
+ }
+}
+
+/// Sublevel-2: Last State.
+public class State2_Sub3()
+ : StateBase()
+{
+ public override Task OnEnter(Context context)
+ {
+ Console.WriteLine($"[State2_Sub3][OnEnter]");
+ return base.OnEnter(context);
+ }
+}
+
+/// Make sure not child-created context is there.
+public class State3()
+ : StateBase()
+{
+ public override Task OnEnter(Context context)
+ {
+ Console.WriteLine($"[State3][OnEnter]");
+ return base.OnEnter(context);
+ }
+}
+
+#pragma warning restore IDE0130 // Namespace does not match folder structure
+#pragma warning restore SA1649 // File name should match first type name
+#pragma warning restore SA1402 // File may only contain a single type
+#pragma warning restore SA1124 // Do not use regions
diff --git a/samples/Sample01.BasicStates/SampleComposite/StateBase.cs b/samples/Sample01.BasicStates/SampleComposite/StateBase.cs
new file mode 100644
index 0000000..b1e9bee
--- /dev/null
+++ b/samples/Sample01.BasicStates/SampleComposite/StateBase.cs
@@ -0,0 +1,50 @@
+// Copyright Xeno Innovations, Inc. 2025
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using Lite.StateMachine;
+
+namespace Sample01.BasicStates.SampleComposite;
+
+///
+/// This is a base state class that can be used for all states in the sample.
+/// It provides default implementations of the IState methods, which can be
+/// overridden by derived classes as needed. This allows for code reuse and
+/// consistency across all states in the state machine.
+///
+/// Note, that if 'OnEnter' is not provided, it will AUTO-SUCCEED and transition
+/// to the next state (if any). This is a convenient default behavior for states
+/// that do not have any specific logic to execute upon entering, but it can be
+/// overridden if you need to perform some actions before transitioning to the
+/// next state.
+///
+/// The type of the state class.
+/// The type of the state identifier.
+public class StateBase : IState
+ where TStateId : struct, Enum
+{
+ #region Suppress CodeMaid Method Sorting
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "ignore")]
+ public virtual Task OnEntering(Context context)
+ {
+ ////Console.WriteLine("[StateBase][OnEntering]");
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
+ public virtual Task OnEnter(Context context)
+ {
+ ////Console.WriteLine("[StateBase][OnEnter]");
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+
+ public virtual Task OnExit(Context context)
+ {
+ ////Console.WriteLine("[StateBase][OnExit]");
+ return Task.CompletedTask;
+ }
+}
diff --git a/samples/Sample01.BasicStates/readme.md b/samples/Sample01.BasicStates/readme.md
new file mode 100644
index 0000000..24ea458
--- /dev/null
+++ b/samples/Sample01.BasicStates/readme.md
@@ -0,0 +1,38 @@
+# Sample 01 - Basic States
+
+This project demonstrates the basics of Lite.StateMachine using a
+.NET application, including how to manage and transition between different
+states such as initialization, running, and shutdown.
+
+It serves as a foundational example for understanding the lifecycle of a
+state-based application and how to handle state management effectively.
+
+There are 2 applications in this sample:
+
+* **Basic State Machine** - Flat state machine with no nested states, demonstrating simple state transitions.
+ * NOTE: _**You MUST**_ handle the `context.NextState` property during the `OnEnter` transition to signify that the state has completed successfully.
+* **Composite State Machine** - More complex state machine with nested states, showcasing hierarchical state management.
+ * NOTE: _**You MUST**_ handle the `context.NextState` property during the `OnExit` transition to signify that the child states have completed successfully.
+
+## Basic State Machine
+
+The file `BasicStateMachine.cs` contains the implementation of a simple state
+machine with three states: `State1`, `State2`, and `State3` for verbosity.
+
+Each of the states will be fully implemented with `OnEntering`, `OnEnter`, and `OnExit` transitions.
+
+## Composite State Machine
+
+The file `CompositeStateMachine.cs` contains a more complex state machine that includes nested states.
+This example demonstrates how to manage hierarchical states, where a parent state can contain multiple
+child states, allowing for more complex behavior and transitions.
+
+### Handling 'context.NextState'
+
+NOTE:
+
+Composite states can have multiple child states, MUST handle the `context.NextState` property
+during the `OnExit` transition to signify that the child states have completed successfully.
+
+This is a crucial step for _Composite States_ and differs from a regular "State" who performs
+the `context.NextState` assignment during the `OnEnter` transition.
diff --git a/samples/Sample.Basics/Models/ParameterType.cs b/samples/Sample02.PassingParams/Models/ParameterType.cs
similarity index 82%
rename from samples/Sample.Basics/Models/ParameterType.cs
rename to samples/Sample02.PassingParams/Models/ParameterType.cs
index 9447f9c..a4bd8fe 100644
--- a/samples/Sample.Basics/Models/ParameterType.cs
+++ b/samples/Sample02.PassingParams/Models/ParameterType.cs
@@ -1,7 +1,7 @@
// Copyright Xeno Innovations, Inc. 2025
// See the LICENSE file in the project root for more information.
-namespace Sample.Basics.Models;
+namespace Sample02.PassingParams.Models;
public enum ParameterType
{
diff --git a/samples/Sample.Basics/Program.cs b/samples/Sample02.PassingParams/Program.cs
similarity index 79%
rename from samples/Sample.Basics/Program.cs
rename to samples/Sample02.PassingParams/Program.cs
index da53eae..8a149f7 100644
--- a/samples/Sample.Basics/Program.cs
+++ b/samples/Sample02.PassingParams/Program.cs
@@ -4,15 +4,16 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
+using Sample02.PassingParams.States;
-namespace Sample.Basics;
+namespace Sample02.PassingParams;
internal class Program
{
private static async Task Main(string[] args)
{
Console.WriteLine("Sample state machine with LiteState!");
- await States.DemoMachine.RunAsync();
+ await DemoMachine.RunAsync();
// Poor man's timestamp
Console.WriteLine("\nRunning again, showing simple benchmarks...");
@@ -20,7 +21,7 @@ private static async Task Main(string[] args)
{
var sw = Stopwatch.StartNew();
- await States.DemoMachine.RunAsync(logOutput: false);
+ await DemoMachine.RunAsync(logOutput: false);
sw.Stop();
Console.WriteLine($"Took {sw.ElapsedMilliseconds} ms ({sw.ElapsedTicks} ticks)");
diff --git a/samples/Sample.Basics/Sample.Basics.csproj b/samples/Sample02.PassingParams/Sample02.PassingParams.csproj
similarity index 100%
rename from samples/Sample.Basics/Sample.Basics.csproj
rename to samples/Sample02.PassingParams/Sample02.PassingParams.csproj
diff --git a/samples/Sample.Basics/States/DemoMachine.cs b/samples/Sample02.PassingParams/States/DemoMachine.cs
similarity index 91%
rename from samples/Sample.Basics/States/DemoMachine.cs
rename to samples/Sample02.PassingParams/States/DemoMachine.cs
index 925bfe0..baf147a 100644
--- a/samples/Sample.Basics/States/DemoMachine.cs
+++ b/samples/Sample02.PassingParams/States/DemoMachine.cs
@@ -3,9 +3,9 @@
using System.Threading.Tasks;
using Lite.StateMachine;
-using Sample.Basics.Models;
+using Sample02.PassingParams.Models;
-namespace Sample.Basics.States;
+namespace Sample02.PassingParams.States;
public enum BasicStateId
{
diff --git a/samples/Sample.Basics/States/DemoStates.cs b/samples/Sample02.PassingParams/States/DemoStates.cs
similarity index 98%
rename from samples/Sample.Basics/States/DemoStates.cs
rename to samples/Sample02.PassingParams/States/DemoStates.cs
index 1a34232..e6b2e2d 100644
--- a/samples/Sample.Basics/States/DemoStates.cs
+++ b/samples/Sample02.PassingParams/States/DemoStates.cs
@@ -4,9 +4,9 @@
using System;
using System.Threading.Tasks;
using Lite.StateMachine;
-using Sample.Basics.Models;
+using Sample02.PassingParams.Models;
-namespace Sample.Basics.States;
+namespace Sample02.PassingParams.States;
#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1402 // File may only contain a single type
diff --git a/samples/Sample.Basics/States/StateBase.cs b/samples/Sample02.PassingParams/States/StateBase.cs
similarity index 93%
rename from samples/Sample.Basics/States/StateBase.cs
rename to samples/Sample02.PassingParams/States/StateBase.cs
index 9716bdb..3670389 100644
--- a/samples/Sample.Basics/States/StateBase.cs
+++ b/samples/Sample02.PassingParams/States/StateBase.cs
@@ -5,7 +5,7 @@
using System.Threading.Tasks;
using Lite.StateMachine;
-namespace Sample.Basics.States;
+namespace Sample02.PassingParams.States;
public class StateBase : IState
where TStateId : struct, Enum
diff --git a/samples/Sample02.PassingParams/readme.md b/samples/Sample02.PassingParams/readme.md
new file mode 100644
index 0000000..89ab781
--- /dev/null
+++ b/samples/Sample02.PassingParams/readme.md
@@ -0,0 +1,13 @@
+# Sample 2 - Passing Parameters
+
+This sample demonstrates how to pass the Context Parameters
+(`context.Parameters`) using the `PropertyBag` class.
+
+```cs
+ var counter = 0;
+ var ctxProperties = new PropertyBag()
+ {
+ { ParameterType.Counter, counter }, // (int)
+ { ParameterType.LogOutput, logOutput }, // (bool)
+ };
+```
diff --git a/samples/Sample.Basics/Services/MessageService.cs b/samples/Sample03.DependencyInjection/CounterService.cs
similarity index 76%
rename from samples/Sample.Basics/Services/MessageService.cs
rename to samples/Sample03.DependencyInjection/CounterService.cs
index 59ff57e..7d42100 100644
--- a/samples/Sample.Basics/Services/MessageService.cs
+++ b/samples/Sample03.DependencyInjection/CounterService.cs
@@ -2,15 +2,16 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
+using Sample03.DependencyInjection.MsDI;
-namespace Sample.Basics.Services;
+namespace Sample03.DependencyInjection;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Allowed for testing.")]
-public interface IMessageService
+public interface ICounterService
{
/// uses it as an automatic state transition counter.
+ /// uses it as an automatic state transition counter.
///
int Counter1 { get; set; }
@@ -20,6 +21,9 @@ public interface IMessageService
/// Gets or sets the user's custom counter.
int Counter3 { get; set; }
+ /// Gets or sets the user's custom counter.
+ int Counter4 { get; set; }
+
/// Gets a list of user's custom messages.
List Messages { get; }
@@ -28,7 +32,7 @@ public interface IMessageService
void AddMessage(string message);
}
-public class MessageService : IMessageService
+public class CounterService : ICounterService
{
///
public int Counter1 { get; set; }
@@ -39,6 +43,9 @@ public class MessageService : IMessageService
///
public int Counter3 { get; set; }
+ ///
+ public int Counter4 { get; set; }
+
///
public List Messages { get; } = [];
diff --git a/samples/Sample03.DependencyInjection/DryIocDI/DryIocStateMachine.cs b/samples/Sample03.DependencyInjection/DryIocDI/DryIocStateMachine.cs
new file mode 100644
index 0000000..c09de02
--- /dev/null
+++ b/samples/Sample03.DependencyInjection/DryIocDI/DryIocStateMachine.cs
@@ -0,0 +1,148 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using DryIoc;
+using Lite.StateMachine;
+using Microsoft.Extensions.Logging;
+
+namespace Sample03.DependencyInjection.DryIocDI;
+
+#pragma warning disable SA1649 // File name should match first type name
+#pragma warning disable SA1402 // File may only contain a single type
+
+public static class DryIocStateMachine
+{
+ /// State definitions.
+ public enum BasicStateId
+ {
+ State1,
+ State2,
+ State3,
+ }
+
+ public static async Task RunAsync()
+ {
+ var container = new Container(rules => rules.With(FactoryMethod.ConstructorWithResolvableArguments));
+
+ // Register Services
+ container.Register(Reuse.Singleton);
+ container.Register(Reuse.Singleton);
+
+ // Register Logger
+ //// For use with NLog:
+ //// var loggerFactory = new NLogLoggerFactory();
+ container.RegisterInstance(LoggerFactory.Create(builder =>
+ {
+ builder.SetMinimumLevel(LogLevel.Trace);
+ builder.AddSimpleConsole(options =>
+ {
+ options.IncludeScopes = true;
+ options.SingleLine = true;
+ });
+ }));
+ container.Register(typeof(ILogger<>), typeof(Logger<>), Reuse.Transient);
+
+ // Register States
+ container.Register(Reuse.Transient);
+ container.Register(Reuse.Transient);
+ container.Register(Reuse.Transient);
+
+ // Resolve dependency services for post-run evaluations
+ Func factory = t => container.Resolve(t);
+ var aggregator = container.Resolve();
+ var counterService = container.Resolve();
+
+ // Create State Machine
+ var machine = new StateMachine(factory)
+ .RegisterState(BasicStateId.State1, BasicStateId.State2)
+ .RegisterState(BasicStateId.State2, BasicStateId.State3)
+ .RegisterState(BasicStateId.State3);
+
+ var result = await machine.RunAsync(BasicStateId.State1);
+
+ Console.WriteLine("Post Execution Validations:");
+ Console.WriteLine("---------------------------");
+
+ Console.WriteLine($"* Counter service Counter1: {counterService.Counter1} (expected 9)");
+
+ // Ensure all states are registered
+ var enums = Enum.GetValues().Cast();
+ Console.WriteLine($"* State Machine Counts: {machine.States.Count()}. State Enum Count: {enums.Count()}");
+ Console.WriteLine($"* All states registered: {enums.All(k => machine.States.Contains(k))}");
+
+ // Ensure they're registered in order
+ // Validates that States are registered for execution in the same order as the defined enums. StateId 1 => 2 => 3.
+ Console.WriteLine($"* State registered in order: {enums.SequenceEqual(machine.States)}");
+ }
+
+ public class State1(ICounterService msg, ILogger log)
+ : BaseDiState(msg, log);
+
+ public class State2(ICounterService msg, ILogger log)
+ : BaseDiState(msg, log);
+
+ public class State3(ICounterService msg, ILogger log)
+ : BaseDiState(msg, log);
+}
+
+public class BaseDiState(ICounterService msg, ILogger logger)
+ : IState
+ where TStateId : struct, Enum
+{
+ private readonly ILogger _logger = logger;
+ private readonly ICounterService _msgService = msg;
+
+ /// Gets or sets a value indicating whether output transitions for debugging tests.
+ public bool HasExtraLogging { get; set; } = false;
+
+ public ILogger Log => _logger;
+
+ public ICounterService MessageService => _msgService;
+
+ #region Suppress CodeMaid Method Sorting
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "ignore")]
+ public virtual Task OnEntering(Context context)
+ {
+ _msgService.Counter1++;
+ _logger.LogInformation("[OnEntering]");
+
+ if (HasExtraLogging)
+ Debug.WriteLine($"[{GetType().Name}] [OnEntering]");
+
+ return Task.CompletedTask;
+ }
+
+ #endregion Suppress CodeMaid Method Sorting
+
+ public virtual Task OnEnter(Context context)
+ {
+ _msgService.Counter1++;
+ _logger.LogInformation("[OnEnter] => OK");
+
+ if (HasExtraLogging)
+ Debug.WriteLine($"[{GetType().Name}] [OnEnter] => OK");
+
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+
+ public virtual Task OnExit(Context context)
+ {
+ _msgService.Counter1++;
+ _logger.LogInformation("[OnExit]");
+
+ if (HasExtraLogging)
+ Debug.WriteLine($"[{GetType().Name}] [OnExit]");
+
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+}
+
+#pragma warning restore SA1649 // File name should match first type name
+#pragma warning restore SA1402 // File may only contain a single type
diff --git a/samples/Sample03.DependencyInjection/MsDI/BaseDiState.cs b/samples/Sample03.DependencyInjection/MsDI/BaseDiState.cs
new file mode 100644
index 0000000..5df7ef8
--- /dev/null
+++ b/samples/Sample03.DependencyInjection/MsDI/BaseDiState.cs
@@ -0,0 +1,68 @@
+// Copyright Xeno Innovations, Inc. 2025
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Lite.StateMachine;
+using Microsoft.Extensions.Logging;
+
+namespace Sample03.DependencyInjection.MsDI;
+
+#pragma warning disable SA1124 // Do not use regions
+
+public class BaseDiState(ICounterService msg, ILogger logger)
+ : IState
+ where TStateId : struct, Enum
+{
+ private readonly ILogger _logger = logger;
+ private readonly ICounterService _msgService = msg;
+
+ /// Gets or sets a value indicating whether output transitions for debugging tests.
+ public bool HasExtraLogging { get; set; } = false;
+
+ public ILogger Log => _logger;
+
+ public ICounterService MessageService => _msgService;
+
+ #region Suppress CodeMaid Method Sorting
+
+ public virtual Task OnEntering(Context context)
+ {
+ _msgService.Counter1++;
+ _logger.LogInformation("[OnEntering]");
+
+ if (HasExtraLogging)
+ Debug.WriteLine($"[{GetType().Name}] [OnEntering]");
+
+ return Task.CompletedTask;
+ }
+
+ #endregion Suppress CodeMaid Method Sorting
+
+ public virtual Task OnEnter(Context context)
+ {
+ _msgService.Counter1++;
+ _logger.LogInformation("[OnEnter] => OK");
+
+ if (HasExtraLogging)
+ Debug.WriteLine($"[{GetType().Name}] [OnEnter] => OK");
+
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+
+ public virtual Task OnExit(Context context)
+ {
+ _msgService.Counter1++;
+ _logger.LogInformation("[OnExit]");
+
+ if (HasExtraLogging)
+ Debug.WriteLine($"[{GetType().Name}] [OnExit]");
+
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+}
+
+#pragma warning restore SA1124 // Do not use regions
diff --git a/samples/Sample03.DependencyInjection/MsDI/MsDIStateMachine.cs b/samples/Sample03.DependencyInjection/MsDI/MsDIStateMachine.cs
new file mode 100644
index 0000000..f9754fe
--- /dev/null
+++ b/samples/Sample03.DependencyInjection/MsDI/MsDIStateMachine.cs
@@ -0,0 +1,80 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Lite.StateMachine;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Sample03.DependencyInjection.MsDI;
+
+#pragma warning disable SA1649 // File name should match first type name
+#pragma warning disable SA1402 // File may only contain a single type
+
+public static class MsDIStateMachine
+{
+ /// State definitions.
+ public enum BasicStateId
+ {
+ State1,
+ State2,
+ State3,
+ }
+
+ public static async Task RunAsync()
+ {
+ // Assemble with Dependency Injection
+ var services = new ServiceCollection()
+ //// Register Services
+ .AddLogging(b => b.AddSimpleConsole(options =>
+ {
+ options.SingleLine = true;
+ options.IncludeScopes = true;
+ }))
+ .AddSingleton()
+ //// Register States
+ .AddTransient()
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ Func factory = t => ActivatorUtilities.CreateInstance(services, t);
+
+ var machine = new StateMachine(factory)
+ .RegisterState(BasicStateId.State1, BasicStateId.State2)
+ .RegisterState(BasicStateId.State2, BasicStateId.State3)
+ .RegisterState(BasicStateId.State3);
+
+ var result = await machine.RunAsync(BasicStateId.State1);
+
+ Console.WriteLine("Post Execution Validations:");
+ Console.WriteLine("---------------------------");
+
+ var counterService = services.GetRequiredService();
+ Console.WriteLine($"* Counter service Counter1: {counterService.Counter1} (expected 9)");
+
+ // Ensure all states are registered
+ var enums = Enum.GetValues().Cast();
+ Console.WriteLine($"* State Machine Counts: {machine.States.Count()}. State Enum Count: {enums.Count()}");
+ Console.WriteLine($"* All states registered: {enums.All(k => machine.States.Contains(k))}");
+
+ // Ensure they're registered in order
+ // Validates that States are registered for execution in the same order as the defined enums. StateId 1 => 2 => 3.
+ Console.WriteLine($"* State registered in order: {enums.SequenceEqual(machine.States)}");
+ }
+
+ public class State1(ICounterService msg, ILogger log)
+ : BaseDiState(msg, log);
+
+ public class State2(ICounterService msg, ILogger log)
+ : BaseDiState(msg, log);
+
+ public class State3(ICounterService msg, ILogger log)
+ : BaseDiState(msg, log);
+}
+
+#pragma warning restore SA1649 // File name should match first type name
+#pragma warning restore SA1402 // File may only contain a single type
diff --git a/samples/Sample03.DependencyInjection/Program.cs b/samples/Sample03.DependencyInjection/Program.cs
new file mode 100644
index 0000000..21f4a5c
--- /dev/null
+++ b/samples/Sample03.DependencyInjection/Program.cs
@@ -0,0 +1,19 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+
+namespace Sample03.DependencyInjection;
+
+internal class Program
+{
+ private static async Task Main()
+ {
+ await DryIocDI.DryIocStateMachine.RunAsync();
+
+ Console.Write("\n-=-=-=-=-=-=-\n\n");
+
+ await MsDI.MsDIStateMachine.RunAsync();
+ }
+}
diff --git a/samples/Sample03.DependencyInjection/Sample03.DependencyInjection.csproj b/samples/Sample03.DependencyInjection/Sample03.DependencyInjection.csproj
new file mode 100644
index 0000000..1efa077
--- /dev/null
+++ b/samples/Sample03.DependencyInjection/Sample03.DependencyInjection.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ latest
+
+
+
+
+
+
+
+
+
diff --git a/samples/Sample03.DependencyInjection/readme.md b/samples/Sample03.DependencyInjection/readme.md
new file mode 100644
index 0000000..325370d
--- /dev/null
+++ b/samples/Sample03.DependencyInjection/readme.md
@@ -0,0 +1,6 @@
+# Sample 3 - State Machine with Dependency Injection
+
+This project demonstrates using Microsoft.Extensions.DependencyInjection to manage dependencies throughout state machine transitions.
+
+The sample includes a simple state machine that uses dependency injection to manage the `CounterService` required by the state machine. It increments a counter for each state's transition and elements of the state (`OnEntering`, `OnEnter`, `OnExit` and outputs the resuts to command line.
+
diff --git a/samples/Sample04.ResultTransitions/Program.cs b/samples/Sample04.ResultTransitions/Program.cs
new file mode 100644
index 0000000..992a8a3
--- /dev/null
+++ b/samples/Sample04.ResultTransitions/Program.cs
@@ -0,0 +1,95 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using Lite.StateMachine;
+
+namespace Sample04.ResultTransitions;
+
+/// State definitions.
+public enum StateId
+{
+ State1,
+ State2,
+ State3,
+}
+
+public class Program
+{
+ private static async Task Main(string[] args)
+ {
+ await ResultsStateMachine.RunAsync();
+ }
+}
+
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Ignore")]
+public class ResultsStateMachine
+{
+ /// Example asynchronous run method.
+ /// Task.
+ public static async Task RunAsync()
+ {
+ var machine = new StateMachine();
+ machine.RegisterState(StateId.State1, onSuccess: StateId.State1, onError: StateId.State2, onFailure: null, subscriptionTypes: null);
+ machine.RegisterState(StateId.State2, onSuccess: StateId.State1, onError: null, onFailure: StateId.State3, subscriptionTypes: null);
+ machine.RegisterState(StateId.State3, subscriptionTypes: null);
+
+ // Async Example!
+ await machine.RunAsync(StateId.State1);
+ }
+
+ public class State1 : IState
+ {
+ public Task OnEnter(Context context)
+ {
+ // Simulate an Error state transition so we can continue.
+ context.NextState(Result.Error);
+
+ Console.WriteLine($"[State1][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'");
+ Console.WriteLine($"[State1][OnEnter].OnError goto: '{context.NextStates.OnError}'");
+ Console.WriteLine($"[State1][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'");
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+ }
+
+ public class State2 : IState
+ {
+ public Task OnEnter(Context context)
+ {
+ // Simulate a Failure state transition so we can continue.
+ context.NextState(Result.Failure);
+
+ Console.WriteLine($"[State2][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'");
+ Console.WriteLine($"[State2][OnEnter].OnError goto: '{context.NextStates.OnError}'");
+ Console.WriteLine($"[State2][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'");
+
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+ }
+
+ public class State3 : IState
+ {
+ public Task OnEnter(Context context)
+ {
+ // Set Failure so we can continue.
+ context.NextState(Result.Success);
+
+ Console.WriteLine($"[State3][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'");
+ Console.WriteLine($"[State3][OnEnter].OnError goto: '{context.NextStates.OnError}'");
+ Console.WriteLine($"[State3][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'");
+
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+ }
+}
diff --git a/samples/Sample04.ResultTransitions/Sample04.ResultTransitions.csproj b/samples/Sample04.ResultTransitions/Sample04.ResultTransitions.csproj
new file mode 100644
index 0000000..c3ee00d
--- /dev/null
+++ b/samples/Sample04.ResultTransitions/Sample04.ResultTransitions.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/samples/Sample04.ResultTransitions/readme.md b/samples/Sample04.ResultTransitions/readme.md
new file mode 100644
index 0000000..ffe8a78
--- /dev/null
+++ b/samples/Sample04.ResultTransitions/readme.md
@@ -0,0 +1,5 @@
+# Sample 04 - Result Transitions
+
+_Coming Soon_
+
+See, Unit Tests, for an example of how to use Result Transitions for now.
diff --git a/samples/Sample05.CommandStates/Program.cs b/samples/Sample05.CommandStates/Program.cs
new file mode 100644
index 0000000..fd3bfc6
--- /dev/null
+++ b/samples/Sample05.CommandStates/Program.cs
@@ -0,0 +1,12 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+namespace Sample05.CommandStates;
+
+internal class Program
+{
+ private static void Main(string[] args)
+ {
+ Console.WriteLine("Coming Soon!");
+ }
+}
diff --git a/samples/Sample05.CommandStates/Sample05.CommandStates.csproj b/samples/Sample05.CommandStates/Sample05.CommandStates.csproj
new file mode 100644
index 0000000..ed9781c
--- /dev/null
+++ b/samples/Sample05.CommandStates/Sample05.CommandStates.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/samples/Sample05.CommandStates/readme.md b/samples/Sample05.CommandStates/readme.md
new file mode 100644
index 0000000..936c98f
--- /dev/null
+++ b/samples/Sample05.CommandStates/readme.md
@@ -0,0 +1,5 @@
+# Sample 05 - Command States
+
+_Coming Soon_
+
+See, Unit Tests, for an example of how to use Command States for now.
diff --git a/samples/Sample06.PassingEvents/Program.cs b/samples/Sample06.PassingEvents/Program.cs
new file mode 100644
index 0000000..d1d7bdd
--- /dev/null
+++ b/samples/Sample06.PassingEvents/Program.cs
@@ -0,0 +1,12 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+namespace Sample06.PassingEvents;
+
+internal class Program
+{
+ private static void Main(string[] args)
+ {
+ Console.WriteLine("Coming Soon!");
+ }
+}
diff --git a/samples/Sample06.PassingEvents/Sample06.PassingEvents.csproj b/samples/Sample06.PassingEvents/Sample06.PassingEvents.csproj
new file mode 100644
index 0000000..ed9781c
--- /dev/null
+++ b/samples/Sample06.PassingEvents/Sample06.PassingEvents.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/samples/Sample06.PassingEvents/readme.md b/samples/Sample06.PassingEvents/readme.md
new file mode 100644
index 0000000..2e2811c
--- /dev/null
+++ b/samples/Sample06.PassingEvents/readme.md
@@ -0,0 +1,5 @@
+# Sample 06 - Event Aggregator for Passing Events
+
+_Coming Soon_
+
+See, Unit Tests, for an example of how to use Event Aggregator for Passing Events for now.
diff --git a/samples/Sample10.UsingEventIPC/Program.cs b/samples/Sample10.UsingEventIPC/Program.cs
new file mode 100644
index 0000000..a0c54c5
--- /dev/null
+++ b/samples/Sample10.UsingEventIPC/Program.cs
@@ -0,0 +1,12 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+namespace Sample10.UsingEventIPC;
+
+internal class Program
+{
+ private static void Main(string[] args)
+ {
+ Console.WriteLine("Coming Soon!");
+ }
+}
diff --git a/samples/Sample10.UsingEventIPC/Sample10.UsingEventIPC.csproj b/samples/Sample10.UsingEventIPC/Sample10.UsingEventIPC.csproj
new file mode 100644
index 0000000..ed9781c
--- /dev/null
+++ b/samples/Sample10.UsingEventIPC/Sample10.UsingEventIPC.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/samples/Sample10.UsingEventIPC/readme.md b/samples/Sample10.UsingEventIPC/readme.md
new file mode 100644
index 0000000..8e36786
--- /dev/null
+++ b/samples/Sample10.UsingEventIPC/readme.md
@@ -0,0 +1,3 @@
+# Sample 10 - Using Lite.EventIPC for External Event Aggregator
+
+_Coming Soon_
diff --git a/source/Lite.StateMachine.Tests/StateTests/NextStateResultTests.cs b/source/Lite.StateMachine.Tests/StateTests/NextStateResultTests.cs
new file mode 100644
index 0000000..0846074
--- /dev/null
+++ b/source/Lite.StateMachine.Tests/StateTests/NextStateResultTests.cs
@@ -0,0 +1,87 @@
+// Copyright Xeno Innovations, Inc. 2026
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Lite.StateMachine.Tests.StateTests;
+
+[TestClass]
+public class NextStateResultTests : TestBase
+{
+ /// State definitions.
+ public enum StateId
+ {
+ State1,
+ State2,
+ State3,
+ }
+
+ [TestMethod]
+ public async Task NextState_Error_Failure__SuccessTestAsync()
+ {
+ var machine = new StateMachine();
+ machine.RegisterState(StateId.State1, onSuccess: StateId.State1, onError: StateId.State2, onFailure: null, subscriptionTypes: null);
+ machine.RegisterState(StateId.State2, onSuccess: StateId.State1, onError: null, onFailure: StateId.State3, subscriptionTypes: null);
+ machine.RegisterState(StateId.State3, subscriptionTypes: null);
+
+ // Async Example!
+ await machine.RunAsync(StateId.State1);
+
+ // Assert Results
+ AssertMachineNotNull(machine);
+
+ // Ensure all states are registered
+ var enums = Enum.GetValues().Cast();
+ Assert.IsNotNull(enums);
+ Assert.HasCount(enums.Count(), machine.States);
+ Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
+ }
+
+ private class State1 : IState
+ {
+ public Task OnEnter(Context context)
+ {
+ // Simulate an Error state transition so we can continue.
+ context.NextState(Result.Error);
+
+ Console.WriteLine($"[State1][OnEnter].OnSuccess goto: '{context.NextStates.OnSuccess}'");
+ Console.WriteLine($"[State1][OnEnter].OnError goto: '{context.NextStates.OnError}'");
+ Console.WriteLine($"[State1][OnEnter].OnFailure goto: '{context.NextStates.OnFailure}'");
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+ }
+
+ private class State2 : IState
+ {
+ public Task OnEnter(Context context)
+ {
+ // Simulate a Failure state transition so we can continue.
+ context.NextState(Result.Failure);
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+ }
+
+ private class State3 : IState
+ {
+ public Task OnEnter(Context context)
+ {
+ // Set Failure so we can continue.
+ context.NextState(Result.Success);
+ return Task.CompletedTask;
+ }
+
+ public Task OnEntering(Context context) => Task.CompletedTask;
+
+ public Task OnExit(Context context) => Task.CompletedTask;
+ }
+}
diff --git a/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs b/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs
index c6321f3..9abd767 100644
--- a/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs
+++ b/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs
@@ -11,9 +11,11 @@
#pragma warning disable SA1402 // File may only contain a single type
#pragma warning disable IDE0130 // Namespace does not match folder structure
-///
namespace Lite.StateMachine.Tests.TestData.States.CompositeL3DiStates;
+///
+/// Added "CompositeL3DiStates" to namespace to reduce class naming collisions.
+///
public class CommonDiStateBase(IMessageService msg, ILogger logger)
: StateDiBase(msg, logger)
where TStateId : struct, Enum
diff --git a/source/Lite.StateMachine/Lite.StateMachine.csproj b/source/Lite.StateMachine/Lite.StateMachine.csproj
index 32faade..08e3beb 100644
--- a/source/Lite.StateMachine/Lite.StateMachine.csproj
+++ b/source/Lite.StateMachine/Lite.StateMachine.csproj
@@ -21,6 +21,9 @@
statemachine;lite;fsm;suesslabs;xeno-innovations
LICENSE.md
+ New Features in v2.4:
+ * Added custom State Results (Success, Error, Failure, etc.) and ability to override the default Result.Success/.Error./Failure.
+
New Features in v2.2:
* New: Renamed, Result.Ok -> Result.Success (Breaking Change).
diff --git a/source/Lite.StateMachine/StateMachine.cs b/source/Lite.StateMachine/StateMachine.cs
index e0bb1a2..237a6a8 100644
--- a/source/Lite.StateMachine/StateMachine.cs
+++ b/source/Lite.StateMachine/StateMachine.cs
@@ -133,7 +133,7 @@ public StateMachine RegisterState(
IReadOnlyCollection? subscriptionTypes = null)
where TStateClass : class, IState
{
- return RegisterState(stateId, onSuccess, onError: null, onFailure: null, parentStateId: null, isCompositeParent: false, initialChildStateId: null, subscriptionTypes: subscriptionTypes);
+ return RegisterState(stateId, onSuccess, onError: onError, onFailure: onFailure, parentStateId: null, isCompositeParent: false, initialChildStateId: null, subscriptionTypes: subscriptionTypes);
}
///