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
10 changes: 8 additions & 2 deletions source/Lite.StateMachine.Tests/StateTests/CommandStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ public async Task BasicState_Override_Executes_SuccessAsync()

machine
.AddContext(ctxProperties)
.RegisterState<State1>(StateId.State1, StateId.State2)
.RegisterState<State1>(StateId.State1, StateId.State2, subscriptionTypes: [typeof(UnlockResponse)])
.RegisterComposite<State2>(StateId.State2, initialChildStateId: StateId.State2_Sub1, onSuccess: StateId.State3)
.RegisterSubState<State2_Sub1>(StateId.State2_Sub1, parentStateId: StateId.State2, onSuccess: StateId.State2_Sub2)
.RegisterSubComposite<State2_Sub2>(StateId.State2_Sub2, parentStateId: StateId.State2, initialChildStateId: StateId.State2_Sub2_Sub1, onSuccess: StateId.State2_Sub3)
.RegisterSubState<State2_Sub2_Sub1>(StateId.State2_Sub2_Sub1, parentStateId: StateId.State2_Sub2, onSuccess: StateId.State2_Sub2_Sub2)
.RegisterSubState<State2_Sub2_Sub1>(StateId.State2_Sub2_Sub1, parentStateId: StateId.State2_Sub2, onSuccess: StateId.State2_Sub2_Sub2, subscriptionTypes: [typeof(UnlockResponse), typeof(CloseResponse)])
.RegisterSubState<State2_Sub2_Sub2>(StateId.State2_Sub2_Sub2, parentStateId: StateId.State2_Sub2, onSuccess: StateId.State2_Sub2_Sub3)
.RegisterSubState<State2_Sub2_Sub3>(StateId.State2_Sub2_Sub3, parentStateId: StateId.State2_Sub2, onSuccess: null)
.RegisterSubState<State2_Sub3>(StateId.State2_Sub3, parentStateId: StateId.State2, onSuccess: null)
Expand Down Expand Up @@ -140,7 +140,11 @@ public async Task CancelsInfiniteStateMachineTestAsync()
Assert.AreEqual(100, counter);
}

#pragma warning disable SA1124 // Do not use regions
#region Infinite Loop Test State Classes

private class InfState1 : IState<StateId>
#pragma warning restore SA1124 // Do not use regions
{
public Task OnEnter(Context<StateId> context)
{
Expand Down Expand Up @@ -178,4 +182,6 @@ public Task OnMessage(Context<StateId> context, object message)

public Task OnTimeout(Context<StateId> context) => Task.CompletedTask;
}

#endregion Infinite Loop Test State Classes
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public interface IMessageService
{
/// <summary
/// Gets or sets a counter.
/// <see cref="DiStateBase{TStateClass, TStateId}"/> uses it as an automatic state transition counter.
/// <see cref="StateDiBase{TStateClass, TStateId}"/> uses it as an automatic state transition counter.
/// </summary>
int Counter1 { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ namespace Lite.StateMachine.Tests.TestData.States;
#pragma warning disable SA1402 // File may only contain a single type

public class BasicDiState1(IMessageService msg, ILogger<BasicDiState1> log)
: DiStateBase<BasicDiState1, BasicStateId>(msg, log);
: StateDiBase<BasicDiState1, BasicStateId>(msg, log);

public class BasicDiState2(IMessageService msg, ILogger<BasicDiState2> log)
: DiStateBase<BasicDiState2, BasicStateId>(msg, log);
: StateDiBase<BasicDiState2, BasicStateId>(msg, log);

public class BasicDiState3(IMessageService msg, ILogger<BasicDiState3> log)
: DiStateBase<BasicDiState3, BasicStateId>(msg, log);
: StateDiBase<BasicDiState3, BasicStateId>(msg, log);

#pragma warning restore SA1649 // File name should match first type name
#pragma warning restore SA1402 // File may only contain a single type
59 changes: 32 additions & 27 deletions source/Lite.StateMachine.Tests/TestData/States/CommandL3States.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,14 @@ public enum StateId
Error,
}

public class CommonDiStateBase<TStateClass, TStateId>(IMessageService msg, ILogger<TStateClass> logger)
: DiStateBase<TStateClass, TStateId>(msg, logger)
where TStateId : struct, Enum
{
// Helper so we don't have to keep rewriting the same "override Task OnEnter(...)"
// 8 lines * 9 states.. useless
public override Task OnEnter(Context<TStateId> context)
{
context.Parameters.Add(context.CurrentStateId.ToString(), Guid.NewGuid());
MessageService.AddMessage($"[Keys-{context.CurrentStateId}]: {string.Join(",", context.Parameters.Keys)}");
return base.OnEnter(context);
}
}

public class State1(IMessageService msg, ILogger<State1> log)
: CommandStateBase<State1, StateId>(msg, log)
{
/// <summary>Gets message types for command state to subscribe to.</summary>
/// <remarks>
/// Already subscribed to in StateMachine builder. Defining twice to test that we don't
/// get duplicate messages.
/// </remarks>
public override IReadOnlyCollection<Type> SubscribedMessageTypes => new[]
{
//// typeof(OpenCommand), // <---- NOTE: Not needed
Expand Down Expand Up @@ -96,7 +86,7 @@ public override Task OnTimeout(Context<StateId> context)

/// <summary>Level-1: Composite.</summary>
public class State2(IMessageService msg, ILogger<State2> log)
: CommonDiStateBase<State2, StateId>(msg, log)
: StateDiMessageBase<State2, StateId>(msg, log)
{
#region CodeMaid - Suppress method sorting

Expand Down Expand Up @@ -127,14 +117,14 @@ public override Task OnExit(Context<StateId> context)

/// <summary>Sublevel-2: State.<summary>
public class State2_Sub1(IMessageService msg, ILogger<State2_Sub1> log)
: CommonDiStateBase<State2_Sub1, StateId>(msg, log)
: StateDiMessageBase<State2_Sub1, StateId>(msg, log)
{
public override Task OnEnter(Context<StateId> context) => base.OnEnter(context);
}

/// <summary>Sublevel-2: Composite.</summary>
public class State2_Sub2(IMessageService msg, ILogger<State2_Sub2> log)
: CommonDiStateBase<State2_Sub2, StateId>(msg, log)
: StateDiMessageBase<State2_Sub2, StateId>(msg, log)
{
#region CodeMaid - DoNotReorder

Expand Down Expand Up @@ -173,7 +163,7 @@ public override Task OnExit(Context<StateId> context)

/// <summary>Sublevel-3: State.</summary>
public class State2_Sub2_Sub1(IMessageService msg, ILogger<State2_Sub2_Sub1> log)
: CommonDiStateBase<State2_Sub2_Sub1, StateId>(msg, log)
: StateDiMessageBase<State2_Sub2_Sub1, StateId>(msg, log)
{
public override Task OnEnter(Context<StateId> context) => base.OnEnter(context);
}
Expand All @@ -182,12 +172,13 @@ public class State2_Sub2_Sub1(IMessageService msg, ILogger<State2_Sub2_Sub1> log
public class State2_Sub2_Sub2(IMessageService msg, ILogger<State2_Sub2_Sub2> log)
: CommandStateBase<State2_Sub2_Sub2, StateId>(msg, log)
{
/// <summary>Gets message types for command state to subscribe to.</summary>
public override IReadOnlyCollection<Type> SubscribedMessageTypes =>
[
typeof(UnlockResponse),
typeof(CloseResponse),
];
// Already subscribed to in StateMachine builder
/////// <summary>Gets message types for command state to subscribe to.</summary>
////public override IReadOnlyCollection<Type> SubscribedMessageTypes =>
////[
//// typeof(UnlockResponse),
//// typeof(CloseResponse),
////];

public override Task OnEnter(Context<StateId> context)
{
Expand Down Expand Up @@ -224,14 +215,14 @@ public override Task OnTimeout(Context<StateId> context)

/// <summary>Sublevel-3: Last State.</summary>
public class State2_Sub2_Sub3(IMessageService msg, ILogger<State2_Sub2_Sub3> log)
: CommonDiStateBase<State2_Sub2_Sub3, StateId>(msg, log)
: StateDiMessageBase<State2_Sub2_Sub3, StateId>(msg, log)
{
public override Task OnEnter(Context<StateId> context) => base.OnEnter(context);
}

/// <summary>Sublevel-2: Last State.</summary>
public class State2_Sub3(IMessageService msg, ILogger<State2_Sub3> log)
: DiStateBase<State2_Sub3, StateId>(msg, log)
: StateDiBase<State2_Sub3, StateId>(msg, log)
{
public override Task OnEnter(Context<StateId> context)
{
Expand All @@ -250,7 +241,7 @@ public override Task OnEnter(Context<StateId> context)

/// <summary>Make sure not child-created context is there.</summary>
public class State3(IMessageService msg, ILogger<State3> log)
: DiStateBase<State3, StateId>(msg, log)
: StateDiBase<State3, StateId>(msg, log)
{
public override Task OnEnter(Context<StateId> context)
{
Expand All @@ -263,6 +254,20 @@ public override Task OnEnter(Context<StateId> context)
}
}

public class StateDiMessageBase<TStateClass, TStateId>(IMessageService msg, ILogger<TStateClass> logger)
: StateDiBase<TStateClass, TStateId>(msg, logger)
where TStateId : struct, Enum
{
// Helper so we don't have to keep rewriting the same "override Task OnEnter(...)"
// 8 lines * 9 states.. useless
public override Task OnEnter(Context<TStateId> context)
{
context.Parameters.Add(context.CurrentStateId.ToString(), Guid.NewGuid());
MessageService.AddMessage($"[Keys-{context.CurrentStateId}]: {string.Join(",", context.Parameters.Keys)}");
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Lite.StateMachine.Tests.TestData.States;
/// <typeparam name="TStateClass">State class object.</typeparam>
/// <typeparam name="TStateId">State Id.</typeparam>
public class CommandStateBase<TStateClass, TStateId>(IMessageService msg, ILogger<TStateClass> logger)
: DiStateBase<TStateClass, TStateId>(msg, logger), ICommandState<TStateId>
: StateDiBase<TStateClass, TStateId>(msg, logger), ICommandState<TStateId>
where TStateId : struct, Enum
{
//// NEEDS TESTED: public virtual IReadOnlyCollection<ICustomCommand> SubscribedMessageTypes => [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ namespace Lite.StateMachine.Tests.TestData.States;
#pragma warning disable SA1402 // File may only contain a single type

public class EntryState(IMessageService msg, ILogger<EntryState> log)
: DiStateBase<EntryState, CompositeMsgStateId>(msg, log)
: StateDiBase<EntryState, CompositeMsgStateId>(msg, log)
{
}

public class ParentState(IMessageService msg, ILogger<ParentState> log)
: DiStateBase<ParentState, CompositeMsgStateId>(msg, log)
: StateDiBase<ParentState, CompositeMsgStateId>(msg, log)
{
/// <summary>Handle the result from our last child state.</summary>
/// <param name="context">Context data.</param>
Expand All @@ -40,12 +40,12 @@ public override Task OnExit(Context<CompositeMsgStateId> context)
}

public class ParentSub_FetchState(IMessageService msg, ILogger<ParentSub_FetchState> log)
: DiStateBase<ParentSub_FetchState, CompositeMsgStateId>(msg, log)
: StateDiBase<ParentSub_FetchState, CompositeMsgStateId>(msg, log)
{
}

public class ParentSub_WaitMessageState(IMessageService msg, ILogger<ParentSub_WaitMessageState> log)
: DiStateBase<ParentSub_WaitMessageState, CompositeMsgStateId>(msg, log),
: StateDiBase<ParentSub_WaitMessageState, CompositeMsgStateId>(msg, log),
ICommandState<CompositeMsgStateId>
{
public override Task OnEnter(Context<CompositeMsgStateId> context)
Expand Down Expand Up @@ -134,12 +134,12 @@ public Task OnTimeout(Context<CompositeMsgStateId> context)
}

public class Workflow_DoneState(IMessageService msg, ILogger<Workflow_DoneState> log)
: DiStateBase<Workflow_DoneState, CompositeMsgStateId>(msg, log)
: StateDiBase<Workflow_DoneState, CompositeMsgStateId>(msg, log)
{
}

public class Workflow_ErrorState(IMessageService msg, ILogger<Workflow_ErrorState> log)
: DiStateBase<Workflow_ErrorState, CompositeMsgStateId>(msg, log)
: StateDiBase<Workflow_ErrorState, CompositeMsgStateId>(msg, log)
{
public override Task OnEnter(Context<CompositeMsgStateId> context)
{
Expand All @@ -158,7 +158,7 @@ public override Task OnEnter(Context<CompositeMsgStateId> context)
}

public class Workflow_FailureState(IMessageService msg, ILogger<Workflow_FailureState> log)
: DiStateBase<Workflow_FailureState, CompositeMsgStateId>(msg, log)
: StateDiBase<Workflow_FailureState, CompositeMsgStateId>(msg, log)
{
public override Task OnEnter(Context<CompositeMsgStateId> context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
namespace Lite.StateMachine.Tests.TestData.States.CompositeL3DiStates;

public class CommonDiStateBase<TStateClass, TStateId>(IMessageService msg, ILogger<TStateClass> logger)
: DiStateBase<TStateClass, TStateId>(msg, logger)
: StateDiBase<TStateClass, TStateId>(msg, logger)
where TStateId : struct, Enum
{
// Helper so we don't have to keep rewriting the same "override Task OnEnter(...)"
Expand All @@ -29,7 +29,7 @@ public override Task OnEnter(Context<TStateId> context)
}

public class State1(IMessageService msg, ILogger<State1> log)
: DiStateBase<State1, CompositeL3>(msg, log)
: StateDiBase<State1, CompositeL3>(msg, log)
{
public override Task OnEnter(Context<CompositeL3> context)
{
Expand Down Expand Up @@ -154,7 +154,7 @@ public override Task OnEnter(Context<CompositeL3> context)

/// <summary>Sublevel-2: Last State.</summary>
public class State2_Sub3(IMessageService msg, ILogger<State2_Sub3> log)
: DiStateBase<State2_Sub3, CompositeL3>(msg, log)
: StateDiBase<State2_Sub3, CompositeL3>(msg, log)
{
public override Task OnEnter(Context<CompositeL3> context)
{
Expand All @@ -169,7 +169,7 @@ public override Task OnEnter(Context<CompositeL3> context)

/// <summary>Make sure not child-created context is there.</summary>
public class State3(IMessageService msg, ILogger<State3> log)
: DiStateBase<State3, CompositeL3>(msg, log)
: StateDiBase<State3, CompositeL3>(msg, log)
{
public override Task OnEnter(Context<CompositeL3> context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Lite.StateMachine.Tests.TestData.States;

#pragma warning disable SA1124 // Do not use regions

public class DiStateBase<TStateClass, TStateId>(IMessageService msg, ILogger<TStateClass> logger) : IState<TStateId>
public class StateDiBase<TStateClass, TStateId>(IMessageService msg, ILogger<TStateClass> logger) : IState<TStateId>
where TStateId : struct, Enum
{
private readonly ILogger<TStateClass> _logger = logger;
Expand Down
21 changes: 15 additions & 6 deletions source/Lite.StateMachine/IStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ public interface IStateMachine<TStateId>
/// <summary>Gets or sets the default <see cref="IState{TState}"/> timeout in milliseconds (<see cref="Timeout.Infinite"/>ms default). Set timeout to ensure no stuck states (i.e., robotics).</summary>
int DefaultStateTimeoutMs { get; set; }

/// <summary>Gets or sets a value indicating whether substate-added context persists when returning to the parent (default: true).</summary>
bool IsContextPersistent { get; set; }

/// <summary>Gets the collection of all registered states.</summary>
/// <remarks>
/// Exposed for validations, debugging, etc.
/// Previously: <![CDATA[Dictionary<TStateId, IState<TStateId>>]]>.
/// </remarks>
List<TStateId> States { get; }

/// <summary>Preload properties and errors to the context.</summary>
/// <param name="parameters">Parameter properties to safely add/update.</param>
/// <param name="errors">Error properties to safely add/update.</param>
/// <returns>Instance of this class.</returns>
StateMachine<TStateId> AddContext(PropertyBag? parameters = null, PropertyBag? errors = null);

/// <summary>
/// Registers a top-level composite parent state (has no parent state) and explicitly sets:
/// - the initial child (initialChildStateId).
Expand Down Expand Up @@ -63,10 +72,11 @@ StateMachine<TStateId> RegisterSubComposite<TCompositeParent>(TStateId stateId,
/// <param name="onSuccess">State Id to transition to on success, or null to denote last state and exit <see cref="StateMachine{TStateId}"/>.</param>
/// <param name="onError">State Id to transition to on error, or null to denote last state and exit <see cref="StateMachine{TStateId}"/>.</param>
/// <param name="onFailure">State Id to transition to on failure, or null to denote last state and exit <see cref="StateMachine{TStateId}"/>.</param>
/// <param name="commandSubscriptionTypes">Optional <see cref="ICommandState{TStateId}"/> subscription message types.</param>
/// <returns>Instance of this class.</returns>
/// <typeparam name="TState">State class.</typeparam>
/// <remarks>Example: <![CDATA[RegisterState<T>(StateId.State1, StateId.State2);]]>.</remarks>
StateMachine<TStateId> RegisterState<TState>(TStateId stateId, TStateId? onSuccess, TStateId? onError, TStateId? onFailure)
StateMachine<TStateId> RegisterState<TState>(TStateId stateId, TStateId? onSuccess, TStateId? onError, TStateId? onFailure, IReadOnlyCollection<Type>? commandSubscriptionTypes = null)
where TState : class, IState<TStateId>;

/// <summary>
Expand All @@ -79,14 +89,15 @@ StateMachine<TStateId> RegisterState<TState>(TStateId stateId, TStateId? onSucce
/// <param name="parentStateId">The identifier of the parent state if the registered state is part of a composite state; otherwise, null.</param>
/// <param name="isCompositeParent">true if the registered state is a composite parent state; otherwise, false.</param>
/// <param name="initialChildStateId">The identifier of the initial child state to activate when entering a composite parent state; otherwise, null.</param>
/// <param name="commandSubscriptionTypes">Optional <see cref="ICommandState{TStateId}"/> subscription message types.</param>
/// <returns>The current <see cref="StateMachine{TStateId}"/> instance, enabling method chaining.</returns>
/// <typeparam name="TState">The type of the state to register. Must implement <see cref="IState{TStateId}"/>.</typeparam>
/// <exception cref="InvalidOperationException">Thrown if a state with the specified stateId is already registered or if the state factory returns null.</exception>
/// <remarks>
/// Use this method to add states and define their transitions and hierarchy before starting the
/// state machine. Registering duplicate state identifiers is not allowed.
/// </remarks>
StateMachine<TStateId> RegisterState<TState>(TStateId stateId, TStateId? onSuccess, TStateId? onError, TStateId? onFailure, TStateId? parentStateId = null, bool isCompositeParent = false, TStateId? initialChildStateId = null)
StateMachine<TStateId> RegisterState<TState>(TStateId stateId, TStateId? onSuccess, TStateId? onError, TStateId? onFailure, TStateId? parentStateId = null, bool isCompositeParent = false, TStateId? initialChildStateId = null, IReadOnlyCollection<Type>? commandSubscriptionTypes = null)
where TState : class, IState<TStateId>;

/// <summary>
Expand All @@ -99,8 +110,9 @@ StateMachine<TStateId> RegisterState<TState>(TStateId stateId, TStateId? onSucce
/// <param name="onSuccess">The identifier of the state to transition to when the state completes successfully, or null to return to the parent composite state.</param>
/// <param name="onError">The identifier of the state to transition to when the registered state encounters an error, or null if no transition is defined.</param>
/// <param name="onFailure">The identifier of the state to transition to when the registered state fails, or null if no transition is defined.</param>
/// <param name="commandSubscriptionTypes">Optional <see cref="ICommandState{TStateId}"/> subscription message types.</param>
/// <returns>The current <see cref="StateMachine{TStateId}"/> instance, enabling method chaining.</returns>
StateMachine<TStateId> RegisterSubState<TChildClass>(TStateId stateId, TStateId parentStateId, TStateId? onSuccess = null, TStateId? onError = null, TStateId? onFailure = null)
StateMachine<TStateId> RegisterSubState<TChildClass>(TStateId stateId, TStateId parentStateId, TStateId? onSuccess = null, TStateId? onError = null, TStateId? onFailure = null, IReadOnlyCollection<Type>? commandSubscriptionTypes = null)
where TChildClass : class, IState<TStateId>;

/// <summary>Starts the machine at the initial state.</summary>
Expand All @@ -109,7 +121,4 @@ StateMachine<TStateId> RegisterSubState<TChildClass>(TStateId stateId, TStateId
/// <returns>Async task of The current <see cref="StateMachine{TStateId}"/> instance, enabling method chaining.</returns>
/// <exception cref="InvalidOperationException">Thrown if the specified state identifier has not been registered.</exception>
Task<StateMachine<TStateId>> RunAsync(TStateId initialState, CancellationToken cancellationToken = default);
/////// <param name="parameterStack">Parameter stack <see cref="PropertyBag"/>.</param>
/////// <param name="errors">Error stack <see cref="PropertyBag"/>.</param>
////Task<StateMachine<TStateId>> RunAsync(TStateId initialState, PropertyBag? parameterStack = null, PropertyBag? errors = null, CancellationToken cancellationToken = default);
}
Loading