Repository: dotnet-state-machine/stateless (2026-03-11T10:23:54Z)

Issue:
Async trigger behaviours are not included when calling GetInfo
Async trigger behaviours are not included when calling GetInfo, resulting in incomplete graphs.
In the following snippet, `stateMachineInfo` will not contain the transition from A to B.

``` csharp
var sm = new StateMachine<State, Trigger>(State.A);
sm.Configure(State.A)
    .PermitIfAsync(Trigger.X, State.B, () => Task.FromResult(true));

var stateMachineInfo = sm.GetInfo();
```


Discussion:

Patch:
diff --git a/src/Stateless/Reflection/FixedTransitionInfo.cs b/src/Stateless/Reflection/FixedTransitionInfo.cs
index acd46896..32ea5222 100644
--- a/src/Stateless/Reflection/FixedTransitionInfo.cs
+++ b/src/Stateless/Reflection/FixedTransitionInfo.cs
@@ -20,6 +20,18 @@ internal static FixedTransitionInfo Create<TState, TTrigger>(StateMachine<TState
             };
         }
 
+        internal static FixedTransitionInfo Create<TState, TTrigger>(StateMachine<TState, TTrigger>.TriggerBehaviourAsync behaviour, StateInfo destinationStateInfo)
+        {
+            return new FixedTransitionInfo
+            {
+                Trigger = new TriggerInfo(behaviour.Trigger),
+                DestinationState = destinationStateInfo,
+                GuardConditionsMethodDescriptions = behaviour.Guard == null
+                    ? Array.Empty<InvocationInfo>() : behaviour.Guard.Conditions.Select(c => c.MethodDescription),
+                IsInternalTransition = behaviour is StateMachine<TState, TTrigger>.InternalTriggerBehaviour
+            };
+        }
+
         private FixedTransitionInfo() { }
 
         /// <summary>
diff --git a/src/Stateless/Reflection/StateInfo.cs b/src/Stateless/Reflection/StateInfo.cs
index 0f1c1ed8..b4df4429 100644
--- a/src/Stateless/Reflection/StateInfo.cs
+++ b/src/Stateless/Reflection/StateInfo.cs
@@ -48,6 +48,7 @@ internal static void AddRelationships<TState, TTrigger>(StateInfo info, StateMac
             var fixedTransitions = new List<FixedTransitionInfo>();
             var dynamicTransitions = new List<DynamicTransitionInfo>();
 
+            // Sync triggers
             foreach (var triggerBehaviours in stateRepresentation.TriggerBehaviours)
             {
                 // First add all the deterministic transitions
@@ -78,6 +79,21 @@ internal static void AddRelationships<TState, TTrigger>(StateInfo info, StateMac
                 }
             }
 
+            // Async triggers
+            foreach (var triggerBehaviour in stateRepresentation.TriggerBehavioursAsync.Values.SelectMany(x => x))
+            {
+                if (triggerBehaviour is StateMachine<TState, TTrigger>.TransitioningTriggerBehaviourAsync transitioningTriggerBehaviourAsync)
+                {
+                    var destinationInfo = lookupState(transitioningTriggerBehaviourAsync.Destination);
+                    fixedTransitions.Add(FixedTransitionInfo.Create(transitioningTriggerBehaviourAsync, destinationInfo));
+                }
+                else if (triggerBehaviour is StateMachine<TState, TTrigger>.ReentryTriggerBehaviourAsync reentryTriggerBehaviourAsync)
+                {
+                    var destinationInfo = lookupState(reentryTriggerBehaviourAsync.Destination);
+                    fixedTransitions.Add(FixedTransitionInfo.Create(reentryTriggerBehaviourAsync, destinationInfo));
+                }
+            }
+
             info.AddRelationships(superstate, substates, fixedTransitions, dynamicTransitions);
         }
 
diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs
index 999297e2..0d11fae9 100644
--- a/src/Stateless/StateMachine.cs
+++ b/src/Stateless/StateMachine.cs
@@ -163,6 +163,8 @@ public StateMachineInfo GetInfo()
 
             var behaviours = _stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehaviours.SelectMany(b => b.Value.OfType<TransitioningTriggerBehaviour>().Select(tb => tb.Destination))).ToList();
             behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehaviours.SelectMany(b => b.Value.OfType<ReentryTriggerBehaviour>().Select(tb => tb.Destination))).ToList());
+            behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehavioursAsync.SelectMany(b => b.Value.OfType<TransitioningTriggerBehaviourAsync>().Select(tb => tb.Destination))).ToList());
+            behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehavioursAsync.SelectMany(b => b.Value.OfType<ReentryTriggerBehaviourAsync>().Select(tb => tb.Destination))).ToList());
 
             var reachable = behaviours
                 .Distinct()


Test patch:
diff --git a/test/Stateless.Tests/GetInfoFixture.cs b/test/Stateless.Tests/GetInfoFixture.cs
index 570ee914..e4f24250 100644
--- a/test/Stateless.Tests/GetInfoFixture.cs
+++ b/test/Stateless.Tests/GetInfoFixture.cs
@@ -1,4 +1,5 @@
-﻿using System.Threading.Tasks;
+﻿using System.Linq;
+using System.Threading.Tasks;
 using Xunit;
 
 namespace Stateless.Tests
@@ -12,16 +13,16 @@ public void GetInfo_should_return_Entry_action_with_trigger_name()
             var sm = new StateMachine<State, Trigger>(State.A);
             sm.Configure(State.B)
                 .OnEntryFrom(Trigger.X, () => { });
-        
+
             // ACT
             var stateMachineInfo = sm.GetInfo();
-        
+
             // ASSERT
             var stateInfo = Assert.Single(stateMachineInfo.States);
             var entryActionInfo = Assert.Single(stateInfo.EntryActions);
             Assert.Equal(Trigger.X.ToString(), entryActionInfo.FromTrigger);
         }
-    
+
         [Fact]
         public void GetInfo_should_return_async_Entry_action_with_trigger_name()
         {
@@ -29,14 +30,31 @@ public void GetInfo_should_return_async_Entry_action_with_trigger_name()
             var sm = new StateMachine<State, Trigger>(State.A);
             sm.Configure(State.B)
                 .OnEntryFromAsync(Trigger.X, () => Task.CompletedTask);
-        
+
             // ACT
             var stateMachineInfo = sm.GetInfo();
-        
+
             // ASSERT
             var stateInfo = Assert.Single(stateMachineInfo.States);
             var entryActionInfo = Assert.Single(stateInfo.EntryActions);
             Assert.Equal(Trigger.X.ToString(), entryActionInfo.FromTrigger);
         }
+
+        [Fact]
+        public void GetInfo_should_include_async_Trigger()
+        {
+            // ARRANGE
+            var sm = new StateMachine<State, Trigger>(State.A);
+            sm.Configure(State.A)
+                .PermitIfAsync(Trigger.X, State.B, () => Task.FromResult(true));
+
+            // ACT
+            var stateMachineInfo = sm.GetInfo();
+
+            // ASSERT
+            var stateInfo = Assert.Single(stateMachineInfo.States.Where(x => x.UnderlyingState.Equals(State.A)));
+            var transitionInfo = Assert.Single(stateInfo.Transitions);
+            Assert.Equal(Trigger.X, transitionInfo.Trigger.UnderlyingTrigger);
+        }
     }
-}
\ No newline at end of file
+}