Skip to content

Commit 800849e

Browse files
authored
Merge branch 'dev' into dev
2 parents 389f156 + a05ed25 commit 800849e

14 files changed

+1238
-5
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: 72 additions & 0 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
{
@@ -271,6 +272,27 @@ public void DestinationStateIsDynamic()
271272

272273
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
273274

275+
#if WRITE_DOTS_TO_FOLDER
276+
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
277+
#endif
278+
279+
Assert.Equal(expected, dotGraph);
280+
}
281+
282+
[Fact]
283+
public void DestinationStateIsDynamicAsync()
284+
{
285+
var expected = Prefix(Style.UML)
286+
+ Box(Style.UML, "A")
287+
+ Decision(Style.UML, "Decision1", "Function")
288+
+ Line("A", "Decision1", "X") + suffix;
289+
290+
var sm = new StateMachine<State, Trigger>(State.A);
291+
sm.Configure(State.A)
292+
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(State.B));
293+
294+
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
295+
274296
#if WRITE_DOTS_TO_FOLDER
275297
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
276298
#endif
@@ -293,6 +315,27 @@ public void DestinationStateIsCalculatedBasedOnTriggerParameters()
293315

294316
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
295317

318+
#if WRITE_DOTS_TO_FOLDER
319+
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
320+
#endif
321+
Assert.Equal(expected, dotGraph);
322+
}
323+
324+
[Fact]
325+
public void DestinationStateIsCalculatedBasedOnTriggerParametersAsync()
326+
{
327+
var expected = Prefix(Style.UML)
328+
+ Box(Style.UML, "A")
329+
+ Decision(Style.UML, "Decision1", "Function")
330+
+ Line("A", "Decision1", "X") + suffix;
331+
332+
var sm = new StateMachine<State, Trigger>(State.A);
333+
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
334+
sm.Configure(State.A)
335+
.PermitDynamicAsync(trigger, i =>Task.FromResult(i == 1 ? State.B : State.C));
336+
337+
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
338+
296339
#if WRITE_DOTS_TO_FOLDER
297340
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
298341
#endif
@@ -523,6 +566,35 @@ public void UmlWithDynamic()
523566
Assert.Equal(expected, dotGraph);
524567
}
525568

569+
[Fact]
570+
public void UmlWithDynamicAsync()
571+
{
572+
var expected = Prefix(Style.UML)
573+
+ Box(Style.UML, "A")
574+
+ Box(Style.UML, "B")
575+
+ Box(Style.UML, "C")
576+
+ Decision(Style.UML, "Decision1", "Function")
577+
+ Line("A", "Decision1", "X")
578+
+ Line("Decision1", "B", "X [ChoseB]")
579+
+ Line("Decision1", "C", "X [ChoseC]")
580+
+ suffix;
581+
582+
var sm = new StateMachine<State, Trigger>(State.A);
583+
584+
sm.Configure(State.A)
585+
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(DestinationSelector()), null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } });
586+
587+
sm.Configure(State.B);
588+
sm.Configure(State.C);
589+
590+
string dotGraph = UmlDotGraph.Format(sm.GetInfo());
591+
#if WRITE_DOTS_TO_FOLDER
592+
System.IO.File.WriteAllText(DestinationFolder + "UmlWithDynamic.dot", dotGraph);
593+
#endif
594+
595+
Assert.Equal(expected, dotGraph);
596+
}
597+
526598
[Fact]
527599
public void TransitionWithIgnoreAndEntry()
528600
{
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)