Skip to content

Commit 0ae2065

Browse files
committed
bump version to 0.1.0 and enhance state machine functionality with new async methods
1 parent f6d3f58 commit 0ae2065

8 files changed

Lines changed: 521 additions & 90 deletions

File tree

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
<RepositoryUrl>https://github.com/managedcode/Orleans.StateMachine</RepositoryUrl>
1818
<PackageProjectUrl>https://github.com/managedcode/Orleans.StateMachine</PackageProjectUrl>
1919
<Product>Managed Code - Orleans StateMachine</Product>
20-
<Version>0.0.9</Version>
21-
<PackageVersion>0.0.9</PackageVersion>
20+
<Version>0.1.0</Version>
21+
<PackageVersion>0.1.0</PackageVersion>
2222

2323
</PropertyGroup>
2424
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using ManagedCode.Orleans.StateMachine.Interfaces;
4+
using Orleans;
5+
6+
namespace ManagedCode.Orleans.StateMachine.Tests.Cluster.Grains.Interfaces;
7+
8+
// Use enums instead of string constants
9+
public enum TestOrleansContextStates
10+
{
11+
Initial,
12+
Active,
13+
Processing,
14+
Final
15+
}
16+
17+
public enum TestOrleansContextTriggers
18+
{
19+
Activate,
20+
Process,
21+
Complete,
22+
Reset,
23+
Deactivate
24+
}
25+
26+
public interface ITestOrleansContextGrain : IGrainWithStringKey, IStateMachineGrain<TestOrleansContextStates, TestOrleansContextTriggers>
27+
{
28+
Task<List<string>> GetExecutionLog();
29+
Task ClearLog();
30+
}

ManagedCode.Orleans.StateMachine.Tests/Cluster/Grains/Interfaces/ITestStatelessGrain.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ namespace ManagedCode.Orleans.StateMachine.Tests.Cluster.Grains.Interfaces;
55
public interface ITestStatelessGrain : IGrainWithStringKey, IStateMachineGrain<string, char>
66
{
77
Task<string> DoSomethingElse(char input);
8-
}
8+
}
9+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using ManagedCode.Orleans.StateMachine.Extensions;
4+
using ManagedCode.Orleans.StateMachine.Tests.Cluster.Grains.Interfaces;
5+
using Stateless;
6+
7+
namespace ManagedCode.Orleans.StateMachine.Tests.Cluster.Grains;
8+
9+
public class TestOrleansContextGrain : StateMachineGrain<TestOrleansContextStates, TestOrleansContextTriggers>, ITestOrleansContextGrain
10+
{
11+
private readonly List<string> _executionLog = new();
12+
13+
public Task<List<string>> GetExecutionLog()
14+
{
15+
return Task.FromResult(_executionLog);
16+
}
17+
18+
public Task ClearLog()
19+
{
20+
_executionLog.Clear();
21+
return Task.CompletedTask;
22+
}
23+
24+
private Task Log(string message)
25+
{
26+
_executionLog.Add(message);
27+
// Simulate async work
28+
return Task.Delay(1);
29+
}
30+
31+
protected override StateMachine<TestOrleansContextStates, TestOrleansContextTriggers> BuildStateMachine()
32+
{
33+
var machine = new StateMachine<TestOrleansContextStates, TestOrleansContextTriggers>(TestOrleansContextStates.Initial);
34+
35+
machine.Configure(TestOrleansContextStates.Initial)
36+
.OnActivateOrleansContextAsync(() => Log("Initial.Activate"))
37+
.OnExitOrleansContextAsync(() => Log("Initial.Exit"))
38+
.Permit(TestOrleansContextTriggers.Activate, TestOrleansContextStates.Active);
39+
40+
machine.Configure(TestOrleansContextStates.Active)
41+
.OnEntryOrleansContextAsync(() => Log("Active.Entry"))
42+
.OnEntryFromOrleansContextAsync(TestOrleansContextTriggers.Activate, () => Log("Active.EntryFrom.Activate"))
43+
.OnEntryFromOrleansContextAsync(TestOrleansContextTriggers.Reset, (StateMachine<TestOrleansContextStates, TestOrleansContextTriggers>.Transition t) => Log($"Active.EntryFrom.Reset (via {t.Trigger})"))
44+
.OnExitOrleansContextAsync(() => Log("Active.Exit"))
45+
.OnActivateOrleansContextAsync(() => Log("Active.Activate"))
46+
.OnDeactivateOrleansContextAsync(() => Log("Active.Deactivate"))
47+
.Permit(TestOrleansContextTriggers.Process, TestOrleansContextStates.Processing)
48+
.Permit(TestOrleansContextTriggers.Deactivate, TestOrleansContextStates.Final);
49+
50+
machine.Configure(TestOrleansContextStates.Processing)
51+
.OnEntryOrleansContextAsync((StateMachine<TestOrleansContextStates, TestOrleansContextTriggers>.Transition t) => Log($"Processing.Entry (via {t.Trigger})"))
52+
.OnEntryFromOrleansContextAsync(TestOrleansContextTriggers.Process, (StateMachine<TestOrleansContextStates, TestOrleansContextTriggers>.Transition t) => Log($"Processing.EntryFrom.Process (id:{t.Parameters?[0] ?? "?"})"))
53+
.OnExitOrleansContextAsync((StateMachine<TestOrleansContextStates, TestOrleansContextTriggers>.Transition t) => Log($"Processing.Exit (to {t.Destination})"))
54+
.Permit(TestOrleansContextTriggers.Complete, TestOrleansContextStates.Final)
55+
.Permit(TestOrleansContextTriggers.Reset, TestOrleansContextStates.Active);
56+
57+
machine.Configure(TestOrleansContextStates.Final)
58+
.OnEntryOrleansContextAsync(() => Log("Final.Entry"))
59+
.OnEntryFromOrleansContextAsync(TestOrleansContextTriggers.Complete, (StateMachine<TestOrleansContextStates, TestOrleansContextTriggers>.Transition t) =>
60+
{
61+
var msg = t.Parameters?[0] ?? "?";
62+
var success = t.Parameters?.Length > 1 && t.Parameters[1] is bool b
63+
? b.ToString().ToLowerInvariant() // Ensure boolean is lowercase for logging
64+
: "?";
65+
return Log($"Final.EntryFrom.Complete (msg:{msg}, success:{success})");
66+
})
67+
.Permit(TestOrleansContextTriggers.Reset, TestOrleansContextStates.Active)
68+
.OnExitOrleansContextAsync(() => Log("Final.Exit"))
69+
.OnActivateOrleansContextAsync(() => Log("Final.Activate"))
70+
.OnDeactivateOrleansContextAsync(() => Log("Final.Deactivate"));
71+
72+
return machine;
73+
}
74+
}

ManagedCode.Orleans.StateMachine.Tests/StateMachineGrainTests.cs

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using ManagedCode.Orleans.StateMachine.Tests.Cluster.Grains.Interfaces;
44
using Xunit;
55
using Xunit.Abstractions;
6+
using System.Collections.Generic; // Added for List<string>
7+
using Stateless; // Added for StateMachine
68

79
namespace ManagedCode.Orleans.StateMachine.Tests;
810

@@ -37,7 +39,7 @@ await Assert.ThrowsAsync<InvalidOperationException>(
3739
[Fact]
3840
public async Task TestStatelessGrainTests()
3941
{
40-
var grain = _testApp.Cluster.Client.GetGrain<ITestStatelessGrain>("test");
42+
var grain = _testApp.Cluster.Client.GetGrain<ITestStatelessGrain>("test-stateless"); // Changed key
4143

4244
await grain.FireAsync(' ');
4345
(await grain.GetStateAsync()).Should().Be(Constants.On);
@@ -52,4 +54,82 @@ await Assert.ThrowsAsync<InvalidOperationException>(
5254
var into = await grain.GetInfoAsync();
5355
into.InitialState.UnderlyingState.Should().Be(Constants.Off);
5456
}
55-
}
57+
58+
[Fact]
59+
public async Task OrleansContextExtensions_ExecuteInOrder()
60+
{
61+
var grain = _testApp.Cluster.Client.GetGrain<ITestOrleansContextGrain>("test-orleans-context");
62+
await grain.ClearLog();
63+
64+
// 1. Initial Activation
65+
await grain.ActivateAsync();
66+
var log = await grain.GetExecutionLog();
67+
log.Should().ContainInOrder("Initial.Activate");
68+
(await grain.GetStateAsync()).Should().Be(TestOrleansContextStates.Initial);
69+
await grain.ClearLog();
70+
71+
// 2. Transition Initial -> Active
72+
await grain.FireAsync(TestOrleansContextTriggers.Activate);
73+
log = await grain.GetExecutionLog();
74+
log.Should().ContainInOrder(
75+
"Initial.Exit", // Expect OnExit during transition
76+
"Active.Entry",
77+
"Active.EntryFrom.Activate"
78+
);
79+
(await grain.GetStateAsync()).Should().Be(TestOrleansContextStates.Active);
80+
await grain.ClearLog();
81+
82+
// 3. Activate Active state (should trigger OnActivate)
83+
await grain.ActivateAsync();
84+
log = await grain.GetExecutionLog();
85+
log.Should().ContainInOrder("Active.Activate");
86+
await grain.ClearLog();
87+
88+
// 4. Transition Active -> Processing (with parameters)
89+
await grain.FireAsync(TestOrleansContextTriggers.Process, 123);
90+
log = await grain.GetExecutionLog();
91+
log.Should().ContainInOrder(
92+
"Active.Exit", // Expect OnExit during transition
93+
"Processing.Entry (via Process)",
94+
"Processing.EntryFrom.Process (id:123)"
95+
);
96+
(await grain.GetStateAsync()).Should().Be(TestOrleansContextStates.Processing);
97+
await grain.ClearLog();
98+
99+
// 5. Transition Processing -> Final (with parameters)
100+
await grain.FireAsync(TestOrleansContextTriggers.Complete, "Done", true);
101+
log = await grain.GetExecutionLog();
102+
log.Should().ContainInOrder(
103+
"Processing.Exit (to Final)",
104+
"Final.Entry",
105+
"Final.EntryFrom.Complete (msg:Done, success:true)"
106+
);
107+
(await grain.GetStateAsync()).Should().Be(TestOrleansContextStates.Final);
108+
await grain.ClearLog();
109+
110+
// 6. Activate Final state
111+
await grain.ActivateAsync();
112+
log = await grain.GetExecutionLog();
113+
log.Should().ContainInOrder("Final.Activate");
114+
await grain.ClearLog();
115+
116+
// 7. Deactivate Final state (should not trigger OnExit as it's not a transition)
117+
await grain.DeactivateAsync();
118+
log = await grain.GetExecutionLog();
119+
log.Should().ContainInOrder("Final.Deactivate");
120+
await grain.ClearLog();
121+
122+
// 8. Reset from Processing back to Active (test OnEntryFrom with Transition)
123+
(await grain.CanFireAsync(TestOrleansContextTriggers.Activate)).Should().BeFalse(); // Go back to Active first
124+
await grain.ClearLog();
125+
126+
await grain.FireAsync(TestOrleansContextTriggers.Reset, "Reason", 99, false);
127+
log = await grain.GetExecutionLog();
128+
log.Should().ContainInOrder(
129+
"Final.Exit", // Exit Processing
130+
"Active.Entry", // Enter Active
131+
"Active.EntryFrom.Reset (via Reset)" // Specific EntryFrom for Reset
132+
);
133+
(await grain.GetStateAsync()).Should().Be(TestOrleansContextStates.Active);
134+
}
135+
}

ManagedCode.Orleans.StateMachine/Extensions/StateMachineExtensions.cs

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,97 @@ public static StateMachine<TState, TEvent>.StateConfiguration OnEntryOrleansCont
1010
this StateMachine<TState, TEvent>.StateConfiguration machine,
1111
Func<Task> entryAction, string entryActionDescription = null)
1212
{
13-
return machine.OnEntryAsync(() => Task.Factory.StartNew(entryAction), entryActionDescription);
13+
return machine.OnEntryAsync(() => Task.Factory.StartNew(entryAction).Unwrap(), entryActionDescription);
1414
}
15-
}
15+
16+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryOrleansContextAsync<TState, TEvent>(
17+
this StateMachine<TState, TEvent>.StateConfiguration machine,
18+
Func<StateMachine<TState, TEvent>.Transition, Task> entryAction, string entryActionDescription = null)
19+
{
20+
return machine.OnEntryAsync(t => Task.Factory.StartNew(() => entryAction(t)).Unwrap(), entryActionDescription);
21+
}
22+
23+
public static StateMachine<TState, TEvent>.StateConfiguration OnExitOrleansContextAsync<TState, TEvent>(
24+
this StateMachine<TState, TEvent>.StateConfiguration machine,
25+
Func<Task> exitAction, string exitActionDescription = null)
26+
{
27+
return machine.OnExitAsync(() => Task.Factory.StartNew(exitAction).Unwrap(), exitActionDescription);
28+
}
29+
30+
public static StateMachine<TState, TEvent>.StateConfiguration OnExitOrleansContextAsync<TState, TEvent>(
31+
this StateMachine<TState, TEvent>.StateConfiguration machine,
32+
Func<StateMachine<TState, TEvent>.Transition, Task> exitAction, string exitActionDescription = null)
33+
{
34+
return machine.OnExitAsync(t => Task.Factory.StartNew(() => exitAction(t)).Unwrap(), exitActionDescription);
35+
}
36+
37+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent>(
38+
this StateMachine<TState, TEvent>.StateConfiguration machine,
39+
TEvent trigger, Func<Task> entryAction, string entryActionDescription = null)
40+
{
41+
return machine.OnEntryFromAsync(trigger, () => Task.Factory.StartNew(entryAction).Unwrap(), entryActionDescription);
42+
}
43+
44+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent>(
45+
this StateMachine<TState, TEvent>.StateConfiguration machine,
46+
TEvent trigger, Func<StateMachine<TState, TEvent>.Transition, Task> entryAction, string entryActionDescription = null)
47+
{
48+
return machine.OnEntryFromAsync(trigger, t => Task.Factory.StartNew(() => entryAction(t)).Unwrap(), entryActionDescription);
49+
}
50+
51+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent, TArg0>(
52+
this StateMachine<TState, TEvent>.StateConfiguration machine,
53+
StateMachine<TState, TEvent>.TriggerWithParameters<TArg0> trigger, Func<TArg0, Task> entryAction, string entryActionDescription = null)
54+
{
55+
return machine.OnEntryFromAsync(trigger, arg0 => Task.Factory.StartNew(() => entryAction(arg0)).Unwrap(), entryActionDescription);
56+
}
57+
58+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent, TArg0>(
59+
this StateMachine<TState, TEvent>.StateConfiguration machine,
60+
StateMachine<TState, TEvent>.TriggerWithParameters<TArg0> trigger, Func<TArg0, StateMachine<TState, TEvent>.Transition, Task> entryAction, string entryActionDescription = null)
61+
{
62+
return machine.OnEntryFromAsync(trigger, (arg0, t) => Task.Factory.StartNew(() => entryAction(arg0, t)).Unwrap(), entryActionDescription);
63+
}
64+
65+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent, TArg0, TArg1>(
66+
this StateMachine<TState, TEvent>.StateConfiguration machine,
67+
StateMachine<TState, TEvent>.TriggerWithParameters<TArg0, TArg1> trigger, Func<TArg0, TArg1, Task> entryAction, string entryActionDescription = null)
68+
{
69+
return machine.OnEntryFromAsync(trigger, (arg0, arg1) => Task.Factory.StartNew(() => entryAction(arg0, arg1)).Unwrap(), entryActionDescription);
70+
}
71+
72+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent, TArg0, TArg1>(
73+
this StateMachine<TState, TEvent>.StateConfiguration machine,
74+
StateMachine<TState, TEvent>.TriggerWithParameters<TArg0, TArg1> trigger, Func<TArg0, TArg1, StateMachine<TState, TEvent>.Transition, Task> entryAction, string entryActionDescription = null)
75+
{
76+
return machine.OnEntryFromAsync(trigger, (arg0, arg1, t) => Task.Factory.StartNew(() => entryAction(arg0, arg1, t)).Unwrap(), entryActionDescription);
77+
}
78+
79+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent, TArg0, TArg1, TArg2>(
80+
this StateMachine<TState, TEvent>.StateConfiguration machine,
81+
StateMachine<TState, TEvent>.TriggerWithParameters<TArg0, TArg1, TArg2> trigger, Func<TArg0, TArg1, TArg2, Task> entryAction, string entryActionDescription = null)
82+
{
83+
return machine.OnEntryFromAsync(trigger, (arg0, arg1, arg2) => Task.Factory.StartNew(() => entryAction(arg0, arg1, arg2)).Unwrap(), entryActionDescription);
84+
}
85+
86+
public static StateMachine<TState, TEvent>.StateConfiguration OnEntryFromOrleansContextAsync<TState, TEvent, TArg0, TArg1, TArg2>(
87+
this StateMachine<TState, TEvent>.StateConfiguration machine,
88+
StateMachine<TState, TEvent>.TriggerWithParameters<TArg0, TArg1, TArg2> trigger, Func<TArg0, TArg1, TArg2, StateMachine<TState, TEvent>.Transition, Task> entryAction, string entryActionDescription = null)
89+
{
90+
return machine.OnEntryFromAsync(trigger, (arg0, arg1, arg2, t) => Task.Factory.StartNew(() => entryAction(arg0, arg1, arg2, t)).Unwrap(), entryActionDescription);
91+
}
92+
93+
public static StateMachine<TState, TEvent>.StateConfiguration OnActivateOrleansContextAsync<TState, TEvent>(
94+
this StateMachine<TState, TEvent>.StateConfiguration machine,
95+
Func<Task> activateAction, string activateActionDescription = null)
96+
{
97+
return machine.OnActivateAsync(() => Task.Factory.StartNew(activateAction).Unwrap(), activateActionDescription);
98+
}
99+
100+
public static StateMachine<TState, TEvent>.StateConfiguration OnDeactivateOrleansContextAsync<TState, TEvent>(
101+
this StateMachine<TState, TEvent>.StateConfiguration machine,
102+
Func<Task> deactivateAction, string deactivateActionDescription = null)
103+
{
104+
return machine.OnDeactivateAsync(() => Task.Factory.StartNew(deactivateAction).Unwrap(), deactivateActionDescription);
105+
}
106+
}

0 commit comments

Comments
 (0)