From 4e066bd296622275925beb1f146e1662c5b33f81 Mon Sep 17 00:00:00 2001 From: designmatters bv Date: Fri, 13 Jun 2025 21:12:41 +0200 Subject: [PATCH 1/2] Use expressions --- PredicateStateMachine /ITransition.cs | 6 ++-- PredicateStateMachine /Transition.cs | 33 ++++++++++++------- .../EmergencyTrafficLights/Example.cs | 4 +-- .../Lock/Example.cs | 10 +++--- .../ModeratedChat/Example.cs | 4 +-- .../Sensor/Example.cs | 6 ++-- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/PredicateStateMachine /ITransition.cs b/PredicateStateMachine /ITransition.cs index a8c817f..86e8295 100644 --- a/PredicateStateMachine /ITransition.cs +++ b/PredicateStateMachine /ITransition.cs @@ -1,9 +1,11 @@ +using System.Linq.Expressions; + namespace PredicateStateMachine; public interface ITransition where TEvent : IEvent { - Func Selector { get; set; } - Func Predicate { get; } + Expression> Selector { get; set; } + public Expression>? Predicate { get; } int Priority { get; } bool CanTransition(TEvent e); } \ No newline at end of file diff --git a/PredicateStateMachine /Transition.cs b/PredicateStateMachine /Transition.cs index f7eccb1..45ffb0b 100644 --- a/PredicateStateMachine /Transition.cs +++ b/PredicateStateMachine /Transition.cs @@ -1,16 +1,27 @@ -namespace PredicateStateMachine; +using System; +using System.Linq.Expressions; -public class Transition : ITransition where TEvent : IEvent +namespace PredicateStateMachine { - public Transition(Func selector, Func? predicate = null, int priority = 0) + public class Transition : ITransition where TEvent : IEvent { - Selector = selector; - Predicate = predicate; - Priority = priority; - } + public Transition(Expression> selector, Expression>? predicate = null, int priority = 0) + { + Selector = selector; + Predicate = predicate; + Priority = priority; + _compiledSelector = selector.Compile(); + _compiledPredicate = predicate?.Compile(); + } + + public Expression> Selector { get; set; } + public Expression>? Predicate { get; set; } + public int Priority { get; } - public Func Selector { get; set; } - public Func? Predicate { get; set; } - public int Priority { get; } - public bool CanTransition(TEvent e) => Selector(e) && (Predicate is null || Predicate(e)); + private readonly Func _compiledSelector; + private readonly Func? _compiledPredicate; + + public bool CanTransition(TEvent e) + => _compiledSelector(e) && (_compiledPredicate == null || _compiledPredicate(e)); + } } \ No newline at end of file diff --git a/PredicateStateMachine.Examples/EmergencyTrafficLights/Example.cs b/PredicateStateMachine.Examples/EmergencyTrafficLights/Example.cs index 4596bca..6ca0614 100644 --- a/PredicateStateMachine.Examples/EmergencyTrafficLights/Example.cs +++ b/PredicateStateMachine.Examples/EmergencyTrafficLights/Example.cs @@ -36,13 +36,13 @@ public static async Task Run() foreach (var state in new[] { red, green, orange }) { machine.AddPath(state, new Transition( - e => e is { Identifier: "EmergencyDetected", DistanceMeters: 1000 }, priority: 10), orangeFlashing); + e => e.Identifier == "EmergencyDetected" && e.DistanceMeters == 1000, priority: 10), orangeFlashing); } foreach (var state in new[] { red, green, orange, orangeFlashing }) { machine.AddPath(state, new Transition( - e => e is { Identifier: "EmergencyDetected", DistanceMeters: < 500 }, priority: 20), red); + e => e.Identifier == "EmergencyDetected" && e.DistanceMeters < 500, priority: 20), red); } machine.AddPath(red, new Transition( diff --git a/PredicateStateMachine.Examples/Lock/Example.cs b/PredicateStateMachine.Examples/Lock/Example.cs index 312184d..a115f0a 100644 --- a/PredicateStateMachine.Examples/Lock/Example.cs +++ b/PredicateStateMachine.Examples/Lock/Example.cs @@ -36,12 +36,12 @@ public static void Run() }; var lockedOut = new AccessState("LockedOut"); - machine.AddPath(idle, new Transition(e => e is { Identifier: "Code Entered" }), checking); - machine.AddPath(checking, new Transition(e => e is { Identifier: "Granted" }), granted); - machine.AddPath(checking, new Transition(e => e is { Identifier: "Denied" }), denied); + machine.AddPath(idle, new Transition(e => e.Identifier == "Code Entered"), checking); + machine.AddPath(checking, new Transition(e => e.Identifier == "Granted"), granted); + machine.AddPath(checking, new Transition(e => e.Identifier == "Denied"), denied); machine.AddPath(denied, new Transition(e => true), idle); - machine.AddPath(denied, new Transition(e => e is { Identifier: "Lockout" }, priority: 1), lockedOut); - machine.AddPath(granted, new Transition(e => e is { Identifier: "Timeout" }), idle); + machine.AddPath(denied, new Transition(e => e.Identifier == "Lockout", priority: 1), lockedOut); + machine.AddPath(granted, new Transition(e => e.Identifier == "Timeout"), idle); machine.AddTimeout(granted, new TimeoutConfiguration(3000, new AccessEvent("Timeout"))); machine.AddStates([idle, checking, granted, denied, lockedOut]); diff --git a/PredicateStateMachine.Examples/ModeratedChat/Example.cs b/PredicateStateMachine.Examples/ModeratedChat/Example.cs index ca07809..866cec0 100644 --- a/PredicateStateMachine.Examples/ModeratedChat/Example.cs +++ b/PredicateStateMachine.Examples/ModeratedChat/Example.cs @@ -28,8 +28,8 @@ public static PredicateStateMachine CreateStateMachine() var rejected = new ModerationState("Rejected"); // note that this needs more edge case configuration - machine.AddPath(normal, new Transition(e => e is { Identifier: "ViolationDetected", Severe: false }), warned); - machine.AddPath(normal, new Transition(e => e is { Identifier: "ViolationDetected", Severe: true }), muted); + machine.AddPath(normal, new Transition(e => e.Identifier == "ViolationDetected" && e.Severe == false), warned); + machine.AddPath(normal, new Transition(e => e.Identifier == "ViolationDetected" && e.Severe == true), muted); machine.AddPath(warned, new Transition(e => e.Identifier == "ViolationDetected"), muted); machine.AddPath(muted, new Transition(e => e.Identifier == "ViolationDetected"), banned); machine.AddTimeout(muted, new TimeoutConfiguration(10000, new ModerationEvent("TimoutExpired"))); diff --git a/PredicateStateMachine.Examples/Sensor/Example.cs b/PredicateStateMachine.Examples/Sensor/Example.cs index 92c4271..d937c28 100644 --- a/PredicateStateMachine.Examples/Sensor/Example.cs +++ b/PredicateStateMachine.Examples/Sensor/Example.cs @@ -26,10 +26,10 @@ public static void Run() }; // If the sensor stays in movementDetected for at least 5s go to alarm. - machine.AddPath(idle, new Transition(e => e is { Identifier: "MovementDetected" }), detected); - machine.AddPath(detected, new Transition(e => e is { Identifier: "MovementCleared" }), idle); + machine.AddPath(idle, new Transition(e => e.Identifier == "MovementDetected"), detected); + machine.AddPath(detected, new Transition(e => e.Identifier == "MovementCleared"), idle); machine.AddTimeout(detected, new TimeoutConfiguration(5000, new SensorEvent("Timeout"))); - machine.AddPath(detected, new Transition(e => e is { Identifier: "Timeout" }), alarm); + machine.AddPath(detected, new Transition(e => e.Identifier == "Timeout"), alarm); machine.AddStates([idle, detected, alarm]); From cfd816b7f36817c2ba20a2ef343667c08ba28e3e Mon Sep 17 00:00:00 2001 From: designmatters bv Date: Fri, 13 Jun 2025 22:02:40 +0200 Subject: [PATCH 2/2] Implement ToSerializable --- .../PredicateStateMachine.cs | 83 +++++++++++++++++-- .../PredicateStateMachine.csproj | 1 + .../ISerializableStateMachine.cs | 7 ++ .../Serialization/SerializableState.cs | 7 ++ .../Serialization/SerializableStateMachine.cs | 9 ++ .../Serialization/SerializableTimeout.cs | 8 ++ .../Serialization/SerializableTransition.cs | 12 +++ .../Lock/Example.cs | 26 ++++-- 8 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 PredicateStateMachine /Serialization/ISerializableStateMachine.cs create mode 100644 PredicateStateMachine /Serialization/SerializableState.cs create mode 100644 PredicateStateMachine /Serialization/SerializableStateMachine.cs create mode 100644 PredicateStateMachine /Serialization/SerializableTimeout.cs create mode 100644 PredicateStateMachine /Serialization/SerializableTransition.cs diff --git a/PredicateStateMachine /PredicateStateMachine.cs b/PredicateStateMachine /PredicateStateMachine.cs index 5262e47..c389353 100644 --- a/PredicateStateMachine /PredicateStateMachine.cs +++ b/PredicateStateMachine /PredicateStateMachine.cs @@ -1,11 +1,33 @@ using Microsoft.Extensions.Logging; using PredicateStateMachine.ActivityMonitor; using PredicateStateMachine.ActivityMonitor.Impl; +using PredicateStateMachine.Serialization; +using Serialize.Linq.Serializers; using ITimer = PredicateStateMachine.Timer.ITimer; namespace PredicateStateMachine; -public class PredicateStateMachine : IPredicateStateMachine +using Serialize.Linq.Factories; +using Serialize.Linq.Nodes; +using System.Linq.Expressions; +using System.Text.Json; + +public static class ExpressionHelper +{ + public static string SerializeExpression(Expression expression) + { + var serializer = new ExpressionSerializer(new Serialize.Linq.Serializers.JsonSerializer()); //share this TODO + return serializer.SerializeText(expression); + } + + public static Expression DeserializeExpression(string json) + { + var serializer = new ExpressionSerializer(new Serialize.Linq.Serializers.JsonSerializer()); //share this TODO + return serializer.DeserializeText(json); + } +} + +public class PredicateStateMachine : IPredicateStateMachine, ISerializableStateMachine where TEvent : IEvent { private readonly ILogger? _logger; @@ -20,13 +42,14 @@ public class PredicateStateMachine : IPredicateStateMachine private readonly Dictionary, ITimer> _timers = new(); private readonly object _lock = new(); - + public PredicateStateMachine(ILogger? logger = null) { _logger = logger; _activityMonitor = new DefaultActivityMonitor(_logger); // for now } + public void AddStates(List> newStates) { _states ??= []; @@ -38,7 +61,8 @@ public void AddStates(List> newStates) .ToList(); if (duplicates.Count > 0) - throw new ApplicationException($"Every state must have a unique name. Duplicates found: {string.Join(", ", duplicates)}"); + throw new ApplicationException( + $"Every state must have a unique name. Duplicates found: {string.Join(", ", duplicates)}"); _states.AddRange(newStates); } @@ -65,7 +89,7 @@ public void HandleEvent(TEvent e) { if (LifecycleState != LifecycleState.Running && LifecycleState != LifecycleState.Resumed) return; - + KeyValuePair, IStateNode>? selected = null; lock (_lock) @@ -111,14 +135,14 @@ public void Start() if (_root == null) throw new InvalidOperationException("Root state is not set."); LifecycleState = LifecycleState.Running; - _activityMonitor.RegisterMachineStarted(_current , _root); + _activityMonitor.RegisterMachineStarted(_current, _root); _current = _root; _current.OnBeforeStart(); _current.OnStart(); _current.OnAfterStart(); StartTimer(_current); } - + public void Pause() { StopTimer(_current); // TODO. What are the specs here. The timer must pause and resume @@ -130,7 +154,7 @@ public void Pause() _activityMonitor?.RegisterMachinePaused(_current); _current = null; } - + public void Resume() { LifecycleState = LifecycleState.Resumed; @@ -176,4 +200,49 @@ private void StopTimer(IStateNode state) _timers.Remove(state); } } + + + public SerializableStateMachine ToSerializable() + { + var result = new SerializableStateMachine + { + States = _states?.Select(s => new SerializableState + { + Name = s.Name + }).ToList() ?? [], + + Transitions = _paths.SelectMany(pathDefinition => + pathDefinition.Value.Select(kvp => + { + var transition = (Transition)kvp.Key; + return new SerializableTransition + { + SourceStateName = pathDefinition.Key.Name, + TargetStateName = kvp.Value.Name, + Selector = ExpressionHelper.SerializeExpression(transition.Selector), + Predicate = transition.Predicate != null + ? ExpressionHelper.SerializeExpression(transition.Predicate) + : null, + Priority = transition.Priority + }; + }) + ).ToList(), + + Timeouts = _timeouts.Select(kvp => new SerializableTimeout + { + StateName = kvp.Key.Name, + TimeoutMs = kvp.Value.TimeoutMs, + TimeoutEvent = JsonSerializer.Serialize(kvp.Value.TimeoutEvent) + }).ToList(), + + RootStateName = _root?.Name + }; + + return result; + } + + public static PredicateStateMachine FromSerializable(SerializableStateMachine dto) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/PredicateStateMachine /PredicateStateMachine.csproj b/PredicateStateMachine /PredicateStateMachine.csproj index 3ca149d..45a19da 100644 --- a/PredicateStateMachine /PredicateStateMachine.csproj +++ b/PredicateStateMachine /PredicateStateMachine.csproj @@ -9,6 +9,7 @@ + diff --git a/PredicateStateMachine /Serialization/ISerializableStateMachine.cs b/PredicateStateMachine /Serialization/ISerializableStateMachine.cs new file mode 100644 index 0000000..44786d4 --- /dev/null +++ b/PredicateStateMachine /Serialization/ISerializableStateMachine.cs @@ -0,0 +1,7 @@ +namespace PredicateStateMachine.Serialization; + +internal interface ISerializableStateMachine where TEvent : IEvent +{ + SerializableStateMachine ToSerializable(); + static abstract PredicateStateMachine FromSerializable(SerializableStateMachine dto); +} \ No newline at end of file diff --git a/PredicateStateMachine /Serialization/SerializableState.cs b/PredicateStateMachine /Serialization/SerializableState.cs new file mode 100644 index 0000000..8d0b9a8 --- /dev/null +++ b/PredicateStateMachine /Serialization/SerializableState.cs @@ -0,0 +1,7 @@ + +namespace PredicateStateMachine.Serialization; + +public class SerializableState where TEvent : IEvent +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/PredicateStateMachine /Serialization/SerializableStateMachine.cs b/PredicateStateMachine /Serialization/SerializableStateMachine.cs new file mode 100644 index 0000000..8dea6cc --- /dev/null +++ b/PredicateStateMachine /Serialization/SerializableStateMachine.cs @@ -0,0 +1,9 @@ +namespace PredicateStateMachine.Serialization; + +public class SerializableStateMachine where TEvent : IEvent +{ + public string RootStateName { get; set; } + public List> States { get; set; } = new(); + public List> Transitions { get; set; } = new(); + public List> Timeouts { get; set; } = new(); +} \ No newline at end of file diff --git a/PredicateStateMachine /Serialization/SerializableTimeout.cs b/PredicateStateMachine /Serialization/SerializableTimeout.cs new file mode 100644 index 0000000..7c5efc2 --- /dev/null +++ b/PredicateStateMachine /Serialization/SerializableTimeout.cs @@ -0,0 +1,8 @@ +namespace PredicateStateMachine.Serialization; + +public class SerializableTimeout where TEvent : IEvent +{ + public string StateName { get; set; } + public double TimeoutMs { get; set; } + public string TimeoutEvent { get; set; } //create a type +} \ No newline at end of file diff --git a/PredicateStateMachine /Serialization/SerializableTransition.cs b/PredicateStateMachine /Serialization/SerializableTransition.cs new file mode 100644 index 0000000..93aa99e --- /dev/null +++ b/PredicateStateMachine /Serialization/SerializableTransition.cs @@ -0,0 +1,12 @@ +namespace PredicateStateMachine.Serialization; + +public class SerializableTransition where TEvent : IEvent +{ + public string SourceStateName { get; set; } + public string TargetStateName { get; set; } + + public string Selector { get; set; } //T look at + public string? Predicate { get; set; } + + public int Priority { get; set; } +} \ No newline at end of file diff --git a/PredicateStateMachine.Examples/Lock/Example.cs b/PredicateStateMachine.Examples/Lock/Example.cs index a115f0a..fc07058 100644 --- a/PredicateStateMachine.Examples/Lock/Example.cs +++ b/PredicateStateMachine.Examples/Lock/Example.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Microsoft.Extensions.Logging; using PredicateStateMachine; @@ -46,13 +47,22 @@ public static void Run() machine.AddStates([idle, checking, granted, denied, lockedOut]); machine.Configure(new StateMachineConfig(idle)); //merge this into prev - machine.Start(); - - machine.HandleEvent(new AccessEvent("Code Entered")); - machine.HandleEvent(new AccessEvent("Denied")); - machine.HandleEvent(new AccessEvent("Code Entered")); - machine.HandleEvent(new AccessEvent("Denied")); - machine.HandleEvent(new AccessEvent("Code Entered")); - machine.HandleEvent(new AccessEvent("Denied")); + + var options = new JsonSerializerOptions + { + WriteIndented = true + }; + + var json = JsonSerializer.Serialize(machine.ToSerializable(), options); + + Console.WriteLine(json); + // machine.Start(); + // + // machine.HandleEvent(new AccessEvent("Code Entered")); + // machine.HandleEvent(new AccessEvent("Denied")); + // machine.HandleEvent(new AccessEvent("Code Entered")); + // machine.HandleEvent(new AccessEvent("Denied")); + // machine.HandleEvent(new AccessEvent("Code Entered")); + // machine.HandleEvent(new AccessEvent("Denied")); } } \ No newline at end of file