diff --git a/Test/ConsoleApp/ConsoleApp.csproj b/Examples/ConsoleApp/ConsoleApp.csproj similarity index 52% rename from Test/ConsoleApp/ConsoleApp.csproj rename to Examples/ConsoleApp/ConsoleApp.csproj index e1d84f4..3fecda7 100644 --- a/Test/ConsoleApp/ConsoleApp.csproj +++ b/Examples/ConsoleApp/ConsoleApp.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp2.0 + net6.0 - + diff --git a/Test/ConsoleApp/Program.cs b/Examples/ConsoleApp/Program.cs similarity index 86% rename from Test/ConsoleApp/Program.cs rename to Examples/ConsoleApp/Program.cs index 773316b..c728121 100644 --- a/Test/ConsoleApp/Program.cs +++ b/Examples/ConsoleApp/Program.cs @@ -1,5 +1,6 @@ -using RandomSolutions; +using FluentStateMachine; using System; +using System.Threading; using System.Threading.Tasks; namespace ConsoleApp @@ -11,21 +12,7 @@ enum Event { E0, E1, E2, E3 } static async Task Main(string[] args) { - var fsm = CreateFsm(); - var events = new[] { Event.E1, Event.E2, Event.E0, Event.E1, Event.E3 }; - - foreach (var e in events) - { - Console.WriteLine($"{fsm.Current}: {string.Join(", ", await fsm.GetEventsAsync())}"); - Console.WriteLine($"Result: {await fsm.TriggerAsync(e)}\n"); - } - - await fsm.ResetAsync(); - } - - static IStateMachine CreateFsm() - { - return new FsmBuilder(State.S1) + var fsm = new FsmBuilder(State.S1) .OnJump(x => Console.WriteLine($"On jump to {x.Fsm.Current} from {x.PrevState}")) .OnReset(x => Console.WriteLine($"On reset to {x.Fsm.Current} from {x.PrevState}")) .OnTrigger(x => Console.WriteLine($"On trigger {x.Event}")) @@ -42,6 +29,7 @@ static IStateMachine CreateFsm() await Task.Delay(1000); return "some data"; }) + .On(Event.E2).JumpTo(State.S2) .On(Event.E3).JumpTo(State.S3) .State(State.S2) @@ -50,6 +38,18 @@ static IStateMachine CreateFsm() .State(State.S3) .OnEnter(x => Console.WriteLine($"Final state")) .Build(); + + + var events = new[] { Event.E1, Event.E2, Event.E0, Event.E1, Event.E3 }; + + foreach (var e in events) + { + Console.WriteLine($"Current state: {fsm.Current}"); + Console.WriteLine($"Available events: {string.Join(", ", fsm.GetEvents())}"); + Console.WriteLine($"Result: {await fsm.TriggerAsync(e)}\n\n"); + } + + await fsm.ResetAsync(); } } } diff --git a/StateMachine.sln b/FluentStateMachine.sln similarity index 64% rename from StateMachine.sln rename to FluentStateMachine.sln index d0e33bf..96bd9f7 100644 --- a/StateMachine.sln +++ b/FluentStateMachine.sln @@ -1,13 +1,20 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2018 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31717.71 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StateMachine", "StateMachine\StateMachine.csproj", "{D6D3C3E9-73EF-48E9-9D82-017BA0123C24}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentStateMachine", "FluentStateMachine\FluentStateMachine.csproj", "{D6D3C3E9-73EF-48E9-9D82-017BA0123C24}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "Test\ConsoleApp\ConsoleApp.csproj", "{EA950F42-CB53-4119-8AB9-D9305AE8A32A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "Examples\ConsoleApp\ConsoleApp.csproj", "{EA950F42-CB53-4119-8AB9-D9305AE8A32A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{4C1BDDED-BF17-4F95-B295-4AC93351993A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{4C1BDDED-BF17-4F95-B295-4AC93351993A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{35BB4148-B10F-4F43-833D-8FD5169DDA63}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + LICENSE = LICENSE + README.md = README.md + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/StateMachine/key.snk b/FluentStateMachine.snk similarity index 100% rename from StateMachine/key.snk rename to FluentStateMachine.snk diff --git a/FluentStateMachine/FluentStateMachine.csproj b/FluentStateMachine/FluentStateMachine.csproj new file mode 100644 index 0000000..241f375 --- /dev/null +++ b/FluentStateMachine/FluentStateMachine.csproj @@ -0,0 +1,35 @@ + + + + net45;netstandard2.0;netstandard2.1; + FluentStateMachine + FluentStateMachine + true + ..\FluentStateMachine.snk + 1.0.0 + 1.0.0 + 1.0.0 + + + Leonid Salavatov + Leonid Salavatov 2021 + FluentStateMachine + FluentStateMachine + FluentStateMachine + .NET Finite-state machine (FSM) with a fluent interface + fluent statemachine finitestatemachine fsm dotnet + true + https://github.com/mustaddon/StateMachine + https://github.com/mustaddon/StateMachine + git + false + MIT + + + + + + NET45 + + + diff --git a/FluentStateMachine/FrameworkExt.cs b/FluentStateMachine/FrameworkExt.cs new file mode 100644 index 0000000..c4a16ab --- /dev/null +++ b/FluentStateMachine/FrameworkExt.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace FluentStateMachine +{ + internal static class FrameworkExt + { + +#if NET45 + public static readonly Task CompletedTask = Task.FromResult(false); +#else + public static readonly Task CompletedTask = Task.CompletedTask; +#endif + + } +} diff --git a/StateMachine/FsmArgs.cs b/FluentStateMachine/FsmArgs.cs similarity index 85% rename from StateMachine/FsmArgs.cs rename to FluentStateMachine/FsmArgs.cs index fc13e8d..bd70c7c 100644 --- a/StateMachine/FsmArgs.cs +++ b/FluentStateMachine/FsmArgs.cs @@ -1,8 +1,11 @@ -namespace RandomSolutions +using System.Threading; + +namespace FluentStateMachine { public class FsmArgs { public IStateMachine Fsm { get; internal set; } + public CancellationToken CancellationToken { get; internal set; } } public class FsmResetArgs : FsmArgs @@ -12,7 +15,7 @@ public class FsmResetArgs : FsmArgs public class FsmDataArgs : FsmArgs { - public object[] Data { get; internal set; } + public object Data { get; internal set; } } public class FsmExitArgs : FsmDataArgs diff --git a/StateMachine/FsmBuilder.cs b/FluentStateMachine/FsmBuilder.cs similarity index 62% rename from StateMachine/FsmBuilder.cs rename to FluentStateMachine/FsmBuilder.cs index 5c1fd23..d9baa12 100644 --- a/StateMachine/FsmBuilder.cs +++ b/FluentStateMachine/FsmBuilder.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace RandomSolutions +namespace FluentStateMachine { public class FsmBuilder { @@ -13,12 +13,7 @@ public FsmBuilder(FsmModel model) Model = model; } - readonly FsmModel Model; - - public FsmBuilder OnReset(Action> action) - { - return OnReset(x => { action(x); return FrameworkExt.CompletedTask; }); - } + private readonly FsmModel Model; public FsmBuilder OnReset(Func, Task> action) { @@ -26,77 +21,42 @@ public FsmBuilder OnReset(Func, Tas return this; } - public FsmBuilder OnExit(Action> action) - { - return OnExit(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmBuilder OnExit(Func, Task> action) { Model.OnExit = action; return this; } - public FsmBuilder OnEnter(Action> action) - { - return OnEnter(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmBuilder OnEnter(Func, Task> action) { Model.OnEnter = action; return this; } - public FsmBuilder OnJump(Action> action) - { - return OnJump(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmBuilder OnJump(Func, Task> action) { Model.OnJump = action; return this; } - public FsmBuilder OnTrigger(Action> action) - { - return OnTrigger(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmBuilder OnTrigger(Func, Task> action) { Model.OnTrigger = action; return this; } - public FsmBuilder OnFire(Action> action) - { - return OnFire(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmBuilder OnFire(Func, Task> action) { Model.OnFire = action; return this; } - public FsmBuilder OnComplete(Action> action) - { - return OnComplete(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmBuilder OnComplete(Func, Task> action) { Model.OnComplete = action; return this; } - public FsmBuilder OnError(Action> action) - { - return OnError(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmBuilder OnError(Func, Task> action) { Model.OnError = action; @@ -143,7 +103,7 @@ public IStateMachine Build() return new StateMachine(Model); } - const string _startNotContains = "States collection is not contains start point"; + private const string _startNotContains = "States collection is not contains start point"; } } diff --git a/FluentStateMachine/FsmBuilderExtensions.cs b/FluentStateMachine/FsmBuilderExtensions.cs new file mode 100644 index 0000000..29c8e53 --- /dev/null +++ b/FluentStateMachine/FsmBuilderExtensions.cs @@ -0,0 +1,56 @@ +using System; + +namespace FluentStateMachine +{ + public static class FsmBuilderExtensions + { + public static FsmBuilder OnReset(this FsmBuilder builder, + Action> action) + { + return builder.OnReset(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmBuilder OnExit(this FsmBuilder builder, + Action> action) + { + return builder.OnExit(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmBuilder OnEnter(this FsmBuilder builder, + Action> action) + { + return builder.OnEnter(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmBuilder OnJump(this FsmBuilder builder, + Action> action) + { + return builder.OnJump(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmBuilder OnTrigger(this FsmBuilder builder, + Action> action) + { + return builder.OnTrigger(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmBuilder OnFire(this FsmBuilder builder, + Action> action) + { + return builder.OnFire(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmBuilder OnComplete(this FsmBuilder builder, + Action> action) + { + return builder.OnComplete(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmBuilder OnError(this FsmBuilder builder, + Action> action) + { + return builder.OnError(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + } +} diff --git a/StateMachine/FsmConfig.cs b/FluentStateMachine/FsmConfig.cs similarity index 64% rename from StateMachine/FsmConfig.cs rename to FluentStateMachine/FsmConfig.cs index 0397193..49f6fd8 100644 --- a/StateMachine/FsmConfig.cs +++ b/FluentStateMachine/FsmConfig.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace RandomSolutions +namespace FluentStateMachine { public class FsmConfig { @@ -35,33 +35,18 @@ public FsmEventConfig On(TEvent e) }; } - public FsmStateConfig OnEnter(Action> action) - { - return OnEnter(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmStateConfig OnEnter(Func, Task> action) { Model.OnEnter = action; return this; } - public FsmStateConfig OnExit(Action> action) - { - return OnExit(x => { action(x); return FrameworkExt.CompletedTask; }); - } - public FsmStateConfig OnExit(Func, Task> action) { Model.OnExit = action; return this; } - public FsmStateConfig Enable(Func, bool> fn) - { - return Enable(x => Task.FromResult(fn(x))); - } - public FsmStateConfig Enable(Func, Task> fn) { Model.Enable = fn; @@ -77,38 +62,18 @@ public class FsmEventConfig : FsmConfig public FsmEventConfig On(TEvent e) => Parent?.On(e) ?? Root.On(e); - public FsmEventConfig Execute(Func, object> fn) - { - return Execute(x => Task.FromResult(fn(x))); - } - public FsmEventConfig Execute(Func, Task> fn) { Model.Execute = fn; return this; } - public FsmEventConfig Enable(Func, bool> fn) - { - return Enable(x => Task.FromResult(fn(x))); - } - public FsmEventConfig Enable(Func, Task> fn) { Model.Enable = fn; return this; } - public FsmEventConfig JumpTo(TState state) - { - return JumpTo(x => Task.FromResult(state)); - } - - public FsmEventConfig JumpTo(Func, TState> fn) - { - return JumpTo(x => Task.FromResult(fn(x))); - } - public FsmEventConfig JumpTo(Func, Task> fn) { Model.JumpTo = fn; diff --git a/FluentStateMachine/FsmConfigExtensions.cs b/FluentStateMachine/FsmConfigExtensions.cs new file mode 100644 index 0000000..f2f8eb8 --- /dev/null +++ b/FluentStateMachine/FsmConfigExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; + +namespace FluentStateMachine +{ + public static class FsmConfigExtensions + { + public static FsmStateConfig OnEnter(this FsmStateConfig cfg, + Action> action) + { + return cfg.OnEnter(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmStateConfig OnExit(this FsmStateConfig cfg, + Action> action) + { + return cfg.OnExit(x => { action(x); return FrameworkExt.CompletedTask; }); + } + + public static FsmStateConfig Enable(this FsmStateConfig cfg, + Func, bool> fn) + { + return cfg.Enable(x => Task.FromResult(fn(x))); + } + + + + public static FsmEventConfig Execute(this FsmEventConfig cfg, + Func, object> fn) + { + return cfg.Execute(x => Task.FromResult(fn(x))); + } + + public static FsmEventConfig Enable(this FsmEventConfig cfg, + Func, bool> fn) + { + return cfg.Enable(x => Task.FromResult(fn(x))); + } + + public static FsmEventConfig JumpTo(this FsmEventConfig cfg, + TState state) + { + return cfg.JumpTo(x => Task.FromResult(state)); + } + + public static FsmEventConfig JumpTo(this FsmEventConfig cfg, + Func, TState> fn) + { + return cfg.JumpTo(x => Task.FromResult(fn(x))); + } + } +} diff --git a/StateMachine/FsmException.cs b/FluentStateMachine/FsmException.cs similarity index 91% rename from StateMachine/FsmException.cs rename to FluentStateMachine/FsmException.cs index 3d9e970..2fc439d 100644 --- a/StateMachine/FsmException.cs +++ b/FluentStateMachine/FsmException.cs @@ -1,6 +1,6 @@ using System; -namespace RandomSolutions +namespace FluentStateMachine { public class FsmException : Exception { diff --git a/FluentStateMachine/FsmExtensions.cs b/FluentStateMachine/FsmExtensions.cs new file mode 100644 index 0000000..7bba377 --- /dev/null +++ b/FluentStateMachine/FsmExtensions.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace FluentStateMachine +{ + public static class FsmExtensions + { + +#if NETSTANDARD2_1_OR_GREATER + private static async Task> ToListAsync(this IAsyncEnumerable items, CancellationToken cancellationToken = default) + { + var result = new List(); + + await foreach (var item in items.WithCancellation(cancellationToken).ConfigureAwait(false)) + result.Add(item); + + return result; + } + + public static IEnumerable GetStates(this IStateMachine fsm, object data = null) + { + return fsm.GetStatesAsync(data).ToListAsync().Result; + } + + public static IEnumerable GetEvents(this IStateMachine fsm, object data = null) + { + return fsm.GetEventsAsync(data).ToListAsync().Result; + } +#else + public static IEnumerable GetStates(this IStateMachine fsm, object data = null) + { + return fsm.GetStatesAsync(data).Result; + } + + public static IEnumerable GetEvents(this IStateMachine fsm, object data = null) + { + return fsm.GetEventsAsync(data).Result; + } +#endif + + + public static object Trigger(this IStateMachine fsm, + TEvent e, object data = null) + { + return fsm.TriggerAsync(e, data).Result; + } + + public static bool JumpTo(this IStateMachine fsm, + TState state, object data = null) + { + return fsm.JumpToAsync(state, data).Result; + } + + public static void ResetTo(this IStateMachine fsm, TState state) + { + fsm.ResetToAsync(state).Wait(); + } + + public static void Reset(this IStateMachine fsm) + { + fsm.ResetAsync().Wait(); + } + } +} diff --git a/StateMachine/FsmModel.cs b/FluentStateMachine/FsmModel.cs similarity index 98% rename from StateMachine/FsmModel.cs rename to FluentStateMachine/FsmModel.cs index 2373181..de7ed12 100644 --- a/StateMachine/FsmModel.cs +++ b/FluentStateMachine/FsmModel.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace RandomSolutions +namespace FluentStateMachine { public class FsmModel { diff --git a/FluentStateMachine/IStateMachine.cs b/FluentStateMachine/IStateMachine.cs new file mode 100644 index 0000000..42b5136 --- /dev/null +++ b/FluentStateMachine/IStateMachine.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace FluentStateMachine +{ + public interface IStateMachine : IStateMachine + { + TState Current { get; } + + Task TriggerAsync(TEvent e, object data = null, CancellationToken cancellationToken = default); + Task JumpToAsync(TState state, object data = null, CancellationToken cancellationToken = default); + Task ResetToAsync(TState state, CancellationToken cancellationToken = default); + +#if NETSTANDARD2_1_OR_GREATER + IAsyncEnumerable GetStatesAsync(object data = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetEventsAsync(object data = null, CancellationToken cancellationToken = default); +#else + Task> GetStatesAsync(object data = null, CancellationToken cancellationToken = default); + Task> GetEventsAsync(object data = null, CancellationToken cancellationToken = default); +#endif + + } + + public interface IStateMachine + { + Task ResetAsync(CancellationToken cancellationToken = default); + } +} diff --git a/FluentStateMachine/StateMachine.cs b/FluentStateMachine/StateMachine.cs new file mode 100644 index 0000000..4f8ce1f --- /dev/null +++ b/FluentStateMachine/StateMachine.cs @@ -0,0 +1,296 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace FluentStateMachine +{ + public partial class StateMachine : IStateMachine + { + public StateMachine(FsmModel model) + { + _model = model; + Current = model.Start; + } + + private readonly FsmModel _model; + + public TState Current { get; private set; } + +#if NETSTANDARD2_1_OR_GREATER + public async IAsyncEnumerable GetStatesAsync(object data = null, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var args = new FsmEnterArgs + { + Fsm = this, + PrevState = Current, + Data = data, + CancellationToken = cancellationToken, + }; + + foreach (var kvp in _model.States) + if (kvp.Value.Enable == null || await kvp.Value.Enable(args).ConfigureAwait(false)) + yield return kvp.Key; + } + + public async IAsyncEnumerable GetEventsAsync(object data = null, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var stateModel = _model.States[Current]; + + var events = _model.Events + .Where(x => !stateModel.Events.ContainsKey(x.Key)) + .Concat(stateModel.Events); + + var args = new FsmTriggerArgs + { + Fsm = this, + Data = data, + CancellationToken = cancellationToken, + }; + + foreach (var kvp in events) + { + args.Event = kvp.Key; + + if (kvp.Value.Enable == null || await kvp.Value.Enable(args).ConfigureAwait(false)) + yield return kvp.Key; + } + } +#else + public async Task> GetStatesAsync(object data = null, CancellationToken cancellationToken = default) + { + var args = new FsmEnterArgs + { + Fsm = this, + PrevState = Current, + Data = data, + CancellationToken = cancellationToken, + }; + + var stateTasks = _model.States + .Select(x => new { State = x.Key, Task = x.Value.Enable?.Invoke(args) }) + .ToList(); + + await Task.WhenAll(stateTasks.Select(x => x.Task).Where(x => x != null)).ConfigureAwait(false); + + return stateTasks + .Where(x => x.Task?.Result != false) + .Select(x => x.State); + } + + public async Task> GetEventsAsync(object data = null, CancellationToken cancellationToken = default) + { + var stateModel = _model.States[Current]; + + var eventTasks = _model.Events + .Where(x => !stateModel.Events.ContainsKey(x.Key)) + .Concat(stateModel.Events) + .Select(x => new + { + Event = x.Key, + Task = x.Value.Enable?.Invoke(new FsmTriggerArgs + { + Fsm = this, + Event = x.Key, + Data = data, + CancellationToken = cancellationToken, + }) + }) + .ToList(); + + await Task.WhenAll(eventTasks.Select(x => x.Task).Where(x => x != null)).ConfigureAwait(false); + + return eventTasks + .Where(x => x.Task?.Result != false) + .Select(x => x.Event); + } +#endif + + public async Task TriggerAsync(TEvent e, object data = null, CancellationToken cancellationToken = default) + { + var args = new FsmTriggerArgs + { + Fsm = this, + Event = e, + Data = data, + CancellationToken = cancellationToken, + }; + + await OnTrigger(args).ConfigureAwait(false); + + var stateModel = _model.States[Current]; + FsmEventModel eventModel; + + if (!stateModel.Events.TryGetValue(e, out eventModel) && !_model.Events.TryGetValue(e, out eventModel)) + { + await OnError(args, _eventNotFound, e).ConfigureAwait(false); + return null; + } + + if (eventModel.Enable != null && !await eventModel.Enable(args).ConfigureAwait(false)) + { + await OnError(args, _eventDisabled, e).ConfigureAwait(false); + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + + await OnFire(args).ConfigureAwait(false); + + var result = eventModel.Execute == null ? null + : await eventModel.Execute(args).ConfigureAwait(false); + + if (eventModel.JumpTo != null) + { + var next = await eventModel.JumpTo(args).ConfigureAwait(false); + var done = await JumpToAsync(next, data).ConfigureAwait(false); + + if (eventModel.Execute == null) + result = done; + } + + await OnComplete(args, result).ConfigureAwait(false); + + return result; + } + + public async Task JumpToAsync(TState next, object data = null, CancellationToken cancellationToken = default) + { + var enterArgs = new FsmEnterArgs + { + Fsm = this, + PrevState = Current, + Data = data, + CancellationToken = cancellationToken, + }; + + if (!_model.States.ContainsKey(next)) + { + await OnError(enterArgs, _stateNextNotFound, next).ConfigureAwait(false); + return false; + } + + var nextModel = _model.States[next]; + + if (nextModel.Enable != null && !await nextModel.Enable(enterArgs).ConfigureAwait(false)) + { + await OnError(enterArgs, _stateNextDisabled, next).ConfigureAwait(false); + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + + await OnExit(enterArgs, next).ConfigureAwait(false); + + lock (_locker) + Current = next; + + await OnEnter(enterArgs).ConfigureAwait(false); + + await OnJump(enterArgs).ConfigureAwait(false); + + return true; + } + + public Task ResetAsync(CancellationToken cancellationToken = default) => ResetToAsync(_model.Start, cancellationToken); + + public async Task ResetToAsync(TState state, CancellationToken cancellationToken = default) + { + var args = new FsmResetArgs + { + Fsm = this, + PrevState = Current, + CancellationToken = cancellationToken, + }; + + cancellationToken.ThrowIfCancellationRequested(); + + lock (_locker) + Current = state; + + await OnReset(args).ConfigureAwait(false); + } + + + + private Task OnReset(FsmResetArgs args) + { + return _model.OnReset?.Invoke(args) ?? FrameworkExt.CompletedTask; + } + + private Task OnTrigger(FsmTriggerArgs args) + { + return _model.OnTrigger?.Invoke(args) ?? FrameworkExt.CompletedTask; + } + + private Task OnFire(FsmTriggerArgs args) + { + return _model.OnFire?.Invoke(args) ?? FrameworkExt.CompletedTask; + } + + private async Task OnExit(FsmDataArgs args, TState next) + { + var exitArgs = new FsmExitArgs + { + Fsm = this, + Data = args.Data, + CancellationToken = args.CancellationToken, + NextState = next, + }; + + if (_model.OnExit != null) + await _model.OnExit(exitArgs).ConfigureAwait(false); + + if (_model.States[Current].OnExit != null) + await _model.States[Current].OnExit(exitArgs).ConfigureAwait(false); + } + + private async Task OnEnter(FsmEnterArgs args) + { + if (_model.OnEnter != null) + await _model.OnEnter(args).ConfigureAwait(false); + + if (_model.States[Current].OnEnter != null) + await _model.States[Current].OnEnter(args).ConfigureAwait(false); + } + + private Task OnJump(FsmEnterArgs args) + { + return _model.OnJump?.Invoke(args) ?? FrameworkExt.CompletedTask; + } + + private Task OnComplete(FsmTriggerArgs args, object result) + { + return _model.OnComplete?.Invoke(new FsmCompleteArgs + { + Fsm = this, + Event = args.Event, + Data = args.Data, + CancellationToken = args.CancellationToken, + Result = result, + }) ?? FrameworkExt.CompletedTask; + } + + private Task OnError(FsmDataArgs args, string message, params object[] formatArgs) + { + return _model.OnError?.Invoke(new FsmErrorArgs + { + Fsm = this, + Data = args.Data, + CancellationToken = args.CancellationToken, + Message = string.Format(message, formatArgs), + }) ?? FrameworkExt.CompletedTask; + } + + + + private object _locker = new object(); + private const string _stateNextDisabled = "Next state '{0}' disabled"; + private const string _stateNextNotFound = "Next state '{0}' not found"; + private const string _eventDisabled = "Event '{0}' disabled"; + private const string _eventNotFound = "Event '{0}' not found"; + } + +} diff --git a/README.md b/README.md index cd499ea..199f6e8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# StateMachine [![NuGet version](https://badge.fury.io/nu/RandomSolutions.StateMachine.svg)](http://badge.fury.io/nu/RandomSolutions.StateMachine) -Finite-state machine (FSM) pattern implementation +# FluentStateMachine [![NuGet version](https://badge.fury.io/nu/FluentStateMachine.svg)](http://badge.fury.io/nu/FluentStateMachine) +.NET Finite-state machine (FSM) with a fluent interface ## Example ```C# @@ -30,4 +30,3 @@ fsm.Trigger(Event.E3); // Enter to final state ``` -[More details in the test console application...](Test/ConsoleApp/Program.cs) diff --git a/StateMachine/FrameworkExt.cs b/StateMachine/FrameworkExt.cs deleted file mode 100644 index 6ad795d..0000000 --- a/StateMachine/FrameworkExt.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading.Tasks; - -namespace RandomSolutions -{ - static class FrameworkExt - { -#if NET45 - internal static readonly Task CompletedTask = Task.FromResult(false); -#else - internal static readonly Task CompletedTask = Task.CompletedTask; -#endif - } -} diff --git a/StateMachine/IStateMachine.cs b/StateMachine/IStateMachine.cs deleted file mode 100644 index 98473b4..0000000 --- a/StateMachine/IStateMachine.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace RandomSolutions -{ - public interface IStateMachine : IStateMachine - { - TState Current { get; } - - IEnumerable GetStates(params object[] data); - Task> GetStatesAsync(params object[] data); - - IEnumerable GetEvents(params object[] data); - Task> GetEventsAsync(params object[] data); - - object Trigger(TEvent e, params object[] data); - Task TriggerAsync(TEvent e, params object[] data); - - bool JumpTo(TState state, params object[] data); - Task JumpToAsync(TState state, params object[] data); - - void ResetTo(TState state); - Task ResetToAsync(TState state); - } - - public interface IStateMachine - { - void Reset(); - Task ResetAsync(); - } -} diff --git a/StateMachine/StateMachine.cs b/StateMachine/StateMachine.cs deleted file mode 100644 index 4882777..0000000 --- a/StateMachine/StateMachine.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace RandomSolutions -{ - public class StateMachine : IStateMachine - { - public StateMachine(FsmModel model) - { - _model = model; - Current = model.Start; - } - - public TState Current { get; private set; } - - public IEnumerable GetStates(params object[] data) - { - return GetStatesAsync(data).Result; - } - - public async Task> GetStatesAsync(params object[] data) - { - var args = new FsmEnterArgs - { - Fsm = this, - PrevState = Current, - Data = data, - }; - - var stateTasks = _model.States - .Select(x => new { State = x.Key, Task = x.Value.Enable?.Invoke(args) }) - .ToList(); - - await Task.WhenAll(stateTasks.Select(x => x.Task).Where(x => x != null)).ConfigureAwait(false); - - return stateTasks - .Where(x => x.Task?.Result != false) - .Select(x => x.State); - } - - public IEnumerable GetEvents(params object[] data) - { - return GetEventsAsync(data).Result; - } - - public async Task> GetEventsAsync(params object[] data) - { - var stateModel = _model.States[Current]; - - var eventTasks = _model.Events - .Where(x => !stateModel.Events.ContainsKey(x.Key)) - .Concat(stateModel.Events) - .Select(x => new - { - Event = x.Key, - Task = x.Value.Enable?.Invoke(new FsmTriggerArgs - { - Fsm = this, - Event = x.Key, - Data = data, - }) - }) - .ToList(); - - await Task.WhenAll(eventTasks.Select(x => x.Task).Where(x => x != null)).ConfigureAwait(false); - - return eventTasks - .Where(x => x.Task?.Result != false) - .Select(x => x.Event); - } - - public object Trigger(TEvent e, params object[] data) - { - return TriggerAsync(e, data).Result; - } - - public async Task TriggerAsync(TEvent e, params object[] data) - { - var args = new FsmTriggerArgs - { - Fsm = this, - Event = e, - Data = data, - }; - - if (_model.OnTrigger != null) - await _model.OnTrigger(args).ConfigureAwait(false); - - var stateModel = _model.States[Current]; - FsmEventModel eventModel; - - if (!stateModel.Events.TryGetValue(e, out eventModel) && !_model.Events.TryGetValue(e, out eventModel)) - { - await _onError(_getErrorArgs(data, _eventNotFound, e)).ConfigureAwait(false); - return null; - } - - if (eventModel.Enable != null && !await eventModel.Enable(args).ConfigureAwait(false)) - { - await _onError(_getErrorArgs(data, _eventDisabled, e)).ConfigureAwait(false); - return null; - } - - if (_model.OnFire != null) - await _model.OnFire(args).ConfigureAwait(false); - - var result = eventModel.Execute == null ? null - : await eventModel.Execute(args).ConfigureAwait(false); - - if (eventModel.JumpTo != null) - { - var next = await eventModel.JumpTo(args).ConfigureAwait(false); - var done = await JumpToAsync(next, data).ConfigureAwait(false); - - if (eventModel.Execute == null) - result = done; - } - - if (_model.OnComplete != null) - await _model.OnComplete(new FsmCompleteArgs - { - Fsm = this, - Event = e, - Data = data, - Result = result, - }).ConfigureAwait(false); - - return result; - } - public bool JumpTo(TState next, params object[] data) - { - return JumpToAsync(next, data).Result; - } - - public async Task JumpToAsync(TState next, params object[] data) - { - if (!_model.States.ContainsKey(next)) - { - await _onError(_getErrorArgs(data, _stateNextNotFound, next)).ConfigureAwait(false); - return false; - } - - var nextModel = _model.States[next]; - - var enterArgs = new FsmEnterArgs - { - Fsm = this, - PrevState = Current, - Data = data, - }; - - if (nextModel.Enable != null && !await nextModel.Enable(enterArgs).ConfigureAwait(false)) - { - await _onError(_getErrorArgs(data, _stateNextDisabled, next)).ConfigureAwait(false); - return false; - } - - var exitArgs = new FsmExitArgs - { - Fsm = this, - NextState = next, - Data = data, - }; - - if (_model.OnExit != null) - await _model.OnExit(exitArgs).ConfigureAwait(false); - - if (_model.States[Current].OnExit != null) - await _model.States[Current].OnExit(exitArgs).ConfigureAwait(false); - - lock (_locker) - Current = next; - - if (_model.OnEnter != null) - await _model.OnEnter(enterArgs).ConfigureAwait(false); - - if (nextModel.OnEnter != null) - await nextModel.OnEnter(enterArgs).ConfigureAwait(false); - - if (_model.OnJump != null) - await _model.OnJump(enterArgs).ConfigureAwait(false); - - return true; - } - - public void Reset() => ResetTo(_model.Start); - public Task ResetAsync() => ResetToAsync(_model.Start); - - public void ResetTo(TState state) - { - ResetToAsync(state).Wait(); - } - - public async Task ResetToAsync(TState state) - { - var args = new FsmResetArgs - { - Fsm = this, - PrevState = Current, - }; - - lock (_locker) - Current = state; - - if (_model.OnReset != null) - await _model.OnReset(args).ConfigureAwait(false); - } - - async Task _onError(FsmErrorArgs args) - { - if (_model.OnError != null) - await _model.OnError(args).ConfigureAwait(false); - } - - FsmErrorArgs _getErrorArgs(object[] data, string message, params object[] formatArgs) - { - return new FsmErrorArgs - { - Fsm = this, - Data = data, - Message = string.Format(message, formatArgs), - }; - } - - readonly FsmModel _model; - object _locker = new object(); - - const string _stateNextDisabled = "Next state '{0}' disabled"; - const string _stateNextNotFound = "Next state '{0}' not found"; - const string _eventDisabled = "Event '{0}' disabled"; - const string _eventNotFound = "Event '{0}' not found"; - } - -} diff --git a/StateMachine/StateMachine.csproj b/StateMachine/StateMachine.csproj deleted file mode 100644 index 54e898e..0000000 --- a/StateMachine/StateMachine.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - netstandard2.0;net45 - RandomSolutions.StateMachine - RandomSolutions - true - key.snk - 1.1.4 - 1.1.4 - 1.1.4 - - - Leonid Salavatov - Leonid Salavatov 2021 - RandomSolutions.StateMachine - RandomSolutions.StateMachine - RandomSolutions.StateMachine - Finite-state machine (FSM) pattern implementation - statemachine finitestatemachine fsm - true - https://github.com/mustaddon/StateMachine - https://github.com/mustaddon/StateMachine - git - false - MIT - - - - - - Core;STANDARD20 - - - - NET45;NETFULL - - -