Skip to content

Commit a05ed25

Browse files
authored
Merge pull request #595 from Jon2G/dev
Allow permitDynamic destination state to be calculated with an async function (Task)
2 parents 94867a0 + 6007331 commit a05ed25

12 files changed

+1219
-6
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace Stateless
5+
{
6+
public partial class StateMachine<TState, TTrigger>
7+
{
8+
internal class DynamicTriggerBehaviourAsync : TriggerBehaviour
9+
{
10+
readonly Func<object[], Task<TState>> _destination;
11+
internal Reflection.DynamicTransitionInfo TransitionInfo { get; private set; }
12+
13+
public DynamicTriggerBehaviourAsync(TTrigger trigger, Func<object[], Task<TState>> destination,
14+
TransitionGuard transitionGuard, Reflection.DynamicTransitionInfo info)
15+
: base(trigger, transitionGuard)
16+
{
17+
_destination = destination ?? throw new ArgumentNullException(nameof(destination));
18+
TransitionInfo = info ?? throw new ArgumentNullException(nameof(info));
19+
}
20+
21+
public async Task<TState> GetDestinationState(TState source, object[] args)
22+
{
23+
return await _destination(args);
24+
}
25+
}
26+
}
27+
}

src/Stateless/Reflection/StateInfo.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ internal static void AddRelationships<TState, TTrigger>(StateInfo info, StateMac
7272
{
7373
dynamicTransitions.Add(((StateMachine<TState, TTrigger>.DynamicTriggerBehaviour)item).TransitionInfo);
7474
}
75+
foreach (var item in triggerBehaviours.Value.Where(behaviour => behaviour is StateMachine<TState, TTrigger>.DynamicTriggerBehaviourAsync))
76+
{
77+
dynamicTransitions.Add(((StateMachine<TState, TTrigger>.DynamicTriggerBehaviourAsync)item).TransitionInfo);
78+
}
7579
}
7680

7781
info.AddRelationships(superstate, substates, fixedTransitions, dynamicTransitions);

src/Stateless/StateConfiguration.Async.cs

Lines changed: 556 additions & 0 deletions
Large diffs are not rendered by default.

src/Stateless/StateMachine.Async.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,15 @@ async Task InternalFireOneAsync(TTrigger trigger, params object[] args)
217217
// Handle transition, and set new state
218218
var transition = new Transition(source, handler.Destination, trigger, args);
219219
await HandleReentryTriggerAsync(args, representativeState, transition);
220+
break;
221+
}
222+
case DynamicTriggerBehaviourAsync asyncHandler:
223+
{
224+
var destination = await asyncHandler.GetDestinationState(source, args);
225+
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
226+
var transition = new Transition(source, destination, trigger, args);
227+
await HandleTransitioningTriggerAsync(args, representativeState, transition);
228+
220229
break;
221230
}
222231
case DynamicTriggerBehaviour handler:

src/Stateless/StateMachine.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,18 @@ private void InternalFireOne(TTrigger trigger, params object[] args)
422422
HandleReentryTrigger(args, representativeState, transition);
423423
break;
424424
}
425+
case DynamicTriggerBehaviourAsync asyncHandler:
426+
{
427+
asyncHandler.GetDestinationState(source, args)
428+
.ContinueWith(t =>
429+
{
430+
var destination = t.Result;
431+
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
432+
var transition = new Transition(source, destination, trigger, args);
433+
return HandleTransitioningTriggerAsync(args, representativeState, transition);
434+
});
435+
break;
436+
}
425437
case DynamicTriggerBehaviour handler:
426438
{
427439
handler.GetDestinationState(source, args, out var destination);

test/Stateless.Tests/DotGraphFixture.cs

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Xunit;
66
using Stateless.Reflection;
77
using Stateless.Graph;
8+
using System.Threading.Tasks;
89

910
namespace Stateless.Tests
1011
{
@@ -196,7 +197,7 @@ public void WhenDiscriminatedByAnonymousGuardWithDescription()
196197
var expected = Prefix(Style.UML)
197198
+ Box(Style.UML, "A") + Box(Style.UML, "B")
198199
+ Line("A", "B", "X [description]")
199-
+ suffix;
200+
+ suffix;
200201

201202
var sm = new StateMachine<State, Trigger>(State.A);
202203

@@ -258,6 +259,27 @@ public void DestinationStateIsDynamic()
258259

259260
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
260261

262+
#if WRITE_DOTS_TO_FOLDER
263+
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
264+
#endif
265+
266+
Assert.Equal(expected, dotGraph);
267+
}
268+
269+
[Fact]
270+
public void DestinationStateIsDynamicAsync()
271+
{
272+
var expected = Prefix(Style.UML)
273+
+ Box(Style.UML, "A")
274+
+ Decision(Style.UML, "Decision1", "Function")
275+
+ Line("A", "Decision1", "X") + suffix;
276+
277+
var sm = new StateMachine<State, Trigger>(State.A);
278+
sm.Configure(State.A)
279+
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(State.B));
280+
281+
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
282+
261283
#if WRITE_DOTS_TO_FOLDER
262284
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
263285
#endif
@@ -280,6 +302,27 @@ public void DestinationStateIsCalculatedBasedOnTriggerParameters()
280302

281303
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
282304

305+
#if WRITE_DOTS_TO_FOLDER
306+
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
307+
#endif
308+
Assert.Equal(expected, dotGraph);
309+
}
310+
311+
[Fact]
312+
public void DestinationStateIsCalculatedBasedOnTriggerParametersAsync()
313+
{
314+
var expected = Prefix(Style.UML)
315+
+ Box(Style.UML, "A")
316+
+ Decision(Style.UML, "Decision1", "Function")
317+
+ Line("A", "Decision1", "X") + suffix;
318+
319+
var sm = new StateMachine<State, Trigger>(State.A);
320+
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
321+
sm.Configure(State.A)
322+
.PermitDynamicAsync(trigger, i =>Task.FromResult(i == 1 ? State.B : State.C));
323+
324+
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
325+
283326
#if WRITE_DOTS_TO_FOLDER
284327
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
285328
#endif
@@ -398,7 +441,7 @@ public void OnEntryWithTriggerParameter()
398441

399442
Assert.Equal(expected, dotGraph);
400443
}
401-
444+
402445
[Fact]
403446
public void SpacedUmlWithSubstate()
404447
{
@@ -408,14 +451,14 @@ public void SpacedUmlWithSubstate()
408451
string StateD = "State D";
409452
string TriggerX = "Trigger X";
410453
string TriggerY = "Trigger Y";
411-
454+
412455
var expected = Prefix(Style.UML)
413456
+ Subgraph(Style.UML, StateD, $"{StateD}\\n----------\\nentry / Enter D",
414457
Box(Style.UML, StateB)
415458
+ Box(Style.UML, StateC))
416459
+ Box(Style.UML, StateA, new List<string> { "Enter A" }, new List<string> { "Exit A" })
417460
+ Line(StateA, StateB, TriggerX) + Line(StateA, StateC, TriggerY)
418-
+ Environment.NewLine
461+
+ Environment.NewLine
419462
+ $" init [label=\"\", shape=point];" + Environment.NewLine
420463
+ $" init -> \"{StateA}\"[style = \"solid\"]" + Environment.NewLine
421464
+ "}";
@@ -493,7 +536,36 @@ public void UmlWithDynamic()
493536
var sm = new StateMachine<State, Trigger>(State.A);
494537

495538
sm.Configure(State.A)
496-
.PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB"}, { State.C, "ChoseC" } });
539+
.PermitDynamic(Trigger.X, DestinationSelector, null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } });
540+
541+
sm.Configure(State.B);
542+
sm.Configure(State.C);
543+
544+
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
545+
#if WRITE_DOTS_TO_FOLDER
546+
System.IO.File.WriteAllText(DestinationFolder + "UmlWithDynamic.dot", dotGraph);
547+
#endif
548+
549+
Assert.Equal(expected, dotGraph);
550+
}
551+
552+
[Fact]
553+
public void UmlWithDynamicAsync()
554+
{
555+
var expected = Prefix(Style.UML)
556+
+ Box(Style.UML, "A")
557+
+ Box(Style.UML, "B")
558+
+ Box(Style.UML, "C")
559+
+ Decision(Style.UML, "Decision1", "Function")
560+
+ Line("A", "Decision1", "X")
561+
+ Line("Decision1", "B", "X [ChoseB]")
562+
+ Line("Decision1", "C", "X [ChoseC]")
563+
+ suffix;
564+
565+
var sm = new StateMachine<State, Trigger>(State.A);
566+
567+
sm.Configure(State.A)
568+
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(DestinationSelector()), null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } });
497569

498570
sm.Configure(State.B);
499571
sm.Configure(State.C);
@@ -513,7 +585,7 @@ public void TransitionWithIgnoreAndEntry()
513585
+ Box(Style.UML, "A", new List<string> { "DoEntry" })
514586
+ Box(Style.UML, "B", new List<string> { "DoThisEntry" })
515587
+ Line("A", "B", "X")
516-
+ Line("A", "A", "Y")
588+
+ Line("A", "A", "Y")
517589
+ Line("B", "B", "Z / DoThisEntry")
518590
+ suffix;
519591

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
5+
namespace Stateless.Tests
6+
{
7+
public class DynamicAsyncTriggerBehaviourAsyncFixture
8+
{
9+
[Fact]
10+
public async Task PermitDynamic_Selects_Expected_State_Async()
11+
{
12+
var sm = new StateMachine<State, Trigger>(State.A);
13+
sm.Configure(State.A)
14+
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return State.B; });
15+
16+
await sm.FireAsync(Trigger.X);
17+
18+
Assert.Equal(State.B, sm.State);
19+
}
20+
21+
[Fact]
22+
public async Task PermitDynamic_With_TriggerParameter_Selects_Expected_State_Async()
23+
{
24+
var sm = new StateMachine<State, Trigger>(State.A);
25+
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
26+
sm.Configure(State.A)
27+
.PermitDynamicAsync(trigger, async (i) => { await Task.Delay(100); return i == 1 ? State.B : State.C; });
28+
29+
await sm.FireAsync(trigger, 1);
30+
31+
Assert.Equal(State.B, sm.State);
32+
}
33+
34+
[Fact]
35+
public async Task PermitDynamic_Permits_Reentry_Async()
36+
{
37+
var sm = new StateMachine<State, Trigger>(State.A);
38+
var onExitInvoked = false;
39+
var onEntryInvoked = false;
40+
var onEntryFromInvoked = false;
41+
sm.Configure(State.A)
42+
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return State.A; })
43+
.OnEntry(() => onEntryInvoked = true)
44+
.OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true)
45+
.OnExit(() => onExitInvoked = true);
46+
47+
await sm.FireAsync(Trigger.X);
48+
49+
Assert.True(onExitInvoked, "Expected OnExit to be invoked");
50+
Assert.True(onEntryInvoked, "Expected OnEntry to be invoked");
51+
Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked");
52+
Assert.Equal(State.A, sm.State);
53+
}
54+
55+
[Fact]
56+
public async Task PermitDynamic_Selects_Expected_State_Based_On_DestinationStateSelector_Function_Async()
57+
{
58+
var sm = new StateMachine<State, Trigger>(State.A);
59+
var value = 'C';
60+
sm.Configure(State.A)
61+
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return value == 'B' ? State.B : State.C; });
62+
63+
await sm.FireAsync(Trigger.X);
64+
65+
Assert.Equal(State.C, sm.State);
66+
}
67+
68+
[Fact]
69+
public async Task PermitDynamicIf_With_TriggerParameter_Permits_Transition_When_GuardCondition_Met_Async()
70+
{
71+
var sm = new StateMachine<State, Trigger>(State.A);
72+
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
73+
sm.Configure(State.A)
74+
.PermitDynamicIfAsync(trigger, async (i) =>{ await Task.Delay(100); return i == 1 ? State.C : State.B; }, (i) => i == 1);
75+
76+
await sm.FireAsync(trigger, 1);
77+
78+
Assert.Equal(State.C, sm.State);
79+
}
80+
81+
[Fact]
82+
public async Task PermitDynamicIf_With_2_TriggerParameters_Permits_Transition_When_GuardCondition_Met_Async()
83+
{
84+
var sm = new StateMachine<State, Trigger>(State.A);
85+
var trigger = sm.SetTriggerParameters<int, int>(Trigger.X);
86+
sm.Configure(State.A).PermitDynamicIfAsync(
87+
trigger,
88+
async (i, j) => { await Task.Yield(); return i == 1 && j == 2 ? State.C : State.B; },
89+
(i, j) => i == 1 && j == 2);
90+
91+
await sm.FireAsync(trigger, 1, 2);
92+
93+
Assert.Equal(State.C, sm.State);
94+
}
95+
96+
[Fact]
97+
public async Task PermitDynamicIf_With_3_TriggerParameters_Permits_Transition_When_GuardCondition_Met_Async()
98+
{
99+
var sm = new StateMachine<State, Trigger>(State.A);
100+
var trigger = sm.SetTriggerParameters<int, int, int>(Trigger.X);
101+
sm.Configure(State.A).PermitDynamicIfAsync(
102+
trigger,
103+
async (i, j, k) => { await Task.Delay(100); return i == 1 && j == 2 && k == 3 ? State.C : State.B; },
104+
(i, j, k) => i == 1 && j == 2 && k == 3);
105+
106+
await sm.FireAsync(trigger, 1, 2, 3);
107+
108+
Assert.Equal(State.C, sm.State);
109+
}
110+
111+
[Fact]
112+
public async Task PermitDynamicIf_With_TriggerParameter_Throws_When_GuardCondition_Not_Met_Async()
113+
{
114+
var sm = new StateMachine<State, Trigger>(State.A);
115+
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
116+
sm.Configure(State.A)
117+
.PermitDynamicIfAsync(trigger, async (i) => { await Task.Delay(100); return i > 0 ? State.C : State.B; }, guard: (i) => i == 2);
118+
119+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sm.FireAsync(trigger, 1));
120+
}
121+
122+
[Fact]
123+
public async Task PermitDynamicIf_With_2_TriggerParameters_Throws_When_GuardCondition_Not_Met_Async()
124+
{
125+
var sm = new StateMachine<State, Trigger>(State.A);
126+
var trigger = sm.SetTriggerParameters<int, int>(Trigger.X);
127+
sm.Configure(State.A).PermitDynamicIfAsync(
128+
trigger,
129+
async (i, j) => { await Task.Delay(100); return i > 0 ? State.C : State.B; },
130+
(i, j) => i == 2 && j == 3);
131+
132+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sm.FireAsync(trigger, 1, 2));
133+
}
134+
135+
[Fact]
136+
public async Task PermitDynamicIf_With_3_TriggerParameters_Throws_When_GuardCondition_Not_Met_Async()
137+
{
138+
var sm = new StateMachine<State, Trigger>(State.A);
139+
var trigger = sm.SetTriggerParameters<int, int, int>(Trigger.X);
140+
sm.Configure(State.A).PermitDynamicIfAsync(trigger,
141+
async (i, j, k) => { await Task.Delay(100); return i > 0 ? State.C : State.B; },
142+
(i, j, k) => i == 2 && j == 3 && k == 4);
143+
144+
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sm.FireAsync(trigger, 1, 2, 3));
145+
}
146+
147+
[Fact]
148+
public async Task PermitDynamicIf_Permits_Reentry_When_GuardCondition_Met_Async()
149+
{
150+
var sm = new StateMachine<State, Trigger>(State.A);
151+
var onExitInvoked = false;
152+
var onEntryInvoked = false;
153+
var onEntryFromInvoked = false;
154+
sm.Configure(State.A)
155+
.PermitDynamicIfAsync(Trigger.X, async () =>{ await Task.Delay(100); return State.A; }, () => true)
156+
.OnEntry(() => onEntryInvoked = true)
157+
.OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true)
158+
.OnExit(() => onExitInvoked = true);
159+
160+
await sm.FireAsync(Trigger.X);
161+
162+
Assert.True(onExitInvoked, "Expected OnExit to be invoked");
163+
Assert.True(onEntryInvoked, "Expected OnEntry to be invoked");
164+
Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked");
165+
Assert.Equal(State.A, sm.State);
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)