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: 0 additions & 41 deletions docs/Concept Designs - Feature Considerations for 2026.md

This file was deleted.

77 changes: 77 additions & 0 deletions docs/Concept Designs - vNext Feature Requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Lite.State - Feature Requests

**Table of Contents:**

* [Lite.State - Feature Requests](#litestate---feature-requests)
* [Adopted Feature Requests](#adopted-feature-requests)
* [Defining States](#defining-states)
* [Basic State](#basic-state)
* [Composite State](#composite-state)
* [Last Defined State - Exit machine or stay at last state](#last-defined-state---exit-machine-or-stay-at-last-state)
* [Option to generate DotGraph of state transitions](#option-to-generate-dotgraph-of-state-transitions)
* [Custom Event Aggregator](#custom-event-aggregator)

## Adopted Feature Requests

* [x] Generate DOT Graph
* [x] Simplify registration of Composite states allow for fluent

## Defining States

**Date:** 2025-12-17

### Basic State

* [ ] Simplify the registration of states to use `RegisterState<T>(...);`

### Composite State

1. Register composite to:
1. [ ] No longer require double-registration
2. [ ] Pass in initial sub-state

```cs
// Composite State
machine.RegisterState<State2>(
stateId: StateId.State2,
onSuccess: StateId.State3,
initialState: StateId.State2_Sub1,
subStates: (sub) =>
{
sub.RegisterState<State2_Sub1>(stateId: StateId.State2_Sub1);
});

```

## Last Defined State - Exit machine or stay at last state

**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.
22 changes: 11 additions & 11 deletions source/Lite.State.Tests/StateTests/BasicStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public void RegisterState_Transition_SuccessTest()
{
// Assemble
var machine = new StateMachine<StateId>();
machine.RegisterState(new State1());
machine.RegisterState(new State2());
machine.RegisterState(new State3());
machine.RegisterState(StateId.State1, () => new State1());
machine.RegisterState(StateId.State2, () => new State2());
machine.RegisterState(StateId.State3, () => new State3());

// Set starting point
machine.SetInitial(StateId.State1);
Expand All @@ -46,10 +46,10 @@ public void RegisterState_Transition_SuccessTest()

// Ensure all states are hit
Assert.AreEqual(enums.Count(), machine.States.Count());
Assert.IsTrue(enums.All(k => machine.States.Keys.Contains(k)));
Assert.IsTrue(enums.All(k => machine.States.Contains(k)));

// Ensure they're in order
Assert.IsTrue(enums.SequenceEqual(machine.States.Keys));
Assert.IsTrue(enums.SequenceEqual(machine.States));
}

/// <summary>Defines State Enum ID and `OnSuccess` transitions from the `RegisterStateEx` method.</summary>
Expand All @@ -58,9 +58,9 @@ public void RegisterStateEx_Transitions_SuccessTest()
{
// Assemble
var machine = new StateMachine<StateId>()
.RegisterStateEx(new StateEx1(StateId.State1), StateId.State2)
.RegisterStateEx(new StateEx2(StateId.State2), StateId.State3)
.RegisterStateEx(new StateEx3(StateId.State3));
.RegisterState(StateId.State1, () => new StateEx1(StateId.State1), StateId.State2)
.RegisterState(StateId.State2, () => new StateEx2(StateId.State2), StateId.State3)
.RegisterState(StateId.State3, () => new StateEx3(StateId.State3));

// Set starting point
machine.SetInitial(StateId.State1);
Expand All @@ -81,9 +81,9 @@ public void RegisterStateEx_WithoutInitialContextTransitions_SuccessTest()
{
// Assemble
var machine = new StateMachine<StateId>()
.RegisterStateEx(new StateEx1(StateId.State1), StateId.State2)
.RegisterStateEx(new StateEx2(StateId.State2), StateId.State3)
.RegisterStateEx(new StateEx3(StateId.State3))
.RegisterState(StateId.State1, () => new StateEx1(StateId.State1), StateId.State2)
.RegisterState(StateId.State2, () => new StateEx2(StateId.State2), StateId.State3)
.RegisterState(StateId.State3, () => new StateEx3(StateId.State3))
.SetInitialEx(StateId.State1);

// Act - Start your engine!
Expand Down
35 changes: 18 additions & 17 deletions source/Lite.State.Tests/StateTests/CommandStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public enum WorkflowState
AwaitMessage, // Command state
Done,
Error,
Failed
Failed,
}

[TestMethod]
Expand All @@ -34,26 +34,27 @@ public void TransitionWithErrorToSuccessTest()

var machine = new StateMachine<WorkflowState>(aggregator)
{
DefaultTimeoutMs = 3000 // as requested (can override per-command state)
// Set default timeout to 3 seconds (can override per-command state)
DefaultTimeoutMs = 3000,
};

// 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.RegisterState(WorkflowState.Start, () => new StartState());
machine.RegisterState(WorkflowState.Processing, () => new ProcessingState());
machine.RegisterState(WorkflowState.Processing, (sub) =>
{
// Register sub-states inside Processing's submachine
sub.RegisterState(WorkflowState.Load, () => new LoadState());
sub.RegisterState(WorkflowState.Validate, () => new ValidateState());
sub.SetInitial(WorkflowState.Load);
});
machine.RegisterState(WorkflowState.AwaitMessage, () => new AwaitMessageState());
machine.RegisterState(WorkflowState.Done, () => new DoneState());
machine.RegisterState(WorkflowState.Error, () => new ErrorState());
machine.RegisterState(WorkflowState.Failed, () => new FailedState());

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

// ====================
// Act - Start workflow
Expand Down
47 changes: 27 additions & 20 deletions source/Lite.State.Tests/StateTests/CompositeStateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,24 @@ public enum StateId
public void RegisterState_TransitionWithError_ToSuccessTest()
{
// Assemble
// NOTE: Composite state has 2 registrations, 1st for the state, 2nd for the children
// POC - Register composite with initial; no longer require double-registration
// machine.RegisterState<State2>(stateId: StateId.State2, onSuccess: xxx, initState: StateId.State2_Sub1, subStates: (sub) => { ... });
var machine = new StateMachine<StateId>();
machine.RegisterState(new State1(StateId.State1));
machine.RegisterState(StateId.State1, () => new State1(StateId.State1));

// Sub-state machine (Composite State + children
// POC - Register Initial: var comState2 = new State2(StateId.State2, StateId.State2_Sub1);
var comState2 = new State2(StateId.State2);
machine.RegisterState(comState2);
var subMachine = comState2.Submachine;
subMachine.RegisterState(new State2_Sub1(StateId.State2_Sub1));
subMachine.RegisterState(new State2_Sub2(StateId.State2_Sub2));
machine.RegisterState(StateId.State2, () => new State2(StateId.State2));
machine.RegisterState(StateId.State2, (sub) =>
{
sub.RegisterState(StateId.State2_Sub1, () => new State2_Sub1(StateId.State2_Sub1));
sub.RegisterState(StateId.State2_Sub2, () => new State2_Sub2(StateId.State2_Sub2));
sub.SetInitial(StateId.State2_Sub1);
});

machine.RegisterState(new State3(StateId.State3));
machine.RegisterState(StateId.State3, () => new State3(StateId.State3));

// Configure initial states
machine.SetInitial(StateId.State1);
subMachine.SetInitial(StateId.State2_Sub1);

// Act
machine.Start();
Expand All @@ -55,15 +57,18 @@ public void RegisterStateEx_Fluent_SuccessTest()
var comState2 = new StateEx2(StateId.State2);

var machine = new StateMachine<StateId>()
.RegisterStateEx(new StateEx1(StateId.State1), StateId.State2)
.RegisterStateEx(comState2, StateId.State3)
.RegisterStateEx(new StateEx3(StateId.State3))
.SetInitialEx(StateId.State1);
.RegisterState(StateId.State1, () => new StateEx1(StateId.State1), StateId.State2)
.RegisterState(StateId.State2, () => new StateEx2(StateId.State2), StateId.State3);

comState2.Submachine
.RegisterStateEx(new StateEx2_Sub1(StateId.State2_Sub1))
.RegisterStateEx(new StateEx2_Sub2(StateId.State2_Sub2))
.SetInitial(StateId.State2_Sub1);
machine.RegisterState(StateId.State2, (sub) =>
{
sub.RegisterState(StateId.State2_Sub1, () => new StateEx2_Sub1(StateId.State2_Sub1));
sub.RegisterState(StateId.State2_Sub2, () => new StateEx2_Sub2(StateId.State2_Sub2));
sub.SetInitial(StateId.State2_Sub1);
});

machine.RegisterState(StateId.State3, () => new StateEx3(StateId.State3))
.SetInitialEx(StateId.State1);

// Act
machine.Start();
Expand All @@ -78,12 +83,13 @@ public void RegisterStateEx_Fluent_SuccessTest()
[Ignore("Intermixing parent and sub-states fluent design does not work yet.")]
public void RegisterStateEx_Fluent_ProofOfConcept_SuccessTest()
{
/*
// Assemble
var comState2 = new StateEx2(StateId.State2);

var machine = new StateMachine<StateId>()
.RegisterStateEx(new StateEx1(StateId.State1), StateId.State2)
.RegisterStateEx(comState2, StateId.State3)
.RegisterState(StateId.State1, () => new StateEx1(StateId.State1), StateId.State2)
.RegisterState(comState2, StateId.State3)
////.RegisterStateEx(
//// new StateEx2(StateId.State2).Submachine
//// .RegisterStateEx(new StateEx2_Sub1(StateId.State2_Sub1))
Expand All @@ -105,6 +111,7 @@ public void RegisterStateEx_Fluent_ProofOfConcept_SuccessTest()
var ctxFinal = machine.Context.Parameters;
Assert.IsNotNull(ctxFinal);
Assert.AreEqual(SUCCESS, ctxFinal[PARAM_SUB_ENTERED]);
*/
}

#region State Machine - Regular
Expand Down
20 changes: 8 additions & 12 deletions source/Lite.State.Tests/StateTests/ErrorStateExTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ public void TransitionWithErrorToSuccessTest()
{
// Assemble
var machine = new StateMachine<StateId>()
.RegisterStateEx(new State1(StateId.State1), StateId.State2)
.RegisterStateEx(new State2(StateId.State2), StateId.State3, StateId.State2Error)
.RegisterStateEx(new State2Error(StateId.State2Error), StateId.State2)
.RegisterStateEx(new State3(StateId.State3));

// Set starting point
machine.SetInitial(StateId.State1);
.RegisterState(StateId.State1, () => new State1(StateId.State1), StateId.State2)
.RegisterState(StateId.State2, () => new State2(StateId.State2), StateId.State3, StateId.State2Error)
.RegisterState(StateId.State2Error, () => new State2Error(StateId.State2Error), StateId.State2)
.RegisterState(StateId.State3, () => new State3(StateId.State3))
.SetInitialEx(StateId.State1);

// Act - Start your engine!
var ctxProperties = new PropertyBag() { { PARAM_TEST, "not-finished" }, };
Expand Down Expand Up @@ -87,15 +85,13 @@ public override void OnEnter(Context<StateId> context)
private class State3(StateId id)
: BaseState<StateId>(id)
{
public override void OnEnter(Context<StateId> context) =>
context.NextState(Result.Ok);

public override void OnEntering(Context<StateId> context)
{
context.Parameters[PARAM_TEST] = SUCCESS;
Console.WriteLine("[State3] OnEntering");
}

public override void OnEnter(Context<StateId> context)
{
context.NextState(Result.Ok);
}
}
}
8 changes: 4 additions & 4 deletions source/Lite.State.Tests/StateTests/ErrorStateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public void TransitionWithErrorToSuccessTest()
// Assemble
var machine = new StateMachine<StateId>();

machine.RegisterState(new State1());
machine.RegisterState(new State2());
machine.RegisterState(new State2Error(StateId.State2Error));
machine.RegisterState(new State3(StateId.State3));
machine.RegisterState(StateId.State1, () => new State1());
machine.RegisterState(StateId.State2, () => new State2());
machine.RegisterState(StateId.State2Error, () => new State2Error(StateId.State2Error));
machine.RegisterState(StateId.State3, () => new State3(StateId.State3));

// Set starting point
machine.SetInitial(StateId.State1);
Expand Down
Loading