Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions PredicateStateMachine /ITransition.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Linq.Expressions;

namespace PredicateStateMachine;

public interface ITransition<TEvent> where TEvent : IEvent
{
Func<TEvent, bool> Selector { get; set; }
Func<TEvent, bool> Predicate { get; }
Expression<Func<TEvent, bool>> Selector { get; set; }
public Expression<Func<TEvent, bool>>? Predicate { get; }
int Priority { get; }
bool CanTransition(TEvent e);
}
83 changes: 76 additions & 7 deletions PredicateStateMachine /PredicateStateMachine.cs
Original file line number Diff line number Diff line change
@@ -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<TEvent> : IPredicateStateMachine<TEvent>
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<TEvent> : IPredicateStateMachine<TEvent>, ISerializableStateMachine<TEvent>
where TEvent : IEvent
{
private readonly ILogger? _logger;
Expand All @@ -20,13 +42,14 @@ public class PredicateStateMachine<TEvent> : IPredicateStateMachine<TEvent>
private readonly Dictionary<IStateNode<TEvent>, ITimer> _timers = new();

private readonly object _lock = new();


public PredicateStateMachine(ILogger? logger = null)
{
_logger = logger;
_activityMonitor = new DefaultActivityMonitor<TEvent>(_logger); // for now
}

public void AddStates(List<IStateNode<TEvent>> newStates)
{
_states ??= [];
Expand All @@ -38,7 +61,8 @@ public void AddStates(List<IStateNode<TEvent>> 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);
}
Expand All @@ -65,7 +89,7 @@ public void HandleEvent(TEvent e)
{
if (LifecycleState != LifecycleState.Running && LifecycleState != LifecycleState.Resumed)
return;

KeyValuePair<ITransition<TEvent>, IStateNode<TEvent>>? selected = null;

lock (_lock)
Expand Down Expand Up @@ -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
Expand All @@ -130,7 +154,7 @@ public void Pause()
_activityMonitor?.RegisterMachinePaused(_current);
_current = null;
}

public void Resume()
{
LifecycleState = LifecycleState.Resumed;
Expand Down Expand Up @@ -176,4 +200,49 @@ private void StopTimer(IStateNode<TEvent> state)
_timers.Remove(state);
}
}


public SerializableStateMachine<TEvent> ToSerializable()
{
var result = new SerializableStateMachine<TEvent>
{
States = _states?.Select(s => new SerializableState<TEvent>
{
Name = s.Name
}).ToList() ?? [],

Transitions = _paths.SelectMany(pathDefinition =>
pathDefinition.Value.Select(kvp =>
{
var transition = (Transition<TEvent>)kvp.Key;
return new SerializableTransition<TEvent>
{
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<TEvent>
{
StateName = kvp.Key.Name,
TimeoutMs = kvp.Value.TimeoutMs,
TimeoutEvent = JsonSerializer.Serialize(kvp.Value.TimeoutEvent)
}).ToList(),

RootStateName = _root?.Name
};

return result;
}

public static PredicateStateMachine<TEvent> FromSerializable(SerializableStateMachine<TEvent> dto)
{
throw new NotImplementedException();
}
}
1 change: 1 addition & 0 deletions PredicateStateMachine /PredicateStateMachine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Serialize.Linq" Version="4.0.167" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace PredicateStateMachine.Serialization;

internal interface ISerializableStateMachine<TEvent> where TEvent : IEvent
{
SerializableStateMachine<TEvent> ToSerializable();
static abstract PredicateStateMachine<TEvent> FromSerializable(SerializableStateMachine<TEvent> dto);
}
7 changes: 7 additions & 0 deletions PredicateStateMachine /Serialization/SerializableState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

namespace PredicateStateMachine.Serialization;

public class SerializableState<TEvent> where TEvent : IEvent
{
public string Name { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace PredicateStateMachine.Serialization;

public class SerializableStateMachine<TEvent> where TEvent : IEvent
{
public string RootStateName { get; set; }
public List<SerializableState<TEvent>> States { get; set; } = new();
public List<SerializableTransition<TEvent>> Transitions { get; set; } = new();
public List<SerializableTimeout<TEvent>> Timeouts { get; set; } = new();
}
8 changes: 8 additions & 0 deletions PredicateStateMachine /Serialization/SerializableTimeout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PredicateStateMachine.Serialization;

public class SerializableTimeout<TEvent> where TEvent : IEvent
{
public string StateName { get; set; }
public double TimeoutMs { get; set; }
public string TimeoutEvent { get; set; } //create a type
}
12 changes: 12 additions & 0 deletions PredicateStateMachine /Serialization/SerializableTransition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace PredicateStateMachine.Serialization;

public class SerializableTransition<TEvent> 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; }
}
33 changes: 22 additions & 11 deletions PredicateStateMachine /Transition.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
namespace PredicateStateMachine;
using System;
using System.Linq.Expressions;

public class Transition<TEvent> : ITransition<TEvent> where TEvent : IEvent
namespace PredicateStateMachine
{
public Transition(Func<TEvent, bool> selector, Func<TEvent, bool>? predicate = null, int priority = 0)
public class Transition<TEvent> : ITransition<TEvent> where TEvent : IEvent
{
Selector = selector;
Predicate = predicate;
Priority = priority;
}
public Transition(Expression<Func<TEvent, bool>> selector, Expression<Func<TEvent, bool>>? predicate = null, int priority = 0)
{
Selector = selector;
Predicate = predicate;
Priority = priority;
_compiledSelector = selector.Compile();
_compiledPredicate = predicate?.Compile();
}

public Expression<Func<TEvent, bool>> Selector { get; set; }
public Expression<Func<TEvent, bool>>? Predicate { get; set; }
public int Priority { get; }

public Func<TEvent, bool> Selector { get; set; }
public Func<TEvent, bool>? Predicate { get; set; }
public int Priority { get; }
public bool CanTransition(TEvent e) => Selector(e) && (Predicate is null || Predicate(e));
private readonly Func<TEvent, bool> _compiledSelector;
private readonly Func<TEvent, bool>? _compiledPredicate;

public bool CanTransition(TEvent e)
=> _compiledSelector(e) && (_compiledPredicate == null || _compiledPredicate(e));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ public static async Task Run()
foreach (var state in new[] { red, green, orange })
{
machine.AddPath(state, new Transition<TrafficEvent>(
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<TrafficEvent>(
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<TrafficEvent>(
Expand Down
36 changes: 23 additions & 13 deletions PredicateStateMachine.Examples/Lock/Example.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using PredicateStateMachine;

Expand Down Expand Up @@ -36,23 +37,32 @@ public static void Run()
};
var lockedOut = new AccessState("LockedOut");

machine.AddPath(idle, new Transition<AccessEvent>(e => e is { Identifier: "Code Entered" }), checking);
machine.AddPath(checking, new Transition<AccessEvent>(e => e is { Identifier: "Granted" }), granted);
machine.AddPath(checking, new Transition<AccessEvent>(e => e is { Identifier: "Denied" }), denied);
machine.AddPath(idle, new Transition<AccessEvent>(e => e.Identifier == "Code Entered"), checking);
machine.AddPath(checking, new Transition<AccessEvent>(e => e.Identifier == "Granted"), granted);
machine.AddPath(checking, new Transition<AccessEvent>(e => e.Identifier == "Denied"), denied);
machine.AddPath(denied, new Transition<AccessEvent>(e => true), idle);
machine.AddPath(denied, new Transition<AccessEvent>(e => e is { Identifier: "Lockout" }, priority: 1), lockedOut);
machine.AddPath(granted, new Transition<AccessEvent>(e => e is { Identifier: "Timeout" }), idle);
machine.AddPath(denied, new Transition<AccessEvent>(e => e.Identifier == "Lockout", priority: 1), lockedOut);
machine.AddPath(granted, new Transition<AccessEvent>(e => e.Identifier == "Timeout"), idle);
machine.AddTimeout(granted, new TimeoutConfiguration<AccessEvent>(3000, new AccessEvent("Timeout")));

machine.AddStates([idle, checking, granted, denied, lockedOut]);
machine.Configure(new StateMachineConfig<AccessEvent>(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"));
}
}
4 changes: 2 additions & 2 deletions PredicateStateMachine.Examples/ModeratedChat/Example.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public static PredicateStateMachine<ModerationEvent> CreateStateMachine()
var rejected = new ModerationState("Rejected");

// note that this needs more edge case configuration
machine.AddPath(normal, new Transition<ModerationEvent>(e => e is { Identifier: "ViolationDetected", Severe: false }), warned);
machine.AddPath(normal, new Transition<ModerationEvent>(e => e is { Identifier: "ViolationDetected", Severe: true }), muted);
machine.AddPath(normal, new Transition<ModerationEvent>(e => e.Identifier == "ViolationDetected" && e.Severe == false), warned);
machine.AddPath(normal, new Transition<ModerationEvent>(e => e.Identifier == "ViolationDetected" && e.Severe == true), muted);
machine.AddPath(warned, new Transition<ModerationEvent>(e => e.Identifier == "ViolationDetected"), muted);
machine.AddPath(muted, new Transition<ModerationEvent>(e => e.Identifier == "ViolationDetected"), banned);
machine.AddTimeout(muted, new TimeoutConfiguration<ModerationEvent>(10000, new ModerationEvent("TimoutExpired")));
Expand Down
6 changes: 3 additions & 3 deletions PredicateStateMachine.Examples/Sensor/Example.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SensorEvent>(e => e is { Identifier: "MovementDetected" }), detected);
machine.AddPath(detected, new Transition<SensorEvent>(e => e is { Identifier: "MovementCleared" }), idle);
machine.AddPath(idle, new Transition<SensorEvent>(e => e.Identifier == "MovementDetected"), detected);
machine.AddPath(detected, new Transition<SensorEvent>(e => e.Identifier == "MovementCleared"), idle);
machine.AddTimeout(detected, new TimeoutConfiguration<SensorEvent>(5000, new SensorEvent("Timeout")));
machine.AddPath(detected, new Transition<SensorEvent>(e => e is { Identifier: "Timeout" }), alarm);
machine.AddPath(detected, new Transition<SensorEvent>(e => e.Identifier == "Timeout"), alarm);


machine.AddStates([idle, detected, alarm]);
Expand Down