Skip to content

Commit 983a9f1

Browse files
authored
Merge pull request #628 from dotnet-state-machine/dev
Release 5.19.0
2 parents 150f51e + fe59691 commit 983a9f1

25 files changed

+656
-133
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## 5.19.0 - 2025.08.22
8+
### Added
9+
- Added methods to unregister from state machine events [#625]
10+
- `OnTransitionedUnregister` and `OnTransitionedAsyncUnregister` for transition events
11+
- `OnTransitionCompletedUnregister` and `OnTransitionCompletedAsyncUnregister` for transition completed events
12+
- `UnregisterAllCallbacks` to unregister all callbacks at once
13+
### Fixed
14+
- Fixed transition precedence issue where substate transitions were not given priority over parent state transitions [#626]
15+
### Changed
16+
- Improved performance by replacing string concatenations with `StringBuilder` in graph generation [#622]
17+
- Moved repeated string literal into `internal const string` for better maintainability [#622]
18+
- Refactored reflection classes for better code organization and consistency [#623]
19+
- Enhanced parameter conversion with additional test coverage [#623]
20+
721
## 5.18.0 - 2025.08.02
822
### Added
923
- Added support for `PermitIfAsync` and `PermitReentryIfAsync` methods to allow async guard conditions [#618], [#189]
@@ -241,6 +255,10 @@ Version 5.10.0 is now listed as the newest, since it has the highest version num
241255
### Removed
242256
### Fixed
243257

258+
[#626]: https://github.com/dotnet-state-machine/stateless/pull/626
259+
[#625]: https://github.com/dotnet-state-machine/stateless/pull/625
260+
[#623]: https://github.com/dotnet-state-machine/stateless/pull/623
261+
[#622]: https://github.com/dotnet-state-machine/stateless/pull/622
244262
[#618]: https://github.com/dotnet-state-machine/stateless/pull/618
245263
[#610]: https://github.com/dotnet-state-machine/stateless/pull/610
246264
[#604]: https://github.com/dotnet-state-machine/stateless/issues/604

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,16 +212,70 @@ Stateless supports 2 types of state machine events:
212212

213213
#### State transition
214214
```csharp
215+
// Synchronously
215216
stateMachine.OnTransitioned((transition) => { });
217+
218+
// Asynchronously
219+
stateMachine.OnTransitionedAsync((transition) => { return Task.FromResult(0); });
216220
```
217221
This event will be invoked every time the state machine changes state.
218222

219223
#### State machine transition completed
220224
```csharp
225+
// Synchronously
221226
stateMachine.OnTransitionCompleted((transition) => { });
227+
228+
// Asynchronously
229+
stateMachine.OnTransitionCompletedAsync((transition) => { return Task.FromResult(0); });
222230
```
223231
This event will be invoked at the very end of the trigger handling, after the last entry action has been executed.
224232

233+
---
234+
235+
In addition to this, Stateless also provides you with the ability to unregister from state machine events in 3 ways.
236+
* State transition unregister (sync/async)
237+
* State machine transition completed (sync/async)
238+
* State machine unregister from all (sync and async)
239+
240+
241+
#### State machine transition unregister (synchronous)
242+
```csharp
243+
// Keep a reference to the synchronous callback action we want to unregister later.
244+
Action transitionCallbackAction = (transition) => { };
245+
stateMachine.OnTransitionedUnregister(transitionCallbackAction);
246+
```
247+
This method will unregister the specified action callback from the transition event.
248+
249+
#### State machine transition unregister (asynchronous)
250+
```csharp
251+
// Keep a reference to the asynchronous callback function we want to unregister later.
252+
Func<Transition, Task> transitionAsyncCallback => (transition) => { return Task.FromResult(0); };
253+
stateMachine.OnTransitionedAsyncUnregister(transitionAsyncCallback);
254+
````
255+
This method will unregister the specified async function callback from the transition event.
256+
257+
#### State machine transition completed unregister (synchronous)
258+
```csharp
259+
// Keep a reference to the synchronous callback action we want to unregister later.
260+
Action transitionCompletedCallbackAction = (transition) => { });
261+
stateMachine.OnTransitionCompletedUnregister(transitionCompletedCallbackAction);
262+
```
263+
This method will unregister the specified action callback from the transition completed event.
264+
265+
#### State machine transition completed unregister (asynchronous)
266+
```csharp
267+
// Keep a reference to to the asynchronous callback function we want to unregister later.
268+
Func<Transition, Task> transitionCompletedAsyncCallback => (transition) => { return Task.FromResult(0); });
269+
stateMachine.OnTransitionCompletedAsyncUnregister(transitionCompletedAsyncCallback);
270+
```
271+
This method will unregister the specified async function callback from the transition completed event.
272+
273+
#### Unregister all registered callbacks (sync and async)
274+
```csharp
275+
stateMachine.UnregisterAllCallbacks();
276+
```
277+
This method will unregister all synchronous and asynchronously registered callbacks from the state machine.
278+
225279
### Export to DOT graph
226280

227281
It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

example/OnOffExample/Program.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Stateless;
3+
using Stateless.Graph;
34

45
namespace OnOffExample
56
{
@@ -9,6 +10,11 @@ namespace OnOffExample
910
/// </summary>
1011
class Program
1112
{
13+
static void TransitionAnnounce(StateMachine<string, char>.Transition transition)
14+
{
15+
Console.WriteLine($"State Machine Transitioning from '{transition.Source}' to '{transition.Destination}'");
16+
}
17+
1218
static void Main(string[] args)
1319
{
1420
const string on = "On";
@@ -28,9 +34,31 @@ static void Main(string[] args)
2834
{
2935
Console.WriteLine("Switch is in state: " + onOffSwitch.State);
3036
var pressed = Console.ReadKey(true).KeyChar;
31-
37+
3238
// Check if user wants to exit
33-
if (pressed != space) break;
39+
if (pressed != space)
40+
{
41+
// Before exiting this is how you can safely register and unregister from transition events.
42+
// Keys 'r' or 'R' = Register for transition events with double subscription prevention built-in.
43+
// Keys 'u' or 'U' = Unregister from transition events to prevent memory leaks in long running applications.
44+
switch (pressed)
45+
{
46+
case 'r':
47+
case 'R':
48+
onOffSwitch.OnTransitioned(TransitionAnnounce);
49+
Console.WriteLine("Now subscribed to transition events..");
50+
continue;
51+
52+
case 'u':
53+
case 'U':
54+
onOffSwitch.OnTransitionedUnregister(TransitionAnnounce);
55+
Console.WriteLine("Successfully unsubscribed from transition events..");
56+
continue;
57+
}
58+
59+
Console.WriteLine("Exiting program");
60+
break;
61+
}
3462

3563
// Use the Fire method with the trigger as payload to supply the state machine with an event.
3664
// The state machine will react according to its configuration.

src/Stateless/Graph/GraphStyleBase.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ public abstract class GraphStyleBase
6464
/// <returns>Description of all transitions, in the desired format.</returns>
6565
public virtual List<string> FormatAllTransitions(List<Transition> transitions)
6666
{
67-
List<string> lines = new List<string>();
68-
if (transitions == null) return lines;
67+
if (transitions == null)
68+
return new List<string>();
69+
70+
// Eagerly set the initial capacity to minimize re-allocation of internal array.
71+
List<string> lines = new List<string>(transitions.Count);
6972

7073
foreach (var transit in transitions)
7174
{
@@ -84,26 +87,23 @@ public virtual List<string> FormatAllTransitions(List<Transition> transitions)
8487
stay.SourceState.NodeName, stay.Guards.Select(x => x.Description));
8588
}
8689
}
87-
else
90+
else if (transit is FixedTransition fix)
8891
{
89-
if (transit is FixedTransition fix)
90-
{
91-
line = FormatOneTransition(fix.SourceState.NodeName, fix.Trigger.UnderlyingTrigger.ToString(),
92+
line = FormatOneTransition(fix.SourceState.NodeName, fix.Trigger.UnderlyingTrigger.ToString(),
9293
fix.DestinationEntryActions.Select(x => x.Method.Description),
9394
fix.DestinationState.NodeName, fix.Guards.Select(x => x.Description));
94-
}
95-
else
96-
{
97-
if (transit is DynamicTransition dyn)
98-
{
99-
line = FormatOneTransition(dyn.SourceState.NodeName, dyn.Trigger.UnderlyingTrigger.ToString(),
95+
}
96+
else if (transit is DynamicTransition dyn)
97+
{
98+
line = FormatOneTransition(dyn.SourceState.NodeName, dyn.Trigger.UnderlyingTrigger.ToString(),
10099
dyn.DestinationEntryActions.Select(x => x.Method.Description),
101100
dyn.DestinationState.NodeName, new List<string> { dyn.Criterion });
102-
}
103-
else
104-
throw new ArgumentException("Unexpected transition type");
105-
}
106101
}
102+
else
103+
{
104+
throw new ArgumentException("Unexpected transition type");
105+
}
106+
107107
if (line != null)
108108
lines.Add(line);
109109
}

src/Stateless/Graph/MermaidGraphStyle.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,21 @@ public override string FormatOneState(State state)
6666
public override string GetPrefix()
6767
{
6868
BuildSanitizedNamedStateMap();
69-
string prefix = "stateDiagram-v2";
69+
70+
StringBuilder sb = new StringBuilder("stateDiagram-v2");
7071
if (_direction.HasValue)
7172
{
72-
prefix += $"{Environment.NewLine}\tdirection {GetDirectionCode(_direction.Value)}";
73+
sb.AppendLine();
74+
sb.Append($"\tdirection {GetDirectionCode(_direction.Value)}");
7375
}
7476

7577
foreach (var state in _stateMap.Where(x => !x.Key.Equals(x.Value.StateName, StringComparison.Ordinal)))
7678
{
77-
prefix += $"{Environment.NewLine}\t{state.Key} : {state.Value.StateName}";
79+
sb.AppendLine();
80+
sb.Append($"\t{state.Key} : {state.Value.StateName}");
7881
}
7982

80-
return prefix;
83+
return sb.ToString();
8184
}
8285

8386
/// <inheritdoc/>
@@ -91,25 +94,29 @@ public override string GetInitialTransition(StateInfo initialState)
9194
/// <inheritdoc/>
9295
public override string FormatOneTransition(string sourceNodeName, string trigger, IEnumerable<string> actions, string destinationNodeName, IEnumerable<string> guards)
9396
{
94-
string label = trigger ?? "";
97+
StringBuilder sb = new StringBuilder(trigger ?? string.Empty);
9598

9699
if (actions?.Count() > 0)
97-
label += " / " + string.Join(", ", actions);
100+
{
101+
sb.Append(" / ");
102+
sb.Append(string.Join(", ", actions));
103+
}
98104

99105
if (guards.Any())
100106
{
101107
foreach (var info in guards)
102108
{
103-
if (label.Length > 0)
104-
label += " ";
105-
label += "[" + info + "]";
109+
if (sb.Length > 0)
110+
sb.Append(" ");
111+
112+
sb.Append("[" + info + "]");
106113
}
107114
}
108115

109116
var sanitizedSourceNodeName = GetSanitizedStateName(sourceNodeName);
110117
var sanitizedDestinationNodeName = GetSanitizedStateName(destinationNodeName);
111118

112-
return FormatOneLine(sanitizedSourceNodeName, sanitizedDestinationNodeName, label);
119+
return FormatOneLine(sanitizedSourceNodeName, sanitizedDestinationNodeName, sb.ToString());
113120
}
114121

115122
internal string FormatOneLine(string fromNodeName, string toNodeName, string label)

src/Stateless/Graph/StateGraph.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
24
using System.Linq;
5+
using System.Text;
36
using Stateless.Reflection;
47

58
namespace Stateless.Graph
@@ -58,37 +61,41 @@ public StateGraph(StateMachineInfo machineInfo)
5861
/// <returns></returns>
5962
public string ToGraph(GraphStyleBase style)
6063
{
61-
string dirgraphText = style.GetPrefix();
64+
StringBuilder sb = new StringBuilder(style.GetPrefix());
6265

6366
// Start with the clusters
6467
foreach (var state in States.Values.Where(x => x is SuperState))
6568
{
66-
dirgraphText += style.FormatOneCluster((SuperState)state);
69+
sb.Append(style.FormatOneCluster((SuperState)state));
6770
}
6871

6972
// Next process all non-cluster states
7073
foreach (var state in States.Values)
7174
{
7275
if (state is SuperState || state is Decision || state.SuperState != null)
7376
continue;
74-
dirgraphText += style.FormatOneState(state);
77+
78+
sb.Append(style.FormatOneState(state));
7579
}
7680

7781
// Finally, add decision nodes
7882
foreach (var dec in Decisions)
7983
{
80-
dirgraphText += style.FormatOneDecisionNode(dec.NodeName, dec.Method.Description);
84+
sb.Append(style.FormatOneDecisionNode(dec.NodeName, dec.Method.Description));
8185
}
8286

8387
// now build behaviours
8488
List<string> transits = style.FormatAllTransitions(Transitions);
8589
foreach (var transit in transits)
86-
dirgraphText += System.Environment.NewLine + transit;
90+
{
91+
sb.Append(Environment.NewLine);
92+
sb.Append(transit);
93+
}
8794

8895
// Add initial transition if present
89-
dirgraphText += style.GetInitialTransition(initialState);
96+
sb.Append(style.GetInitialTransition(initialState));
9097

91-
return dirgraphText;
98+
return sb.ToString();
9299
}
93100

94101
/// <summary>
@@ -202,8 +209,9 @@ void AddSingleStates(StateMachineInfo machineInfo)
202209
{
203210
foreach (var stateInfo in machineInfo.States)
204211
{
205-
if (!States.ContainsKey(stateInfo.UnderlyingState.ToString()))
206-
States[stateInfo.UnderlyingState.ToString()] = new State(stateInfo);
212+
string underlyingState = stateInfo.UnderlyingState.ToString();
213+
if (!States.ContainsKey(underlyingState))
214+
States[underlyingState] = new State(stateInfo);
207215
}
208216
}
209217

@@ -225,22 +233,23 @@ void AddSubstates(SuperState superState, IEnumerable<StateInfo> substates)
225233
{
226234
foreach (var subState in substates)
227235
{
228-
if (States.ContainsKey(subState.UnderlyingState.ToString()))
236+
string underlyingState = subState.UnderlyingState.ToString();
237+
if (States.ContainsKey(underlyingState))
229238
{
230239
// This shouldn't happen
231240
}
232241
else if (subState.Substates.Any())
233242
{
234243
SuperState sub = new SuperState(subState);
235-
States[subState.UnderlyingState.ToString()] = sub;
244+
States[underlyingState] = sub;
236245
superState.SubStates.Add(sub);
237246
sub.SuperState = superState;
238247
AddSubstates(sub, subState.Substates);
239248
}
240249
else
241250
{
242251
State sub = new State(subState);
243-
States[subState.UnderlyingState.ToString()] = sub;
252+
States[underlyingState] = sub;
244253
superState.SubStates.Add(sub);
245254
sub.SuperState = superState;
246255
}

0 commit comments

Comments
 (0)