Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ jobs:

## 10) Publish to NuGet.org (master branch, push events only)
- name: 8) NuGet Push Package
if: startsWith(github.ref, 'refs/tags/')
env:
NUGET_API_URL: "https://api.nuget.org/v3/index.json"

## Optionally pass, `--skip-duplicate` if a pkg version already exists on NuGet.org
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
run: dotnet nuget push ${{env.PATH_ARTIFACTS}}\*.nupkg -k ${{secrets.NUGET_AUTH_TOKEN}} -s ${{env.NUGET_API_URL}}

#- name: Publish to NuGet.org
Expand Down
97 changes: 97 additions & 0 deletions source/Lite.StateMachine.Tests/StateTests/ContextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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.Tests.TestData.States;

namespace Lite.StateMachine.Tests.StateTests;

[TestClass]
public class ContextTests
{
public const string ParameterCounter = "Counter";
public const string ParameterKeyTest = "TestKey";
public const string TestValue = "success";

private enum CtxStateId
{
State1,
State2,
State3,
}

private enum ParameterType
{
Param1,
Param2,
Param3,
}

public TestContext TestContext { get; set; }

/// <summary>Standard synchronous state registration exiting to completion.</summary>
[TestMethod]
public void Basic_RegisterState_Executes123_SuccessTest()
{
// Assemble
var ctxProperties = new PropertyBag() { { "KeyString_ValueInt", 99 } };

var machine = new StateMachine<CtxStateId>();
machine.RegisterState<CtxState1>(CtxStateId.State1, CtxStateId.State2);
machine.RegisterState<CtxState2>(CtxStateId.State2, CtxStateId.State3);
machine.RegisterState<CtxState3>(CtxStateId.State3);

// Act - Non async Start your engine!
var task = machine.RunAsync(CtxStateId.State1, ctxProperties);
task.GetAwaiter().GetResult();

// Assert Results
Assert.IsNotNull(machine);
Assert.IsNull(machine.Context);

// Ensure all states are registered
var enums = Enum.GetValues<CtxStateId>().Cast<CtxStateId>();
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're registered in order
Assert.IsTrue(enums.SequenceEqual(machine.States), "States should be registered for execution in the same order as the defined enums, StateId 1 => 2 => 3.");
}

private class CtxState1 : StateBase<CtxState1, CtxStateId>
{
public override Task OnEnter(Context<CtxStateId> context)
{
context.Parameters.SafeAdd(ParameterType.Param1, "KeyEnum_ValueString (2nd Item)");
return base.OnEnter(context);
}
}

private class CtxState2 : StateBase<CtxState2, CtxStateId>
{
public override Task OnEnter(Context<CtxStateId> context)
{
context.Parameters.SafeAdd(ParameterType.Param2, "KeyEnum_ValueString (3rd Item)");
return base.OnEnter(context);
}
}

private class CtxState3 : StateBase<CtxState3, CtxStateId>
{
public override Task OnEnter(Context<CtxStateId> context)
{
context.Parameters.SafeAdd(ParameterType.Param2, "KeyEnum_ValueString (3rd-Item UPDATED)");
context.Parameters.SafeAdd("KeyString", "ValueString (4th Item)");
context.Parameters.SafeAdd(1, "KeyInt_ValueString (Last Item)");
context.Parameters.SafeAdd("Key6_NullValue", null);

foreach (var x in context.Parameters)
Console.WriteLine($"{x.Key}: '{x.Value}'");

Assert.HasCount(6, context.Parameters);
return base.OnEnter(context);
}
}
}
23 changes: 23 additions & 0 deletions source/Lite.StateMachine.Tests/TestData/States/StateBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright Xeno Innovations, Inc. 2025
// See the LICENSE file in the project root for more information.

using System;
using System.Threading.Tasks;

namespace Lite.StateMachine.Tests.TestData.States;

public class StateBase<TStateClass, TStateId> : IState<TStateId>
where TStateId : struct, Enum
{
public virtual Task OnEnter(Context<TStateId> context)
{
context.NextState(Result.Ok);
return Task.CompletedTask;
}

public virtual Task OnEntering(Context<TStateId> context) =>
Task.CompletedTask;

public virtual Task OnExit(Context<TStateId> context) =>
Task.CompletedTask;
}
4 changes: 2 additions & 2 deletions source/Lite.StateMachine/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ internal Context(
/// <param name="result">Result to pass to the next state.</param>
public void NextState(Result result) => _tcs.TrySetResult(result);

public bool ParameterAsBool(string key, bool defaultBool = false)
public bool ParameterAsBool(object key, bool defaultBool = false)
{
if (Parameters.TryGetValue(key, out var value) && value is bool boolValue)
return boolValue;
Expand All @@ -59,7 +59,7 @@ public bool ParameterAsBool(string key, bool defaultBool = false)
/// <param name="key">Parameter Key.</param>
/// <param name="defaultInt">Default int (default=0).</param>
/// <returns>Integer or default.</returns>
public int ParameterAsInt(string key, int defaultInt = 0)
public int ParameterAsInt(object key, int defaultInt = 0)
{
if (Parameters.TryGetValue(key, out var value) && value is int intValue)
return intValue;
Expand Down
2 changes: 1 addition & 1 deletion source/Lite.StateMachine/Lite.StateMachine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<AssemblyVersion>2.1.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<VersionPrefix>$(AssemblyVersion)</VersionPrefix>
<VersionSuffix>-beta3</VersionSuffix>
<VersionSuffix>-beta4</VersionSuffix>
<Version>$(VersionPrefix)$(VersionSuffix)</Version>

<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
Expand Down
17 changes: 14 additions & 3 deletions source/Lite.StateMachine/PropertyBag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@
namespace Lite.StateMachine;

[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "No need to waste a file.")]
public interface IPropertyBag : IDictionary<string, object>;
public interface IPropertyBag<TKey> : IDictionary<TKey, object?>
{
void SafeAdd(object key, object? value);
}

/// <summary>Context parameter stack properties for passing data between states.</summary>
/// <typeparam name="TKey">TKey is Key.</typeparam>
/// <remarks>
/// In a future release, make the keys flexible so we can use enums, strings, etc (2025-12-16 DS).
/// 1) Use thread-safe ConcurrentDictionary
/// 2) In a future release, make the keys only enums for fast execution (2025-12-16 DS).
/// <![CDATA[
/// public class PropertyBag<TKey, TValue> : Dictionary<TKey, TValue> where TKey : notnull
/// ]]>
/// </remarks>
public class PropertyBag : Dictionary<string, object>;
public class PropertyBag : Dictionary<object, object?>
{
public void SafeAdd(object key, object? value)
{
this[key] = value;
}
}
8 changes: 4 additions & 4 deletions source/Lite.StateMachine/StateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,8 @@ private StateRegistration<TStateId> GetRegistration(TStateId stateId)
//
// Any new Context keys added via OnEnter are considered "for children consumption only".
// After our OnExit, they'll be (optionally) removed.
var originalParamKeys = new HashSet<string>(parameters.Keys);
var originalErrorKeys = new HashSet<string>(errors.Keys);
var originalParamKeys = new HashSet<object>(parameters.Keys);
var originalErrorKeys = new HashSet<object>(errors.Keys);

await instance.OnEnter(parentEnterCtx).ConfigureAwait(false);

Expand Down Expand Up @@ -383,13 +383,13 @@ private StateRegistration<TStateId> GetRegistration(TStateId stateId)
{
if (parameters is not null)
{
foreach (var k in parameters.Keys)
foreach (string k in parameters.Keys)
if (!originalParamKeys.Contains(k)) parameters.Remove(k);
}

if (errors is not null)
{
foreach (var k in errors.Keys)
foreach (string k in errors.Keys)
if (!originalErrorKeys.Contains(k)) errors.Remove(k);
}
}
Expand Down