DSeries.DStateMachine.Core 1.0.4

dotnet add package DSeries.DStateMachine.Core --version 1.0.4
                    
NuGet\Install-Package DSeries.DStateMachine.Core -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DSeries.DStateMachine.Core" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DSeries.DStateMachine.Core" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="DSeries.DStateMachine.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DSeries.DStateMachine.Core --version 1.0.4
                    
#r "nuget: DSeries.DStateMachine.Core, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#addin nuget:?package=DSeries.DStateMachine.Core&version=1.0.4
                    
Install DSeries.DStateMachine.Core as a Cake Addin
#tool nuget:?package=DSeries.DStateMachine.Core&version=1.0.4
                    
Install DSeries.DStateMachine.Core as a Cake Tool

DStateMachine

DStateMachine is a powerful and flexible asynchronous state machine library for .NET, designed with a clean, fluent API and production-ready architecture. It supports dynamic transitions, guard conditions, entry/exit hooks, and internal transitions, making it ideal for complex stateful workflows.


✨ Features

  • Generic Support: Works with any type for states and triggers (e.g., string, int, enum).
  • Fluent API: Concise and expressive DSL for configuration.
  • Asynchronous Execution: Seamless async/await support for transitions and actions.
  • Entry and Exit Hooks: Configure entry/exit actions per state.
  • Guard Clauses: Conditionally block transitions.
  • Internal Transitions: Perform side-effect actions without state change.
  • Dynamic Transitions: Determine the destination state at runtime.
  • DOT Export: Generate DOT-format graphs for visualization.

πŸ“š Example Usage

var sm = new DStateMachine<string, string>("A");

sm.ForState("A")
    .OnEntry(() => Console.WriteLine("Entering A"))
    .OnExit(() => Console.WriteLine("Exiting A"))
    .OnTrigger("toB", tb => tb.ChangeState("B"));

sm.ForState("B").OnEntry(() => Console.WriteLine("Entered B"));

await sm.TriggerAsync("toB");
Console.WriteLine(sm.CurrentState); // Output: B

βœ… Feature Examples

βœ… Generic Type Support

var sm = new DStateMachine<int, int>(0);
sm.ForState(0).OnTrigger(1, tb => tb.ChangeState(2));
sm.Trigger(1);
Console.WriteLine(sm.CurrentState); // Output: 2

πŸ” Entry and Exit Actions

var sm = new DStateMachine<string, string>("Init");
bool entered = false, exited = false;

sm.ForState("Init")
    .OnEntry(() => { entered = true; return Task.CompletedTask; })
    .OnExit(() => { exited = true; return Task.CompletedTask; })
    .OnTrigger("go", tb => tb.ChangeState("Done"));

sm.ForState("Done").OnEntry(() => Task.CompletedTask);
sm.Trigger("go");
Console.WriteLine($"Entered: {entered}, Exited: {exited}"); // Output: Entered: False, Exited: True

β›” Guard Clauses

var sm = new DStateMachine<string, string>("A");
sm.ForState("A")
    .OnTrigger("toB", tb => tb.ChangeState("B").If(() => false));

sm.OnUnhandledTrigger((trigger, machine) => {
    Console.WriteLine("Blocked by guard");
    return Task.CompletedTask;
});

sm.Trigger("toB"); // Output: Blocked by guard

⏳ Asynchronous Transitions

var sm = new DStateMachine<string, string>("Start");
sm.ForState("Start")
    .OnTrigger("load", tb => tb.ChangeStateAsync(async () => {
        await Task.Delay(100);
        return "Loaded";
    }));

sm.ForState("Loaded").OnEntry(() => Task.CompletedTask);
await sm.TriggerAsync("load");
Console.WriteLine(sm.CurrentState); // Output: Loaded

🧠 Dynamic Transitions

var sm = new DStateMachine<string, string>("A");
sm.ForState("A")
    .OnTrigger("toNext", tb => tb.ChangeState(() => DateTime.Now.Second % 2 == 0 ? "Even" : "Odd"));

sm.ForState("Even").OnEntry(() => Task.CompletedTask);
sm.ForState("Odd").OnEntry(() => Task.CompletedTask);

sm.Trigger("toNext");
Console.WriteLine(sm.CurrentState); // Output: "Even" or "Odd"

πŸ” Internal Transitions

var sm = new DStateMachine<string, string>("Idle");
bool logged = false;

sm.ForState("Idle")
    .OnTrigger("ping", tb => tb.ExecuteAction(() => logged = true));

await sm.TriggerAsync("ping");
Console.WriteLine($"State: {sm.CurrentState}, Logged: {logged}");
// Output: State: Idle, Logged: True

πŸ’¬ Fluent DSL

var sm = new DStateMachine<string, string>("X");
sm.ForState("X")
    .OnTrigger("a", tb => tb.ChangeState("A"))
    .OnTrigger("b", tb => tb.ChangeState("B"));

Console.WriteLine(sm.ForState("X").Machine == sm); // Output: True

πŸ“ˆ DOT Graph Export

var sm = new DStateMachine<string, string>("Start");
sm.ForState("Start").OnTrigger("toEnd", tb => tb.ChangeState("End"));
sm.ForState("End").OnEntry(() => Task.CompletedTask);

string dot = sm.ExportToDot();
Console.WriteLine(dot);
// Output: DOT-format string of the state machine

πŸŽ“ Getting Started

  1. Clone the repository or add the files to your project.
  2. Create a new instance: new DStateMachine<TTrigger, TState>(initialState).
  3. Configure states using .ForState(state) and chain OnEntry, OnExit, and OnTrigger.
  4. Fire transitions using Trigger(trigger) or await TriggerAsync(trigger).

DStateMachine Documentation

OnTrigger and Transition States

Overview

The OnTrigger method is part of the fluent API provided by DStateMachine. It configures state transitions based on triggers. Each transition may specify synchronous or asynchronous destination states, guard conditions, or internal actions.

Method Signature

public StateConfiguration<TTrigger, TState> OnTrigger(TTrigger trigger, Action<TransitionBuilder<TTrigger, TState>> config)
  • trigger: The trigger causing the transition.
  • config: Configuration action for defining the transitions.

TransitionBuilder<TTrigger, TState>

Overview

The TransitionBuilder class provides a fluent interface to define transitions for a given trigger. It supports:

  • Fixed and dynamic destination states
  • Asynchronous transitions
  • Guard conditions
  • Internal (side-effect-only) actions

Methods

ChangeState
public TransitionBuilder<TTrigger, TState> ChangeState(TState destination)

Transitions to a specific state.

ChangeState(Func<TState> destinationSelector)
public TransitionBuilder<TTrigger, TState> ChangeState(Func<TState> destinationSelector)

Transitions dynamically based on the provided function.

ChangeStateAsync(Func<Task<TState>> destinationSelector)
public TransitionBuilder<TTrigger, TState> ChangeStateAsync(Func<Task<TState>> destinationSelector)

Asynchronously determines the destination state.

If(Func<bool> guard)
public TransitionBuilder<TTrigger, TState> If(Func<bool> guard)

Adds a synchronous guard to the most recent transition.

IfAsync(Func<Task<bool>> asyncGuard)
public TransitionBuilder<TTrigger, TState> IfAsync(Func<Task<bool>> asyncGuard)

Adds an asynchronous guard.

ExecuteAction(Action action = null)
public TransitionBuilder<TTrigger, TState> ExecuteAction(Action action = null)

Defines an internal transition with a synchronous side-effect.

ExecuteActionAsync(Func<Task> actionAsync = null)
public TransitionBuilder<TTrigger, TState> ExecuteActionAsync(Func<Task> actionAsync = null)

Defines an internal transition with an asynchronous side-effect.

Example Usage per Method

ChangeState
.OnTrigger(Triggers.Start, t => t.ChangeState(States.Running))
ChangeState (dynamic)
.OnTrigger(Triggers.Restart, t => t.ChangeState(() => ComputeNextState()))
ChangeStateAsync
.OnTrigger(Triggers.Refresh, t => t.ChangeStateAsync(async () => await GetNextStateAsync()))
If
.OnTrigger(Triggers.Start, t => t.ChangeState(States.Running).If(() => IsReady))
IfAsync
.OnTrigger(Triggers.Start, t => t.ChangeStateAsync(GetRunningState).IfAsync(IsReadyAsync))
ExecuteAction
.OnTrigger(Triggers.Ping, t => t.ExecuteAction(() => Console.WriteLine("Pinged!")))
ExecuteActionAsync
.OnTrigger(Triggers.Ping, t => t.ExecuteActionAsync(async () => await LogPingAsync()))

Combined Example

enum States { Idle, Running, Stopped }
enum Triggers { Start, Stop, Pause, Ping }

var stateMachine = new DStateMachine<Triggers, States>(States.Idle);

stateMachine.ForState(States.Idle)
    .OnEntry(() => Console.WriteLine("Entering Idle"))
    .OnExit(() => Console.WriteLine("Exiting Idle"))
    .OnTrigger(Triggers.Start, t => t.ChangeState(States.Running).If(() => CanStart()))
    .OnTrigger(Triggers.Pause, t => t.ExecuteActionAsync(async () => await LogPauseAttempt()))
    .OnTrigger(Triggers.Ping, t => t.ExecuteAction(() => Console.WriteLine("Ping from Idle")));

stateMachine.ForState(States.Running)
    .OnTrigger(Triggers.Stop, t => t.ChangeStateAsync(async () => await DetermineStopState()))
    .OnTrigger(Triggers.Ping, t => t.ExecuteAction(() => Console.WriteLine("Ping from Running")));

await stateMachine.TriggerAsync(Triggers.Start);

Handling Unhandled Triggers

You can define a handler for triggers without defined transitions:

stateMachine.OnUnhandledTrigger(async (trigger, machine) =>
{
    await LogAsync($"Unhandled trigger {trigger} in state {machine.CurrentState}");
});

public StateConfiguration<TTrigger, TState> OnEntry(Action<DStateMachine<TTrigger, TState>> action)
public StateConfiguration<TTrigger, TState> OnEntry(Func<Task> asyncAction)

public StateConfiguration<TTrigger, TState> OnExit(Action<DStateMachine<TTrigger, TState>> action)
public StateConfiguration<TTrigger, TState> OnExit(Func<Task> asyncAction)
  • OnEntry: Defines an action that runs after the state is entered.
  • OnExit: Defines an action that runs before the state is exited.
  • Both support synchronous and asynchronous versions.

Usage Examples

Synchronous Entry/Exit
stateMachine.ForState(States.Idle)
    .OnEntry(sm => Console.WriteLine("Now in Idle"))
    .OnExit(sm => Console.WriteLine("Leaving Idle"));
Asynchronous Entry/Exit
stateMachine.ForState(States.Running)
    .OnEntry(async () => await LogAsync("Entered Running"))
    .OnExit(async () => await LogAsync("Exited Running"));

You may define multiple entry or exit actions per stateβ€”they will be executed in the order they are registered.


🧭 Default Entry and Exit Actions

The DStateMachine<TTrigger, TState> class supports defining global entry and exit actions that apply to all states. These are called Default Entry and Default Exit actions.

They are executed in addition to any state-specific entry/exit actions and are useful for:

  • Logging state transitions globally
  • Auditing
  • Notifying external services
  • Performing shared cleanup or setup tasks

🟒 Default Entry Actions

Default entry actions are executed every time any state is entered.

βž• Adding Default Entry Actions

stateMachine.DefaultOnEntry(sm =>
{
    Console.WriteLine($"[ENTRY] Entering state: {sm.CurrentState}");
});

You can also use an asynchronous version:

stateMachine.DefaultOnEntry(async sm =>
{
    await logService.LogAsync($"Entered state: {sm.CurrentState}");
});

πŸ”΄ Default Exit Actions

Default exit actions are executed every time any state is exited.

βž• Adding Default Exit Actions

stateMachine.DefaultOnExit(sm =>
{
    Console.WriteLine($"[EXIT] Exiting state: {sm.CurrentState}");
});

Or asynchronously:

stateMachine.DefaultOnExit(async sm =>
{
    await telemetry.TrackStateExitAsync(sm.CurrentState);
});

πŸ”„ Execution Order

When a state transition occurs, the actions are executed in the following order:

  1. State-specific Exit Actions
  2. Default Exit Actions βœ…
  3. (State change)
  4. Default Entry Actions βœ…
  5. State-specific Entry Actions

This ensures shared logic is applied after specific logic during exit, and before specific logic during entry.


βœ… Best Practices

  • Use default entry/exit for logging, telemetry, or global side-effects.
  • Keep default actions lightweight and non-blocking where possible.
  • Avoid introducing state-specific logic in global hooks β€” keep them generic.

πŸ“Œ Summary

Action Type Scope Supports Async Receives State Machine
DefaultOnEntry All States βœ… Yes βœ… Yes
DefaultOnExit All States βœ… Yes βœ… Yes

These hooks make your state machine more powerful and extensible without cluttering individual state definitions.

Ignoring Default Entry and Exit Actions in DStateMachine

Overview

In DStateMachine, global default actions can be defined for every state transition:

  • Default Entry Actions: Executed on entry to any state.
  • Default Exit Actions: Executed on exit from any state.

However, there are cases where you may want to disable these default actions for specific states. This feature allows for more precise control over individual state behaviors.

How to Ignore Default Actions

When configuring a specific state, you can explicitly tell the state machine to skip the default entry and/or exit actions using the following fluent API:

stateMachine.ForState(State.SomeState)
    .IgnoreDefaultEntry()
    .IgnoreDefaultExit();

Methods

IgnoreDefaultEntry()

Prevents the global default entry actions from running when this state is entered.

IgnoreDefaultExit()

Prevents the global default exit actions from running when this state is exited.

Example

var sm = new DStateMachine<string, MyState>(MyState.Idle);

sm.DefaultOnEntry(sm => Console.WriteLine("[Default] Entered state."));
sm.DefaultOnExit(sm => Console.WriteLine("[Default] Exited state."));

sm.ForState(MyState.Processing)
    .OnEntry(sm => Console.WriteLine("[State] Entering Processing"))
    .OnExit(sm => Console.WriteLine("[State] Exiting Processing"))
    .IgnoreDefaultEntry() // Skips default entry action
    .IgnoreDefaultExit(); // Skips default exit action

sm.ForState(MyState.Idle)
    .OnTrigger("Start", t => t.ChangeState(MyState.Processing));

await sm.TriggerAsync("Start");

Output:

[State] Entering Processing

In this example, the state-specific entry action runs, but the default actions are ignored for Processing.

Notes

  • Ignoring default actions is optional. If not called, the default actions are applied as usual.
  • This configuration is per-state and can be applied independently for entry and exit.
  • Internal transitions do not trigger entry or exit actions, including default ones.

Use Cases

  • You want more control during transitions to/from certain critical states.
  • You have special initialization/cleanup logic that should not inherit the global behavior.
  • You want to optimize performance by skipping unnecessary actions in specific states.

For more, see the DefaultOnEntry and DefaultOnExit registration methods in DStateMachine.

Best Practices

  • Define guards clearly to ensure transitions occur under expected conditions.
  • Use internal transitions (ExecuteAction, ExecuteActionAsync) for logging, notifications, or other side effects.
  • Favor asynchronous methods for operations involving I/O or long-running tasks.
  • Keep transition logic lightweight and non-blocking.

πŸ” DStateMachine features

A feature-by-feature

Feature DStateMachine
Asynchronous Support βœ… Built-in async/await throughout
Fluent API βœ… Clean, chainable DSL
Generic State & Trigger Types βœ… Full support (string, enum, etc.)
Entry/Exit Hooks βœ… Both sync & async, with access to state machine
Global Entry/Exit Actions βœ… Via DefaultOnEntry/Exit
Per-State Default Ignore βœ… Can opt out of default entry/exit per state
Internal Transitions βœ… Explicit via ExecuteAction (sync/async)
Dynamic Transitions βœ… Sync & async destination selectors supported
Guard Clauses βœ… Sync & async guard support
Unhandled Trigger Handling βœ… Configurable async handler
Visualization (Graph Export) βœ… DOT export + Tree text visualization
Testability βœ… Built-in xUnit tests, clean test support
State Hierarchies / Substates ❌ Not yet supported
Licensing MIT
Maturity / Community πŸ†• Emerging, modern design

πŸ™Œ Contributions

Pull requests and issues are welcome! If you'd like to contribute improvements or new features, feel free to fork and open a PR.


πŸ“„ License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.4 448 3/22/2025
1.0.3 130 3/22/2025
1.0.2 127 3/22/2025
1.0.1 50 3/22/2025
1.0.0 59 3/21/2025

Release Notes v1.0.4

βœ… Global Entry/Exit Actions: You can now define default entry and exit behaviors that run on every transition via DefaultOnEntry() and DefaultOnExit() methods.

🎯 Per-State Overrides: States can opt out of global actions using .IgnoreDefaultEntry() and .IgnoreDefaultExit().

🔄 Async Entry/Exit Support: OnEntry() and OnExit() now support async delegates and provide access to the state machine instance.

🌳 Improved Visualization: New VisualizeAsTree() method for easy-to-read text-based layout of transitions.

βœ… xUnit Test Coverage: Added unit tests to verify global action handling and ignore logic.

🛠️ Internal enhancements for cleaner async flow and state behavior control.

Added a new IfAsync method to support asynchronous guard conditions.

Updated all transition delegates (fixed, dynamic, and internal) to support asynchronous execution.

Async Action Support:

Added the ExecuteActionAsync method to allow asynchronous execution of side-effect actions during internal transitions.

Maintained backward compatibility by keeping the synchronous ExecuteAction method with wrapping.
           
These changes provide a flexible and robust asynchronous state machine framework while ensuring existing synchronous functionality remains intact.