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
41 changes: 41 additions & 0 deletions docs/Concept Designs - Feature Considerations for 2026.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Lite.State - Feature Considerations

**Table of Contents:**

* [Lite.State - Feature Considerations](#litestate---feature-considerations)
* [How the system treats the last defined state transition](#how-the-system-treats-the-last-defined-state-transition)
* [Option to generate DotGraph of state transitions](#option-to-generate-dotgraph-of-state-transitions)
* [Custom Event Aggregator](#custom-event-aggregator)

## How the system treats the last defined state transition

**Date:** 2025-12-15

1. Sit at the last state and wait until told to go to the next state
* Awaits, context.NextState(<StateId>)
* PROs:
* Waits for the user to inform it.
* Idle state sits and waits for a triggering `OnMessage` without a defined `timeout`.
* CONs:
* Can sit without wraning
2. Auto-exit the StateMachine
* PROs:
* We could be done and can auto close the operation or application.
* CONs:
* Undesired exit of the operations/application

## Option to generate DotGraph of state transitions

1. PROs:
* Early discoverery of errors
* Auto-generated documentation
2. CONs:
* N/A
3. Limitations:
* Custom transitions may not be represented

## Custom Event Aggregator

Allow for built-in or 3rd-party event aggregator system.

Requires interfaces and API hooks.
9 changes: 6 additions & 3 deletions docs/Concept Designs.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ public class CarBuilder
public CarBuilder WithMake(string make) { _car.Make = make; return this; }
public CarBuilder WithModel(string model) { _car.Model = model; return this; }
public CarBuilder WithColor(string color) { _car.Color = color; return this; }

// Builder's `Build` method.
public Car Build() { return _car; }
}
```

## Chosen
## Chosen


## Design Concepts
Expand Down Expand Up @@ -122,7 +122,7 @@ public int MultiSuccess_OnEnter(Context params)

* CON: How to strictly define which success state to exit to?
* Cannot say, `return State.Success` - there are multiple successes
* CON: Cannot use `return State.Success;`
* CON: Cannot use `return State.Success;`

#### 4) FREE - User sets whatever

Expand Down Expand Up @@ -298,6 +298,9 @@ StateId InitOnEnter()

## Model B - State Defined Transitions

* Date: 2022-12-15
* Modified: 2022-12-16

```cpp
enum StateId =
{
Expand Down
279 changes: 279 additions & 0 deletions source/Lite.State.Tests/StateTests/CommandStateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// Copyright Xeno Innovations, Inc. 2025
// See the LICENSE file in the project root for more information.

using System;
using System.Threading.Tasks;

namespace Lite.State.Tests.StateTests;

[TestClass]
public class CommandStateTests
{
public const string MessageTypeTest = "MessageType-Sent";
public const string ParameterKeyTest = "TestKey";
public const string TestValueBegin = "Initial-Value";
public const string TestValueEnd = "Expected-Value";

public enum WorkflowState
{
Start,
Processing, // Composite state
Load, // Sub-state of Processing
Validate, // Sub-state of Processing
AwaitMessage, // Command state
Done,
Error,
Failed
}

[TestMethod]
public void TransitionWithErrorToSuccessTest()
{
// Assemble
var aggregator = new EventAggregator();

var machine = new StateMachine<WorkflowState>(aggregator)
{
DefaultTimeoutMs = 3000 // as requested (can override per-command state)
};

// Register top-level states
machine.RegisterState(new StartState());
var processing = new ProcessingState();
machine.RegisterState(processing);
machine.RegisterState(new AwaitMessageState());
machine.RegisterState(new DoneState());
machine.RegisterState(new ErrorState());
machine.RegisterState(new FailedState());

// Register sub-states inside Processing's submachine
var sub = processing.Submachine;
sub.RegisterState(new LoadState());
sub.RegisterState(new ValidateState());

// Set initials
machine.SetInitial(WorkflowState.Start);
sub.SetInitial(WorkflowState.Load);

// ====================
// Act - Start workflow
var ctx = new PropertyBag { { ParameterKeyTest, TestValueBegin } };
machine.Start(ctx);

// Act - Simulate publishing a message to complete command state
// Publish after ~1 second (within timeout) — change timing to test OnTimeout
var msgObject = new PropertyBag() { { MessageTypeTest, TestValueBegin } };
Task.Delay(1000).ContinueWith(_ => aggregator.Publish(msgObject));

// Keep console alive to observe async timeout/message handling
Task.Delay(5000).Wait();

// =================
// Assert
var ctxFinal = machine.Context.Parameters;
Assert.IsNotNull(ctxFinal);
Assert.AreEqual(TestValueEnd, ctxFinal[ParameterKeyTest]);
}

// Command state: AwaitMessage (listens to event aggregator; timeout defaults to 3000ms)
public sealed class AwaitMessageState : CommandState<WorkflowState>
{
public AwaitMessageState() : base(WorkflowState.AwaitMessage)
{
AddTransition(Result.Ok, WorkflowState.Done);
AddTransition(Result.Error, WorkflowState.Error);
AddTransition(Result.Failure, WorkflowState.Failed);
}

// Optionally override default timeout:
// public override int? TimeoutOverrideMs => 5000;

// Filter messages (optional). Here we only accept string messages that begin with "go".
////public override Func<object, bool> MessageFilter => msg =>
//// msg is string s && s.StartsWith("go", StringComparison.OrdinalIgnoreCase);

public override void OnEnter(Context<WorkflowState> context)
{
Console.WriteLine("[AwaitMessage] OnEnter (subscribed; awaiting message)");
}

public override void OnEntering(Context<WorkflowState> context)
{
Console.WriteLine("[AwaitMessage] OnEntering");
}

public override void OnExit(Context<WorkflowState> context)
{
Console.WriteLine("[AwaitMessage] OnExit (unsubscribed; timer cancelled)");
}

public override void OnMessage(Context<WorkflowState> context, object message)
{
Console.WriteLine($"[AwaitMessage] OnMessage: '{message}' (timeout cancelled)");

if (message is PropertyBag prop &&
prop is not null &&
prop.ContainsKey(MessageTypeTest) &&
prop[MessageTypeTest].Equals(TestValueBegin))
{
context.Parameters[ParameterKeyTest] = TestValueEnd;
context.NextState(Result.Ok);
}
else
{
context.NextState(Result.Error);
}

// Decide outcome based on message content
////if (message is string s && s.Contains("error", StringComparison.OrdinalIgnoreCase))
//// context.NextState(Result.Error);
////else
//// context.NextState(Result.Ok);
}

public override void OnTimeout(Context<WorkflowState> context)
{
Console.WriteLine("[AwaitMessage] OnTimeout: no messages received in time.");
context.NextState(Result.Failure);
}
}

// Terminal states
public sealed class DoneState : BaseState<WorkflowState>
{
public DoneState() : base(WorkflowState.Done)
{
}

public override void OnEnter(Context<WorkflowState> context) =>
Console.WriteLine("[Done] OnEnter — workflow complete.");

public override void OnEntering(Context<WorkflowState> context) =>
Console.WriteLine("[Done] OnEntering");

public override void OnExit(Context<WorkflowState> context) =>
Console.WriteLine("[Done] OnExit");
}

public sealed class ErrorState : BaseState<WorkflowState>
{
public ErrorState() : base(WorkflowState.Error)
{
}

public override void OnEnter(Context<WorkflowState> context) =>
Console.WriteLine("[Error] OnEnter");
}

public sealed class FailedState : BaseState<WorkflowState>
{
public FailedState() : base(WorkflowState.Failed)
{
}

public override void OnEnter(Context<WorkflowState> context) =>
Console.WriteLine("[Failed] OnEnter");
}

// Sub-state: Load (belongs to Processing submachine)
public sealed class LoadState : BaseState<WorkflowState>
{
public LoadState() : base(WorkflowState.Load)
{
AddTransition(Result.Ok, WorkflowState.Validate);
AddTransition(Result.Error, WorkflowState.Validate); // Example: still go validate to confirm
AddTransition(Result.Failure, WorkflowState.Validate); // Example: still go validate
}

public override void OnEnter(Context<WorkflowState> context)
{
Console.WriteLine("[Load] OnEnter (loading resources)");

// Simulate outcome
context.NextState(Result.Ok);
}

public override void OnEntering(Context<WorkflowState> context) =>
Console.WriteLine("[Load] OnEntering (sub)");

public override void OnExit(Context<WorkflowState> context) =>
Console.WriteLine("[Load] OnExit (sub)");
}

// Composite state: Processing (submachine controls Load -> Validate)
public sealed class ProcessingState : CompositeState<WorkflowState>
{
public ProcessingState() : base(WorkflowState.Processing)
{
// When submachine is done and bubbles Outcome:
// This parent state's transitions will be applied.
AddTransition(Result.Ok, WorkflowState.AwaitMessage);
AddTransition(Result.Error, WorkflowState.Error);
AddTransition(Result.Failure, WorkflowState.Failed);
}

public override void OnEnter(Context<WorkflowState> context) =>
Console.WriteLine("[Processing] OnEnter (starting submachine)");

public override void OnEntering(Context<WorkflowState> context) =>
Console.WriteLine("[Processing] OnEntering");

public override void OnExit(Context<WorkflowState> context) =>
Console.WriteLine("[Processing] OnExit (submachine exhausted)");
}

// Regular state: Start
public sealed class StartState : BaseState<WorkflowState>
{
public StartState() : base(WorkflowState.Start)
{
// Decide where to go based on outcome
AddTransition(Result.Ok, WorkflowState.Processing);
AddTransition(Result.Error, WorkflowState.Error);
AddTransition(Result.Failure, WorkflowState.Failed);
}

public override void OnEnter(Context<WorkflowState> context)
{
Console.WriteLine($"[Start] OnEnter, Parameter='{context.Parameters[ParameterKeyTest]}'");

// Simulate work; then decide outcome
context.NextState(Result.Ok);
}

public override void OnEntering(Context<WorkflowState> context) =>
Console.WriteLine("[Start] OnEntering");

public override void OnExit(Context<WorkflowState> context) =>
Console.WriteLine("[Start] OnExit");
}

// Sub-state: Validate (last sub-state; no local transition on Ok -> bubbles up)
public sealed class ValidateState : BaseState<WorkflowState>
{
public ValidateState() : base(WorkflowState.Validate)
{
// Local mapping only for non-OK; OK intentionally not mapped to demonstrate bubble-up.
AddTransition(Result.Error, WorkflowState.Validate); // example self-loop error check
AddTransition(Result.Failure, WorkflowState.Validate); // example self-loop failure check
}

public override void OnEnter(Context<WorkflowState> context)
{
Console.WriteLine("[Validate] OnEnter (checking data)");

// Suppose validation passed: bubble up to Processing
context.NextState(Result.Ok);
}

public override void OnEntering(Context<WorkflowState> context)
{
Console.WriteLine("[Validate] OnEntering (sub)");
}

public override void OnExit(Context<WorkflowState> context)
{
Console.WriteLine("[Validate] OnExit (sub)");
}
}
}
4 changes: 4 additions & 0 deletions source/Lite.State.Tests/StateTests/CompositeStateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ private class State3(StateId id)
{
public override void OnEnter(Context<StateId> context)
{
// NOTE: Not needed, as this is the "last state"
// FUTURE CONSIDERATIONS:
// 1. Sit at this state, as it could be intended or an error
// 2. Or continue to allow the system to auto-exit (possible undesirable outcomes)
// context.NextState(Result.Ok);
}
}
Expand Down
Loading