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/Lite.StateMachine.slnx b/Lite.StateMachine.slnx index 3e7a3ea..f736c75 100644 --- a/Lite.StateMachine.slnx +++ b/Lite.StateMachine.slnx @@ -1,7 +1,8 @@ - - + + + 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 804b2a3..6ca4cf6 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,19 @@ 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_ + ## Usage Create a _state machine_ by defining the states, transitions, and shared context. 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/Sample03.DependencyInjection/BasicDiStates.cs b/samples/Sample03.DependencyInjection/BasicDiStates.cs new file mode 100644 index 0000000..83ce830 --- /dev/null +++ b/samples/Sample03.DependencyInjection/BasicDiStates.cs @@ -0,0 +1,29 @@ +// Copyright Xeno Innovations, Inc. 2025 +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Logging; + +namespace Sample03.DependencyInjection; + +#pragma warning disable SA1649 // File name should match first type name +#pragma warning disable SA1402 // File may only contain a single type + +/// State definitions. +public enum BasicStateId +{ + State1, + State2, + State3, +} + +public class BasicDiState1(ICounterService msg, ILogger log) + : StateDiBase(msg, log); + +public class BasicDiState2(ICounterService msg, ILogger log) + : StateDiBase(msg, log); + +public class BasicDiState3(ICounterService msg, ILogger log) + : StateDiBase(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/Sample.Basics/Services/MessageService.cs b/samples/Sample03.DependencyInjection/CounterService.cs similarity index 78% rename from samples/Sample.Basics/Services/MessageService.cs rename to samples/Sample03.DependencyInjection/CounterService.cs index 59ff57e..7517485 100644 --- a/samples/Sample.Basics/Services/MessageService.cs +++ b/samples/Sample03.DependencyInjection/CounterService.cs @@ -3,14 +3,14 @@ using System.Collections.Generic; -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 +20,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 +31,7 @@ public interface IMessageService void AddMessage(string message); } -public class MessageService : IMessageService +public class CounterService : ICounterService { /// public int Counter1 { get; set; } @@ -39,6 +42,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/Program.cs b/samples/Sample03.DependencyInjection/Program.cs new file mode 100644 index 0000000..d4e8753 --- /dev/null +++ b/samples/Sample03.DependencyInjection/Program.cs @@ -0,0 +1,57 @@ +// Copyright Xeno Innovations, Inc. 2025 +// 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; + +namespace Sample03.DependencyInjection; + +internal class Program +{ + private static async Task Main() + { + await TestMicrosoftDependencyInjectionAsync(); + } + + private static async Task TestMicrosoftDependencyInjectionAsync() + { + // Assemble with Dependency Injection + var services = new ServiceCollection() + //// Register Services + .AddLogging(b => b.AddSimpleConsole()) + .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("\n\nPost Execution Validations:"); + Console.WriteLine("---------------------------"); + + var msgService = services.GetRequiredService(); + Console.WriteLine($"* Message service Counter1: {msgService.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)}"); + } +} diff --git a/samples/Sample03.DependencyInjection/Sample03.DependencyInjection.csproj b/samples/Sample03.DependencyInjection/Sample03.DependencyInjection.csproj new file mode 100644 index 0000000..bc3c792 --- /dev/null +++ b/samples/Sample03.DependencyInjection/Sample03.DependencyInjection.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + latest + + + + + + + + diff --git a/samples/Sample03.DependencyInjection/StateDiBase.cs b/samples/Sample03.DependencyInjection/StateDiBase.cs new file mode 100644 index 0000000..6cf9413 --- /dev/null +++ b/samples/Sample03.DependencyInjection/StateDiBase.cs @@ -0,0 +1,67 @@ +// 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; + +#pragma warning disable SA1124 // Do not use regions + +public class StateDiBase(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/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/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