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 | [![Lite.StateMachine NuGet Badge](https://img.shields.io/nuget/v/Lite.StateMachine)](https://www.nuget.org/packages/Lite.StateMachine/) | [![Lite.StateMachine NuGet Badge](https://img.shields.io/nuget/vpre/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 +![](docs/images/nuget-state-machine-icon-isometric.png) +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); } ///