diff --git a/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs b/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs
index 94865ea..f3979c8 100644
--- a/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/BasicStateTests.cs
@@ -12,10 +12,6 @@ namespace Lite.StateMachine.Tests.StateTests;
[TestClass]
public class BasicStateTests
{
- public const string ParameterCounter = "Counter";
- public const string ParameterKeyTest = "TestKey";
- public const string TestValue = "success";
-
public TestContext TestContext { get; set; }
/// Standard synchronous state registration exiting to completion.
@@ -23,8 +19,11 @@ public class BasicStateTests
public void Basic_RegisterState_Executes123_SuccessTest()
{
// Assemble
- var counter = 0;
- var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
+ var ctxProperties = new PropertyBag()
+ {
+ { ParameterType.Counter, 0 },
+ { ParameterType.TestExecutionOrder, true },
+ };
var machine = new StateMachine();
machine.RegisterState(BasicStateId.State1, BasicStateId.State2);
@@ -41,7 +40,7 @@ public void Basic_RegisterState_Executes123_SuccessTest()
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're registered in order
@@ -54,8 +53,11 @@ public void Basic_RegisterState_Executes123_SuccessTest()
public async Task Basic_RegisterState_Executes123_SuccessTestAsync()
{
// Assemble
- var counter = 0;
- var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
+ var ctxProperties = new PropertyBag()
+ {
+ { ParameterType.Counter, 0 },
+ { ParameterType.TestExecutionOrder, true },
+ };
var machine = new StateMachine();
machine.RegisterState(BasicStateId.State1, BasicStateId.State2);
@@ -71,7 +73,7 @@ public async Task Basic_RegisterState_Executes123_SuccessTestAsync()
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're registered in order
@@ -88,7 +90,8 @@ public async Task Basic_RegisterState_Executes132_SuccessTestAsync()
machine.RegisterState(BasicStateId.State2);
// Act - Start your engine!
- await machine.RunAsync(BasicStateId.State1);
+ var ctxProperties = new PropertyBag() { { ParameterType.TestExecutionOrder, false } };
+ await machine.RunAsync(BasicStateId.State1, ctxProperties);
// Assert Results
Assert.IsNotNull(machine);
@@ -96,7 +99,7 @@ public async Task Basic_RegisterState_Executes132_SuccessTestAsync()
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're NOT registered in order
@@ -109,8 +112,11 @@ public async Task Basic_RegisterState_Executes132_SuccessTestAsync()
public async Task Basic_RegisterState_Fluent_SuccessTestAsync()
{
// Assemble
- var counter = 0;
- var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
+ var ctxProperties = new PropertyBag()
+ {
+ { ParameterType.Counter, 0 },
+ { ParameterType.TestExecutionOrder, true },
+ };
// Assemble/Act - Start your engine!
var machine = await new StateMachine()
@@ -125,7 +131,7 @@ public async Task Basic_RegisterState_Fluent_SuccessTestAsync()
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're registered in order
@@ -168,7 +174,12 @@ public async Task HungState_Proceeds_DefaultStateTimeout_SuccessTestAsync()
var machine = new StateMachine();
// This test will take 1 full second to complete versus
- var paramStack = new PropertyBag() { { ParameterType.HungStateAvoidance, true } };
+ var ctxProperties = new PropertyBag()
+ {
+ { ParameterType.TestHungStateAvoidance, true },
+ { ParameterType.TestExecutionOrder, true },
+ };
+
machine.DefaultStateTimeoutMs = 1000;
machine.RegisterState(BasicStateId.State1, BasicStateId.State2);
@@ -176,7 +187,7 @@ public async Task HungState_Proceeds_DefaultStateTimeout_SuccessTestAsync()
machine.RegisterState(BasicStateId.State3);
// Act - Start your engine!
- await machine.RunAsync(BasicStateId.State1, paramStack);
+ await machine.RunAsync(BasicStateId.State1, ctxProperties);
// Assert Results
Assert.IsNotNull(machine);
@@ -184,7 +195,7 @@ public async Task HungState_Proceeds_DefaultStateTimeout_SuccessTestAsync()
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
}
@@ -194,8 +205,14 @@ public async Task HungState_Proceeds_DefaultStateTimeout_SuccessTestAsync()
[Ignore("vNext - Currently StateMachine destroys context after run completes.")]
public async Task RegisterState_ReturnsContext_SuccessTestAsync()
{
+ const string TestValue = "success";
+
// Assemble
- var counter = 0;
+ var ctxProperties = new PropertyBag()
+ {
+ { ParameterType.Counter, 0 },
+ { ParameterType.TestExecutionOrder, true },
+ };
var machine = new StateMachine();
machine.RegisterState(BasicStateId.State1, BasicStateId.State2);
@@ -203,7 +220,6 @@ public async Task RegisterState_ReturnsContext_SuccessTestAsync()
machine.RegisterState(BasicStateId.State3);
// Act - Start your engine!
- var ctxProperties = new PropertyBag() { { ParameterCounter, counter } };
var task = machine.RunAsync(BasicStateId.State1, ctxProperties);
await task; // Non async method: task.GetAwaiter().GetResult();
@@ -213,10 +229,10 @@ public async Task RegisterState_ReturnsContext_SuccessTestAsync()
var ctxFinalParams = machine.Context.Parameters;
Assert.IsNotNull(ctxFinalParams);
- Assert.AreEqual(TestValue, ctxFinalParams[ParameterKeyTest]);
+ Assert.AreEqual(TestValue, ctxFinalParams[ParameterType.KeyTest]);
// NOTE: This should be 9 because each state has 3 hooks that increment the counter
// TODO (2025-12-22 DS): Fix last state not calling OnExit.
- Assert.AreEqual(9, ctxFinalParams[ParameterCounter]);
+ Assert.AreEqual(9, ctxFinalParams[ParameterType.Counter]);
}
}
diff --git a/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs b/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs
index 8020450..1c283d6 100644
--- a/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/CompositeStateTest.cs
@@ -61,7 +61,7 @@ public async Task Level1_Basic_RegisterHelpers_SuccessTestAsync()
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're in order
@@ -89,7 +89,7 @@ public async Task Level1_Basic_RegisterState_SuccessTestAsync()
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're in order
@@ -139,7 +139,7 @@ public async Task Level1_Fluent_RegisterHelpers_SuccessTestAsync()
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're in order
@@ -166,7 +166,7 @@ public void Level1_Fluent_RegisterState_SuccessTest()
// Ensure all states are hit
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));
// Ensure they're in order
@@ -186,20 +186,10 @@ public async Task Level3_ExportUml_SuccessTestAsync()
var msgService = services.GetRequiredService();
Func factory = t => ActivatorUtilities.CreateInstance(services, t);
- var machine = new StateMachine(factory);
-
- 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);
-
+ // Act
+ var machine = GenerateStateMachineL3(new StateMachine(factory));
var uml = machine.ExportUml([CompositeL3.State1], includeLegend: false);
+
Assert.IsNotNull(uml);
Console.WriteLine(uml);
}
@@ -229,18 +219,7 @@ public async Task Level3_IsContextPersistent_False_SuccessTestAsync(bool context
var msgService = services.GetRequiredService();
Func factory = t => ActivatorUtilities.CreateInstance(services, t);
- var machine = new StateMachine(factory, null, isContextPersistent: contextIsPersistent);
-
- 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);
+ var machine = GenerateStateMachineL3(new StateMachine(factory, null, isContextPersistent: contextIsPersistent));
// Act
await machine.RunAsync(CompositeL3.State1, cancellationToken: TestContext.CancellationToken);
@@ -251,7 +230,7 @@ public async Task Level3_IsContextPersistent_False_SuccessTestAsync(bool context
// Ensure all states are registered
var enums = Enum.GetValues().Cast();
- Assert.AreEqual(enums.Count(), machine.States.Count());
+ Assert.HasCount(enums.Count(), machine.States);
Assert.IsTrue(enums.All(stateId => machine.States.Contains(stateId)));
// State Transition counter (9 states, 3 transitions)
@@ -287,4 +266,43 @@ public async Task Level3_IsContextPersistent_False_SuccessTestAsync(bool context
Assert.AreEqual(11, msgService.Counter3);
}
}
+
+ [TestMethod]
+ public async Task Level3_PreviousStateId_SuccessTestAsync()
+ {
+ // Assemble - Using DI for MessageService's counters
+ var services = new ServiceCollection()
+ .AddLogging(InlineTraceLogger(LogLevel.None))
+ .AddSingleton()
+ .BuildServiceProvider();
+
+ var msgService = services.GetRequiredService();
+ Func factory = t => ActivatorUtilities.CreateInstance(services, t);
+
+ var ctxProperties = new PropertyBag() { { ParameterType.TestExecutionOrder, true } };
+ var machine = GenerateStateMachineL3(new StateMachine(factory));
+
+ // Act
+ await machine.RunAsync(CompositeL3.State1, ctxProperties, null, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(machine);
+ Assert.IsNull(machine.Context);
+ }
+
+ 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;
+ }
}
diff --git a/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs b/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
index 67080d3..11ad0a6 100644
--- a/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
+++ b/source/Lite.StateMachine.Tests/StateTests/CustomStateTests.cs
@@ -94,8 +94,8 @@ await Assert.ThrowsExactlyAsync(()
[TestMethod]
[DataRow(false, DisplayName = "Run State2_Sub3")]
- [DataRow(true, DisplayName = "Skip State2_Sub3")]
- public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState3)
+ [DataRow(true, DisplayName = "Skip State2_Sub2")]
+ public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState2)
{
// Assemble with Dependency Injection
var services = new ServiceCollection()
@@ -110,7 +110,7 @@ public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState3)
var ctxProperties = new PropertyBag()
{
{ ParameterType.Counter, 0 },
- { ParameterType.TestExitEarly2, skipSubState3 },
+ { ParameterType.TestExitEarly2, skipSubState2 },
};
var machine = new StateMachine(factory, null, isContextPersistent: true);
@@ -130,7 +130,7 @@ public async Task Composite_Override_Executes_SuccessAsync(bool skipSubState3)
Assert.IsNotNull(machine);
Assert.IsNull(machine.Context);
- Assert.AreEqual(skipSubState3 ? 2 : 3, msgService.Counter1, "State Counter1 failed.");
+ Assert.AreEqual(skipSubState2 ? 3 : 4, msgService.Counter1, "State Counter1 failed.");
Assert.AreEqual(0, msgService.Counter2, "State2Dummy should never enter");
Assert.AreEqual(1, msgService.Counter3, "Skip Substate Counter3 failed");
}
diff --git a/source/Lite.StateMachine.Tests/TestData/ParameterType.cs b/source/Lite.StateMachine.Tests/TestData/ParameterType.cs
index 1083087..a289bb1 100644
--- a/source/Lite.StateMachine.Tests/TestData/ParameterType.cs
+++ b/source/Lite.StateMachine.Tests/TestData/ParameterType.cs
@@ -8,18 +8,21 @@ public enum ParameterType
/// Generic counter.
Counter,
- /// Tests for DoNotAllowHungStatesTest.
- HungStateAvoidance,
-
/// Random test.
KeyTest,
+ /// Test states executed in order using context.LastStateId or states in a different order.
+ TestExecutionOrder,
+
/// Test triggers an early exit. Setting OnSuccess to NULL.
TestExitEarly,
/// Test triggers a 2nd early exit. Setting OnSuccess to NULL.
TestExitEarly2,
+ /// Tests for DoNotAllowHungStatesTest.
+ TestHungStateAvoidance,
+
/// Test trigger to go to an invalid state transition.
TestUnregisteredTransition,
}
diff --git a/source/Lite.StateMachine.Tests/TestData/States/BasicStates.cs b/source/Lite.StateMachine.Tests/TestData/States/BasicStates.cs
index f56cc33..144b337 100644
--- a/source/Lite.StateMachine.Tests/TestData/States/BasicStates.cs
+++ b/source/Lite.StateMachine.Tests/TestData/States/BasicStates.cs
@@ -8,9 +8,21 @@ namespace Lite.StateMachine.Tests.TestData.States;
#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 SA1124 // Do not use regions
public class BasicState1() : IState
{
+ #region Suppress CodeMaid Method Sorting
+
+ public Task OnEntering(Context context)
+ {
+ context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
+ Console.WriteLine($"[BasicState1][OnEntering] {context.Parameters[ParameterType.Counter]}");
+ return Task.CompletedTask;
+ }
+
+ #endregion Suppress CodeMaid Method Sorting
+
public async Task OnEnter(Context context)
{
// Some async work here...
@@ -21,13 +33,6 @@ public async Task OnEnter(Context context)
Console.WriteLine($"[BasicState1][OnEnter] {context.Parameters[ParameterType.Counter]} => OK");
}
- public Task OnEntering(Context context)
- {
- context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
- Console.WriteLine($"[BasicState1][OnEntering] {context.Parameters[ParameterType.Counter]}");
- return Task.CompletedTask;
- }
-
public Task OnExit(Context context)
{
context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
@@ -38,12 +43,29 @@ public Task OnExit(Context context)
public class BasicState2() : IState
{
+ #region Suppress CodeMaid Method Sorting
+
+ public Task OnEntering(Context context)
+ {
+ context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
+ Console.WriteLine($"[BasicState2][OnEntering] {context.Parameters[ParameterType.Counter]}");
+ return Task.CompletedTask;
+ }
+
+ #endregion Suppress CodeMaid Method Sorting
+
public Task OnEnter(Context context)
{
context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
+ // Assert origin of the previous state
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.AreEqual(BasicStateId.State1, context.PreviousStateId);
+ else
+ Assert.AreEqual(BasicStateId.State3, context.PreviousStateId);
+
// Only move to the next state if we are not testing hanging state avoidance
- var testHangingState = context.ParameterAsBool(ParameterType.HungStateAvoidance);
+ var testHangingState = context.ParameterAsBool(ParameterType.TestHungStateAvoidance);
if (!testHangingState)
context.NextState(Result.Success);
@@ -51,13 +73,6 @@ public Task OnEnter(Context context)
return Task.CompletedTask;
}
- public Task OnEntering(Context context)
- {
- context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
- Console.WriteLine($"[BasicState2][OnEntering] {context.Parameters[ParameterType.Counter]}");
- return Task.CompletedTask;
- }
-
public Task OnExit(Context context)
{
context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
@@ -68,19 +83,29 @@ public Task OnExit(Context context)
public class BasicState3() : IState
{
- public Task OnEnter(Context context)
+ #region Suppress CodeMaid Method Sorting
+
+ public Task OnEntering(Context context)
{
context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
- context.Parameters[ParameterType.KeyTest] = MessageType.SuccessResponse;
- context.NextState(Result.Success);
- Console.WriteLine($"[BasicState3][OnEnter] {context.Parameters[ParameterType.Counter]}");
+ Console.WriteLine($"[BasicState3][OnEntering] {context.Parameters[ParameterType.Counter]}");
return Task.CompletedTask;
}
- public Task OnEntering(Context context)
+ #endregion Suppress CodeMaid Method Sorting
+
+ public Task OnEnter(Context context)
{
+ // Assert origin of the previous state
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.AreEqual(BasicStateId.State2, context.PreviousStateId);
+ else
+ Assert.AreEqual(BasicStateId.State1, context.PreviousStateId);
+
context.Parameters[ParameterType.Counter] = context.ParameterAsInt(ParameterType.Counter) + 1;
- Console.WriteLine($"[BasicState3][OnEntering] {context.Parameters[ParameterType.Counter]}");
+ context.Parameters[ParameterType.KeyTest] = MessageType.SuccessResponse;
+ context.NextState(Result.Success);
+ Console.WriteLine($"[BasicState3][OnEnter] {context.Parameters[ParameterType.Counter]}");
return Task.CompletedTask;
}
@@ -92,5 +117,6 @@ public Task OnExit(Context context)
}
}
+#pragma warning restore SA1124 // Do not use regions
#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/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs b/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs
index e911112..b0fbb74 100644
--- a/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs
+++ b/source/Lite.StateMachine.Tests/TestData/States/CompositeL3DiStates.cs
@@ -33,6 +33,9 @@ public class State1(IMessageService msg, ILogger log)
{
public override Task OnEnter(Context context)
{
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.IsNull(context.PreviousStateId);
+
context.Parameters.Add(context.CurrentStateId.ToString(), Guid.NewGuid());
MessageService.AddMessage($"[Keys-{context.CurrentStateId}]: {string.Join(",", context.Parameters.Keys)}");
return base.OnEnter(context);
@@ -56,6 +59,9 @@ public override Task OnEntering(Context context)
public override Task OnEnter(Context context)
{
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.AreEqual(CompositeL3.State1, context.PreviousStateId);
+
// Demonstrate temporary parameter that will be discarded after State2's OnExit
context.Parameters.Add($"{context.CurrentStateId}!TEMP", Guid.NewGuid());
return base.OnEnter(context);
@@ -72,7 +78,16 @@ public override Task OnExit(Context context)
/// Sublevel-2: State.
public class State2_Sub1(IMessageService msg, ILogger log)
- : CommonDiStateBase(msg, log);
+ : CommonDiStateBase(msg, log)
+{
+ public override Task OnEnter(Context context)
+ {
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.IsNull(context.PreviousStateId);
+
+ return base.OnEnter(context);
+ }
+}
/// Sublevel-2: Composite.
public class State2_Sub2(IMessageService msg, ILogger log)
@@ -91,6 +106,9 @@ public override Task OnEntering(Context context)
public override Task OnEnter(Context context)
{
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.AreEqual(CompositeL3.State2_Sub1, context.PreviousStateId);
+
// Demonstrate temporary parameter that will be discarded after State2_Sub2's OnExit
context.Parameters.Add($"{context.CurrentStateId}!TEMP", Guid.NewGuid());
return base.OnEnter(context);
@@ -106,7 +124,16 @@ public override Task OnExit(Context context)
/// Sublevel-3: State.
public class State2_Sub2_Sub1(IMessageService msg, ILogger log)
- : CommonDiStateBase(msg, log);
+ : CommonDiStateBase(msg, log)
+{
+ public override Task OnEnter(Context context)
+ {
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.IsNull(context.PreviousStateId);
+
+ return base.OnEnter(context);
+ }
+}
/// Sublevel-3: State.
public class State2_Sub2_Sub2(IMessageService msg, ILogger log)
@@ -114,7 +141,16 @@ public class State2_Sub2_Sub2(IMessageService msg, ILogger log
/// Sublevel-3: Last State.
public class State2_Sub2_Sub3(IMessageService msg, ILogger log)
- : CommonDiStateBase(msg, log);
+ : CommonDiStateBase(msg, log)
+{
+ public override Task OnEnter(Context context)
+ {
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.AreEqual(CompositeL3.State2_Sub2_Sub2, context.PreviousStateId);
+
+ return base.OnEnter(context);
+ }
+}
/// Sublevel-2: Last State.
public class State2_Sub3(IMessageService msg, ILogger log)
@@ -122,6 +158,9 @@ public class State2_Sub3(IMessageService msg, ILogger log)
{
public override Task OnEnter(Context context)
{
+ if (context.ParameterAsBool(ParameterType.TestExecutionOrder))
+ Assert.AreEqual(CompositeL3.State2_Sub2, context.PreviousStateId);
+
context.Parameters.Add(context.CurrentStateId.ToString(), Guid.NewGuid());
MessageService.AddMessage($"[Keys-{context.CurrentStateId}]: {string.Join(",", context.Parameters.Keys)}");
return base.OnEnter(context);
diff --git a/source/Lite.StateMachine.Tests/TestData/States/CustomBasicStates.cs b/source/Lite.StateMachine.Tests/TestData/States/CustomBasicStates.cs
index 9a4338e..74a8d8d 100644
--- a/source/Lite.StateMachine.Tests/TestData/States/CustomBasicStates.cs
+++ b/source/Lite.StateMachine.Tests/TestData/States/CustomBasicStates.cs
@@ -16,8 +16,6 @@ public class State1 : StateBase
public override async Task OnEnter(Context ctx)
{
- int cnt = ctx.ParameterAsInt(ParameterType.Counter);
-
if (ctx.ParameterAsBool(ParameterType.TestUnregisteredTransition))
ctx.NextStates.OnSuccess = CustomStateId.State2_Unregistered;
else
@@ -60,6 +58,8 @@ public override Task OnEnter(Context ctx)
{
_msgService.Counter1++;
+ Assert.AreEqual(CustomStateId.State1, ctx.PreviousStateId);
+
if (ctx.ParameterAsBool(ParameterType.TestExitEarly))
ctx.NextStates.OnSuccess = null;
@@ -76,7 +76,24 @@ public override Task OnExit(Context context)
public class State2Success_Sub1 : StateBase
{
- public State2Success_Sub1() => HasDebugLogging = true;
+ private readonly IMessageService _msgService;
+
+ public State2Success_Sub1(IMessageService msg)
+ {
+ _msgService = msg;
+ HasDebugLogging = true;
+ }
+
+ public override Task OnEnter(Context ctx)
+ {
+ _msgService.Counter1++;
+
+ // Skip Sub2 and goto Sub3
+ if (ctx.ParameterAsBool(ParameterType.TestExitEarly2))
+ ctx.NextStates.OnSuccess = CustomStateId.State2_Sub3;
+
+ return base.OnEnter(ctx);
+ }
}
public class State2Success_Sub2 : StateBase
@@ -92,10 +109,6 @@ public State2Success_Sub2(IMessageService msg)
public override Task OnEnter(Context ctx)
{
_msgService.Counter1++;
-
- if (ctx.ParameterAsBool(ParameterType.TestExitEarly2))
- ctx.NextStates.OnSuccess = null;
-
return base.OnEnter(ctx);
}
}
@@ -112,6 +125,11 @@ public State2Success_Sub3(IMessageService msg)
public override Task OnEnter(Context ctx)
{
+ if (ctx.ParameterAsBool(ParameterType.TestExitEarly2))
+ Assert.AreEqual(CustomStateId.State2_Sub1, ctx.PreviousStateId);
+ else
+ Assert.AreEqual(CustomStateId.State2_Sub2, ctx.PreviousStateId);
+
_msgService.Counter1++;
return base.OnEnter(ctx);
}
@@ -129,6 +147,8 @@ public State3(IMessageService msg)
public override Task OnEnter(Context ctx)
{
+ Assert.AreEqual(CustomStateId.State2_Success, ctx.PreviousStateId);
+
_msgService.Counter3++;
return base.OnEnter(ctx);
}
diff --git a/source/Lite.StateMachine/Context.cs b/source/Lite.StateMachine/Context.cs
index 6ad01c6..fb4b80b 100644
--- a/source/Lite.StateMachine/Context.cs
+++ b/source/Lite.StateMachine/Context.cs
@@ -47,12 +47,12 @@ internal Context(
/// Gets result emitted by the last child state (for composite parents only).
public Result? LastChildResult { get; }
- /////// Gets the previous state's enum value.
- ////public TStateId PreviousState { get; internal set; }
-
/// Gets or sets an arbitrary parameter provided by caller to the current action.
public PropertyBag Parameters { get; set; } = [];
+ /// Gets the previous state's enum value.
+ public TStateId? PreviousStateId { get; internal set; }
+
/// Signal the machine to move forward (only once per state entry).
/// Result to pass to the next state.
/// Consider renaming to `StateResult` or `Result` for clarity.
diff --git a/source/Lite.StateMachine/StateMachine.cs b/source/Lite.StateMachine/StateMachine.cs
index 715d3f3..755160f 100644
--- a/source/Lite.StateMachine/StateMachine.cs
+++ b/source/Lite.StateMachine/StateMachine.cs
@@ -205,11 +205,13 @@ public async Task> RunAsync(
if (!_states.ContainsKey(initialStateId))
throw new MissingInitialStateException($"Initial state '{initialStateId}' was not registered.");
- var current = initialStateId;
+ TStateId? prevStateId = null;
+ var currentStateId = initialStateId;
while (!cancellationToken.IsCancellationRequested)
{
- var reg = GetRegistration(current);
+ var reg = GetRegistration(currentStateId);
+ reg.PreviousStateId = prevStateId;
// TODO (2025-12-28 DS): Configure context and pass it along
////var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -231,7 +233,8 @@ public async Task> RunAsync(
if (nextId is null)
break;
- current = nextId.Value;
+ prevStateId = currentStateId;
+ currentStateId = nextId.Value;
}
return this;
@@ -310,6 +313,7 @@ private StateRegistration GetRegistration(TStateId stateId)
{
Parameters = parameters,
Errors = errors,
+ PreviousStateId = reg.PreviousStateId,
};
await instance.OnEntering(parentEnterCtx).ConfigureAwait(false);
@@ -337,10 +341,14 @@ private StateRegistration GetRegistration(TStateId stateId)
var childId = reg.InitialChildId.Value;
Result? lastChildResult = null;
+ // Set the initial substate's PreviousStateId to NULL, as we already know the parent.
+ TStateId? childPrevStateId = null;
+
// Composite Loop
while (!ct.IsCancellationRequested)
{
var childReg = GetRegistration(childId);
+ childReg.PreviousStateId = childPrevStateId;
// The child state was not registered the specified composite parent state
if (!Equals(childReg.ParentId, reg.StateId))
@@ -371,6 +379,7 @@ private StateRegistration GetRegistration(TStateId stateId)
throw new DisjointedNextSubStateException($"Child '{childId}' maps to '{nextChildId}', which is not a sibling under '{reg.StateId}'.");
// Proceed to the next substate
+ childPrevStateId = childId;
childId = nextChildId.Value;
}
@@ -433,6 +442,7 @@ private StateRegistration GetRegistration(TStateId stateId)
{
Parameters = parameterStack ?? [],
Errors = errorStack ?? [],
+ PreviousStateId = reg.PreviousStateId,
};
IDisposable? subscription = null;
diff --git a/source/Lite.StateMachine/StateRegistration.cs b/source/Lite.StateMachine/StateRegistration.cs
index af10d96..90cc6b2 100644
--- a/source/Lite.StateMachine/StateRegistration.cs
+++ b/source/Lite.StateMachine/StateRegistration.cs
@@ -32,10 +32,9 @@ internal sealed class StateRegistration
/// Gets the sub-state's parent State Id (optional).
public TStateId? ParentId { get; init; }
+ /// Gets or sets the Previous we transitioned from.
+ public TStateId? PreviousStateId { get; set; }
+
/// Gets the State Id, used by ExportUml for .
public TStateId StateId { get; init; }
-
- //// INFO: Though dict lookup is more flexible, properties are faster. Leaving this here for reference.
- /////// Gets the state result transitions OnSuccess, OnError, OnFailure.
- ////public Dictionary Transitions { get; } = new();
}
diff --git a/source/Sample.Basics/States/DemoMachine.cs b/source/Sample.Basics/States/DemoMachine.cs
index b4e1d09..cdaa5cf 100644
--- a/source/Sample.Basics/States/DemoMachine.cs
+++ b/source/Sample.Basics/States/DemoMachine.cs
@@ -19,7 +19,8 @@ public static class DemoMachine
public static async Task RunAsync(bool logOutput = true)
{
var counter = 0;
- var ctxProperties = new PropertyBag() {
+ var ctxProperties = new PropertyBag()
+ {
{ ParameterType.Counter, counter },
{ ParameterType.LogOutput, logOutput },
};