From 2fed1cfd0855aaedeea83ba5dd00adfa039504b8 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 30 Dec 2022 18:26:06 +0000 Subject: [PATCH 01/43] Enable Source Link and Determnistic Builds; don't attempt to publish package from forks. --- .../workflows/BuildAndTestOnPullRequests.yml | 16 ++++++++-------- src/Stateless/Stateless.csproj | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/BuildAndTestOnPullRequests.yml b/.github/workflows/BuildAndTestOnPullRequests.yml index 6dc23c47..3e449a5f 100644 --- a/.github/workflows/BuildAndTestOnPullRequests.yml +++ b/.github/workflows/BuildAndTestOnPullRequests.yml @@ -4,35 +4,35 @@ on: push: branches: [ dev, master ] pull_request: - branches: [ dev , master ] + branches: [ dev, master ] jobs: Build_Stateless_solution: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: - dotnet-version: 3.1.101 + dotnet-version: 6.0.x - name: Install dependencies run: dotnet restore - name: Build Stateless solution - run: dotnet build Stateless.sln --configuration Release --no-restore + run: dotnet build Stateless.sln --configuration Release --no-restore - name: Test Stateless run: dotnet test --no-restore --no-build --configuration Release - name: Pack alpha version - if: github.ref == 'refs/heads/dev' && github.event_name == 'push' + if: github.ref == 'refs/heads/dev' && github.event_name == 'push' && github.repository == 'dotnet-state-machine/stateless' run: dotnet pack src\Stateless\Stateless.csproj --version-suffix dev-${{github.run_id}} --configuration Release - name: Publish alpha version - if: github.ref == 'refs/heads/dev' && github.event_name == 'push' + if: github.ref == 'refs/heads/dev' && github.event_name == 'push' && github.repository == 'dotnet-state-machine/stateless' run: dotnet nuget push src\Stateless\bin\Release\*.nupkg -s nuget.org --api-key ${{ secrets.NUGETAPIKEY }} - name: Pack Release version - if: github.ref == 'refs/heads/master' && github.event_name == 'push' + if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'dotnet-state-machine/stateless' run: dotnet pack src\Stateless\Stateless.csproj --configuration Release - name: Publish Release version - if: github.ref == 'refs/heads/master' && github.event_name == 'push' + if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'dotnet-state-machine/stateless' run: dotnet nuget push src\Stateless\bin\Release\*.nupkg -s nuget.org --api-key ${{ secrets.NUGETAPIKEY }} diff --git a/src/Stateless/Stateless.csproj b/src/Stateless/Stateless.csproj index da712780..7205a4ef 100644 --- a/src/Stateless/Stateless.csproj +++ b/src/Stateless/Stateless.csproj @@ -6,7 +6,7 @@ Stateless netstandard2.0;netstandard1.0;net45;net40;net472;net5.0;net6.0 Create state machines and lightweight state machine-based workflows directly in .NET code - Copyright © Stateless Contributors 2009-2019 + Copyright © Stateless Contributors 2009-$([System.DateTime]::Now.ToString(yyyy)) en-US 5.13.0 Stateless Contributors @@ -21,6 +21,13 @@ false + + true + true + true + snupkg + + $(DefineConstants);PORTABLE_REFLECTION;TASKS @@ -29,7 +36,15 @@ $(DefineConstants);TASKS + + true + + + + + + From a436129e292d50a4f01958511266a35605084e89 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 30 Dec 2022 21:03:41 +0000 Subject: [PATCH 02/43] Remove unsupported input. --- .github/workflows/BuildAndTestOnPullRequests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/BuildAndTestOnPullRequests.yml b/.github/workflows/BuildAndTestOnPullRequests.yml index 3e449a5f..5f843ed1 100644 --- a/.github/workflows/BuildAndTestOnPullRequests.yml +++ b/.github/workflows/BuildAndTestOnPullRequests.yml @@ -11,8 +11,6 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v3 - with: - dotnet-version: 6.0.x - name: Install dependencies run: dotnet restore From 0883481d9f6f48e2678bf6c34770fd78a5121cc0 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 30 Dec 2022 21:53:51 +0000 Subject: [PATCH 03/43] Create SECURITY.md --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..984203e2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in Stateles, please report it via the [Security Advisories](https://github.com/dotnet-state-machine/stateless/security/advisories) page. Creating a security advistory will notify the project owners and allow them to assess it and take appropriate action to resolve it. From 159b88f084cff08c167d3aa86f1033a6619fce43 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Sat, 31 Dec 2022 16:07:56 +0000 Subject: [PATCH 04/43] Create CONTRIBUTING.md --- CONTRIBUTING.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..18db56c8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing to Stateless + +If you're reading this page, thank you for considering making a contribution to Stateless! This project depends on the work of the community. As maintainers, we'll try our best to make bug fixes, review PRs, and respond to issues and feature requests. + +## Getting Started + +If you've found a bug, need a new feature or want to suggest a change, be sure to take a look through the [issues](https://github.com/dotnet-state-machine/stateless/issues?q=is%3Aissue) and [pull requests](https://github.com/dotnet-state-machine/stateless/pulls) in case it's been discussed before or is already in progress. + +If you've found a security vulnerability, please report it using the [Security Advisories](https://github.com/dotnet-state-machine/stateless/security/advisories) page. + +For anyone new to contributing to open source, there are some great guides to help you get started, such as [this one by freeCodeCamp.org](https://github.com/freeCodeCamp/how-to-contribute-to-open-source) and [this one from Open Source Guides](https://opensource.guide/how-to-contribute/). + +## General Guidance + +It's best to start by discussing a proposed change in an [issue](https://github.com/dotnet-state-machine/stateless/issues), be it a new issue you've created or an existing issue you're willing to help with. Let others know you're working on it. + +Check that your [fork is synced with the upsream repo](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) before you start working on a change, then create a branch from it and commit your changes to that branch. When you're ready for the change to be reviewed, create a pull request to merge your change back to the upstream `dotnet-state-machine:dev` branch. + +Please make sure your change meets the following criteria before raising a pull request: + +* Keep the change relevant to the specific issue being addressed; making changes that haven't been discussed could have unintended side effects. +* Add unit test coverage for your work; this not only helps to validate that your change works as described, it also acts as documentation and helps to defend against future regressions. +* Update the documentation! Help others benefit from your work by including guidance in the README. +* Be open and encouraging to constructive feedback; reviewers may ask for further changes or may want to discuss alternative approaches; the project will benefit from your patience and your willingness to engage with reviewers. + +## Other Ways to Contribute + +* Participate in open discussions, for instance by helping to answer questions or offering guidance to others. +* Improve the documentation, even something as small as fixing a typo or including a code snippet in the README; it all helps. +* Review a pull request; take a look through the [open pull requests](https://github.com/dotnet-state-machine/stateless/pulls) and offer constructive feedback. +* Boost the project; star the repository, mention it on social media, or link to it in your project's README. From cbdeef940dec0ba37aeb677b0ff96ee493dde1f4 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Sat, 31 Dec 2022 16:18:47 +0000 Subject: [PATCH 05/43] Link to CONTRIBUTING.md in README. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 1a99a811..9c2e3ef4 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,11 @@ await stateMachine.FireAsync(Trigger.Assigned); Stateless runs on .NET 4.0+ and practically all modern .NET platforms by targeting .NET Standard 1.0 and .NET Standard2.0. Visual Studio 2017 or later is required to build the solution. +## Contributing + +We welcome contributions to this project. Check [CONTRIBUTING.md](CONTRIBUTING.md) for more info. + + ## Project Goals This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal. From b6a77a4fbd79d16fd082781183cd803d3048de65 Mon Sep 17 00:00:00 2001 From: LipatovAlexander Date: Tue, 11 Apr 2023 13:08:01 +0300 Subject: [PATCH 06/43] fix: InternalTransitionAsyncIf guard parameters --- src/Stateless/InternalTriggerBehaviour.cs | 2 +- src/Stateless/StateConfiguration.Async.cs | 57 +++-------- .../InternalTransitionAsyncFixture.cs | 95 ++++++++++++++++++- 3 files changed, 110 insertions(+), 44 deletions(-) diff --git a/src/Stateless/InternalTriggerBehaviour.cs b/src/Stateless/InternalTriggerBehaviour.cs index fe6dd927..8885bd91 100644 --- a/src/Stateless/InternalTriggerBehaviour.cs +++ b/src/Stateless/InternalTriggerBehaviour.cs @@ -45,7 +45,7 @@ public class Async : InternalTriggerBehaviour { readonly Func InternalAction; - public Async(TTrigger trigger, Func guard,Func internalAction, string guardDescription = null) : base(trigger, new TransitionGuard(guard, guardDescription)) + public Async(TTrigger trigger, Func guard,Func internalAction, string guardDescription = null) : base(trigger, new TransitionGuard(guard, guardDescription)) { InternalAction = internalAction; } diff --git a/src/Stateless/StateConfiguration.Async.cs b/src/Stateless/StateConfiguration.Async.cs index 6a89fbc3..93fac07b 100644 --- a/src/Stateless/StateConfiguration.Async.cs +++ b/src/Stateless/StateConfiguration.Async.cs @@ -9,21 +9,6 @@ public partial class StateMachine { public partial class StateConfiguration { - /// - /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine - /// - /// - /// Function that must return true in order for the trigger to be accepted. - /// - /// - public StateConfiguration InternalTransitionAsyncIf(TTrigger trigger, Func guard, Func entryAction) - { - if (entryAction == null) throw new ArgumentNullException(nameof(entryAction)); - - _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger, guard, (t, args) => entryAction(t))); - return this; - } - /// /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine /// @@ -35,23 +20,22 @@ public StateConfiguration InternalTransitionAsyncIf(TTrigger trigger, Func { if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); - _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger, guard, (t, args) => internalAction())); + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger, t => guard(), (t, args) => internalAction())); return this; } /// /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine /// - /// /// The accepted trigger /// Function that must return true in order for the trigger to be accepted. /// The asynchronous action performed by the internal transition /// - public StateConfiguration InternalTransitionAsyncIf(TTrigger trigger, Func guard, Func internalAction) + public StateConfiguration InternalTransitionAsyncIf(TTrigger trigger, Func guard, Func internalAction) { if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); - _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger, guard, (t, args) => internalAction(t))); + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger, t => guard(), (t, args) => internalAction(t))); return this; } @@ -63,12 +47,12 @@ public StateConfiguration InternalTransitionAsyncIf(TTrigger trigger, Fun /// Function that must return true in order for the trigger to be accepted. /// The asynchronous action performed by the internal transition /// - public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) + public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) { if (trigger == null) throw new ArgumentNullException(nameof(trigger)); if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); - _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, guard, (t, args) => internalAction(ParameterConversion.Unpack(args, 0), t))); + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, TransitionGuard.ToPackedGuard(guard), (t, args) => internalAction(ParameterConversion.Unpack(args, 0), t))); return this; } @@ -81,12 +65,12 @@ public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters /// Function that must return true in order for the trigger to be accepted. /// The asynchronous action performed by the internal transition /// - public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) + public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) { if (trigger == null) throw new ArgumentNullException(nameof(trigger)); if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); - _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, guard, (t, args) => internalAction( + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, TransitionGuard.ToPackedGuard(guard), (t, args) => internalAction( ParameterConversion.Unpack(args, 0), ParameterConversion.Unpack(args, 1), t))); return this; @@ -102,29 +86,19 @@ public StateConfiguration InternalTransitionAsyncIf(TriggerWithPar /// Function that must return true in order for the trigger to be accepted. /// The asynchronous action performed by the internal transition /// - public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) + public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) { if (trigger == null) throw new ArgumentNullException(nameof(trigger)); if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); - _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, guard, (t, args) => internalAction( + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, TransitionGuard.ToPackedGuard(guard), (t, args) => internalAction( ParameterConversion.Unpack(args, 0), ParameterConversion.Unpack(args, 1), ParameterConversion.Unpack(args, 2), t))); return this; } - - /// - /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine - /// - /// - /// - /// - public StateConfiguration InternalTransitionAsync(TTrigger trigger, Func entryAction) - { - return InternalTransitionAsyncIf(trigger, () => true, entryAction); - } + /// /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine /// @@ -138,11 +112,10 @@ public StateConfiguration InternalTransitionAsync(TTrigger trigger, Func i /// /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine /// - /// /// The accepted trigger /// The asynchronous action performed by the internal transition /// - public StateConfiguration InternalTransitionAsync(TTrigger trigger, Func internalAction) + public StateConfiguration InternalTransitionAsync(TTrigger trigger, Func internalAction) { return InternalTransitionAsyncIf(trigger, () => true, internalAction); } @@ -155,7 +128,7 @@ public StateConfiguration InternalTransitionAsync(TTrigger trigger, Func< /// public StateConfiguration InternalTransitionAsync(TriggerWithParameters trigger, Func internalAction) { - return InternalTransitionAsyncIf(trigger, () => true, internalAction); + return InternalTransitionAsyncIf(trigger, t => true, internalAction); } /// @@ -168,7 +141,7 @@ public StateConfiguration InternalTransitionAsync(TriggerWithParameters public StateConfiguration InternalTransitionAsync(TriggerWithParameters trigger, Func internalAction) { - return InternalTransitionAsyncIf(trigger, () => true, internalAction); + return InternalTransitionAsyncIf(trigger, (t1, t2) => true, internalAction); } /// @@ -182,7 +155,7 @@ public StateConfiguration InternalTransitionAsync(TriggerWithParam /// public StateConfiguration InternalTransitionAsync(TriggerWithParameters trigger, Func internalAction) { - return InternalTransitionAsyncIf(trigger, () => true, internalAction); + return InternalTransitionAsyncIf(trigger, (t1, t2, t3) => true, internalAction); } /// @@ -462,4 +435,4 @@ public StateConfiguration OnExitAsync(Func exitAction, string } } } -#endif +#endif \ No newline at end of file diff --git a/test/Stateless.Tests/InternalTransitionAsyncFixture.cs b/test/Stateless.Tests/InternalTransitionAsyncFixture.cs index 0078a929..df2092a7 100644 --- a/test/Stateless.Tests/InternalTransitionAsyncFixture.cs +++ b/test/Stateless.Tests/InternalTransitionAsyncFixture.cs @@ -5,6 +5,99 @@ namespace Stateless.Tests { public class InternalTransitionAsyncFixture { + [Fact] + public async Task InternalTransitionAsyncIf_AllowGuardWithParameter() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + const int intParam = 5; + var guardInvoked = false; + var callbackInvoked = false; + + sm.Configure(State.A) + .InternalTransitionAsyncIf(trigger, i => + { + guardInvoked = true; + Assert.Equal(intParam, i); + return true; + }, (i, transition) => + { + callbackInvoked = true; + Assert.Equal(intParam, i); + return Task.CompletedTask; + }); + + await sm.FireAsync(trigger, intParam); + + Assert.True(guardInvoked); + Assert.True(callbackInvoked); + } + + [Fact] + public async Task InternalTransitionAsyncIf_AllowGuardWithTwoParameters() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + const int intParam = 5; + const string stringParam = "5"; + var guardInvoked = false; + var callbackInvoked = false; + + sm.Configure(State.A) + .InternalTransitionAsyncIf(trigger, (i, s) => + { + guardInvoked = true; + Assert.Equal(intParam, i); + Assert.Equal(stringParam, s); + return true; + }, (i, s, transition) => + { + callbackInvoked = true; + Assert.Equal(intParam, i); + Assert.Equal(stringParam, s); + return Task.CompletedTask; + }); + + await sm.FireAsync(trigger, intParam, stringParam); + + Assert.True(guardInvoked); + Assert.True(callbackInvoked); + } + + [Fact] + public async Task InternalTransitionAsyncIf_AllowGuardWithThreeParameters() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + const int intParam = 5; + const string stringParam = "5"; + const bool boolParam = true; + var guardInvoked = false; + var callbackInvoked = false; + + sm.Configure(State.A) + .InternalTransitionAsyncIf(trigger, (i, s, b) => + { + guardInvoked = true; + Assert.Equal(intParam, i); + Assert.Equal(stringParam, s); + Assert.Equal(boolParam, b); + return true; + }, (i, s, b, transition) => + { + callbackInvoked = true; + Assert.Equal(intParam, i); + Assert.Equal(stringParam, s); + Assert.Equal(boolParam, b); + return Task.CompletedTask; + }); + + await sm.FireAsync(trigger, intParam, stringParam, boolParam); + + Assert.True(guardInvoked); + Assert.True(callbackInvoked); + } + /// /// This unit test demonstrated bug report #417 /// @@ -48,4 +141,4 @@ private class Order public PaymentStatus PaymentStatus { get; internal set; } } } -} +} \ No newline at end of file From f755ddfcc32072447752e4f0cb5848802bc7fd11 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 11 Apr 2023 12:20:13 +0100 Subject: [PATCH 07/43] Represent async "entry actions from trigger" in the same way as the sync version. Fix bug where trigger information is missing from the StateMachine.GetInfo. --- src/Stateless/EntryActionBehaviour.cs | 25 ++++++++++++- src/Stateless/Reflection/ActionInfo.cs | 13 ++++--- src/Stateless/StateRepresentation.Async.cs | 12 +++---- test/Stateless.Tests/GetInfoFixture.cs | 41 ++++++++++++++++++++++ 4 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 test/Stateless.Tests/GetInfoFixture.cs diff --git a/src/Stateless/EntryActionBehaviour.cs b/src/Stateless/EntryActionBehaviour.cs index 2289fc99..280452ef 100644 --- a/src/Stateless/EntryActionBehaviour.cs +++ b/src/Stateless/EntryActionBehaviour.cs @@ -40,7 +40,7 @@ public override Task ExecuteAsync(Transition transition, object[] args) public class SyncFrom : Sync { - internal TTriggerType Trigger { get; private set; } + internal TTriggerType Trigger { get; } public SyncFrom(TTriggerType trigger, Action action, Reflection.InvocationInfo description) : base(action, description) @@ -82,6 +82,29 @@ public override Task ExecuteAsync(Transition transition, object[] args) return _action(transition, args); } } + + public class AsyncFrom : Async + { + internal TTriggerType Trigger { get; } + + public AsyncFrom(TTriggerType trigger, Func action, Reflection.InvocationInfo description) + : base(action, description) + { + Trigger = trigger; + } + + public override void Execute(Transition transition, object[] args) + { + if (transition.Trigger.Equals(Trigger)) + base.Execute(transition, args); + } + + public override Task ExecuteAsync(Transition transition, object[] args) + { + Execute(transition, args); + return TaskResult.Done; + } + } } } } diff --git a/src/Stateless/Reflection/ActionInfo.cs b/src/Stateless/Reflection/ActionInfo.cs index 7f05d5c9..26427880 100644 --- a/src/Stateless/Reflection/ActionInfo.cs +++ b/src/Stateless/Reflection/ActionInfo.cs @@ -18,13 +18,16 @@ public class ActionInfo internal static ActionInfo Create(StateMachine.EntryActionBehavior entryAction) { StateMachine.EntryActionBehavior.SyncFrom syncFrom = entryAction as StateMachine.EntryActionBehavior.SyncFrom; - if (syncFrom != null) return new ActionInfo(entryAction.Description, syncFrom.Trigger.ToString()); - else - return new ActionInfo(entryAction.Description, null); + + StateMachine.EntryActionBehavior.AsyncFrom asyncFrom = entryAction as StateMachine.EntryActionBehavior.AsyncFrom; + if (asyncFrom != null) + return new ActionInfo(entryAction.Description, asyncFrom.Trigger.ToString()); + + return new ActionInfo(entryAction.Description, null); } - + /// /// Constructor /// @@ -34,4 +37,4 @@ public ActionInfo(InvocationInfo method, string fromTrigger) FromTrigger = fromTrigger; } } -} +} \ No newline at end of file diff --git a/src/Stateless/StateRepresentation.Async.cs b/src/Stateless/StateRepresentation.Async.cs index 8041e154..6a48ca9f 100644 --- a/src/Stateless/StateRepresentation.Async.cs +++ b/src/Stateless/StateRepresentation.Async.cs @@ -24,14 +24,10 @@ public void AddEntryAction(TTrigger trigger, Func ac if (action == null) throw new ArgumentNullException(nameof(action)); EntryActions.Add( - new EntryActionBehavior.Async((t, args) => - { - if (t.Trigger.Equals(trigger)) - return action(t, args); - - return TaskResult.Done; - }, - entryActionDescription)); + new EntryActionBehavior.AsyncFrom( + trigger, + action, + entryActionDescription)); } public void AddEntryAction(Func action, Reflection.InvocationInfo entryActionDescription) diff --git a/test/Stateless.Tests/GetInfoFixture.cs b/test/Stateless.Tests/GetInfoFixture.cs new file mode 100644 index 00000000..d8a77825 --- /dev/null +++ b/test/Stateless.Tests/GetInfoFixture.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Stateless.Tests; + +public class GetInfoFixture +{ + [Fact] + public void GetInfo_should_return_Entry_action_with_trigger_name() + { + // ARRANGE + var sm = new StateMachine(State.A); + sm.Configure(State.B) + .OnEntryFrom(Trigger.X, () => { }); + + // ACT + var stateMachineInfo = sm.GetInfo(); + + // ASSERT + var stateInfo = Assert.Single(stateMachineInfo.States); + var entryActionInfo = Assert.Single(stateInfo.EntryActions); + Assert.Equal(Trigger.X.ToString(), entryActionInfo.FromTrigger); + } + + [Fact] + public void GetInfo_should_return_async_Entry_action_with_trigger_name() + { + // ARRANGE + var sm = new StateMachine(State.A); + sm.Configure(State.B) + .OnEntryFromAsync(Trigger.X, () => Task.CompletedTask); + + // ACT + var stateMachineInfo = sm.GetInfo(); + + // ASSERT + var stateInfo = Assert.Single(stateMachineInfo.States); + var entryActionInfo = Assert.Single(stateInfo.EntryActions); + Assert.Equal(Trigger.X.ToString(), entryActionInfo.FromTrigger); + } +} \ No newline at end of file From 46d3883498855b0460b7d2cd867afe3f599a1dc9 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 11 Apr 2023 16:25:36 +0100 Subject: [PATCH 08/43] Fix spelling mistake "fireing" to "firing" --- src/Stateless/StateMachine.Async.cs | 2 +- src/Stateless/StateMachine.cs | 6 +++--- ...ingModesFixture.cs => AsyncFiringModesFixture.cs} | 12 ++++++------ ...{FireingModesFixture.cs => FiringModesFixture.cs} | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) rename test/Stateless.Tests/{AsyncFireingModesFixture.cs => AsyncFiringModesFixture.cs} (93%) rename test/Stateless.Tests/{FireingModesFixture.cs => FiringModesFixture.cs} (90%) diff --git a/src/Stateless/StateMachine.Async.cs b/src/Stateless/StateMachine.Async.cs index f46a4825..4d8b0151 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -249,7 +249,7 @@ private async Task HandleTransitioningTriggerAsync(object[] args, StateRepresent await _onTransitionedEvent.InvokeAsync(transition); var representation =await EnterStateAsync(newRepresentation, transition, args); - // Check if state has changed by entering new state (by fireing triggers in OnEntry or such) + // Check if state has changed by entering new state (by firing triggers in OnEntry or such) if (!representation.UnderlyingState.Equals(State)) { // The state has been changed after entering the state, must update current state to new one diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 2e374bb5..57ef4c50 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -64,7 +64,7 @@ public StateMachine(TState initialState) : this(initialState, FiringMode.Queued) /// /// A function that will be called to read the current state value. /// An action that will be called to write new state values. - /// Optional specification of fireing mode. + /// Optional specification of firing mode. public StateMachine(Func stateAccessor, Action stateMutator, FiringMode firingMode) : this() { _stateAccessor = stateAccessor ?? throw new ArgumentNullException(nameof(stateAccessor)); @@ -78,7 +78,7 @@ public StateMachine(Func stateAccessor, Action stateMutator, Fir /// Construct a state machine. /// /// The initial state. - /// Optional specification of fireing mode. + /// Optional specification of firing mode. public StateMachine(TState initialState, FiringMode firingMode) : this() { var reference = new StateReference { State = initialState }; @@ -476,7 +476,7 @@ private void HandleTransitioningTrigger( object[] args, StateRepresentation repr _onTransitionedEvent.Invoke(transition); var representation = EnterState(newRepresentation, transition, args); - // Check if state has changed by entering new state (by fireing triggers in OnEntry or such) + // Check if state has changed by entering new state (by firing triggers in OnEntry or such) if (!representation.UnderlyingState.Equals(State)) { // The state has been changed after entering the state, must update current state to new one diff --git a/test/Stateless.Tests/AsyncFireingModesFixture.cs b/test/Stateless.Tests/AsyncFiringModesFixture.cs similarity index 93% rename from test/Stateless.Tests/AsyncFireingModesFixture.cs rename to test/Stateless.Tests/AsyncFiringModesFixture.cs index 5f7b6484..5802abe8 100644 --- a/test/Stateless.Tests/AsyncFireingModesFixture.cs +++ b/test/Stateless.Tests/AsyncFiringModesFixture.cs @@ -9,10 +9,10 @@ namespace Stateless.Tests /// /// This test class verifies that the firing modes are working as expected /// - public class AsyncFireingModesFixture + public class AsyncFiringModesFixture { /// - /// Check that the immediate fireing modes executes entry/exit out of order. + /// Check that the immediate Firing modes executes entry/exit out of order. /// [Fact] public void ImmediateEntryAProcessedBeforeEnterB() @@ -46,7 +46,7 @@ public void ImmediateEntryAProcessedBeforeEnterB() } /// - /// Checks that queued fireing mode executes triggers in order + /// Checks that queued Firing mode executes triggers in order /// [Fact] public void ImmediateEntryAProcessedBeforeEterB() @@ -79,10 +79,10 @@ public void ImmediateEntryAProcessedBeforeEterB() } /// - /// Check that the immediate fireing modes executes entry/exit out of order. + /// Check that the immediate Firing modes executes entry/exit out of order. /// [Fact] - public void ImmediateFireingOnEntryEndsUpInCorrectState() + public void ImmediateFiringOnEntryEndsUpInCorrectState() { var record = new List(); var sm = new StateMachine(State.A, FiringMode.Immediate); @@ -119,7 +119,7 @@ public void ImmediateFireingOnEntryEndsUpInCorrectState() } /// - /// Check that the immediate fireing modes executes entry/exit out of order. + /// Check that the immediate Firing modes executes entry/exit out of order. /// [Fact] public async Task ImmediateModeTransitionsAreInCorrectOrderWithAsyncDriving() diff --git a/test/Stateless.Tests/FireingModesFixture.cs b/test/Stateless.Tests/FiringModesFixture.cs similarity index 90% rename from test/Stateless.Tests/FireingModesFixture.cs rename to test/Stateless.Tests/FiringModesFixture.cs index 4afedd98..f3437274 100644 --- a/test/Stateless.Tests/FireingModesFixture.cs +++ b/test/Stateless.Tests/FiringModesFixture.cs @@ -7,10 +7,10 @@ namespace Stateless.Tests /// /// This test class verifies that the firing modes are working as expected /// - public class FireingModesFixture + public class FiringModesFixture { /// - /// Check that the immediate fireing modes executes entry/exit out of order. + /// Check that the immediate firing modes executes entry/exit out of order. /// [Fact] public void ImmediateEntryAProcessedBeforeEnterB() @@ -43,10 +43,10 @@ public void ImmediateEntryAProcessedBeforeEnterB() } /// - /// Checks that queued fireing mode executes triggers in order + /// Checks that queued firing mode executes triggers in order /// [Fact] - public void ImmediateEntryAProcessedBeforeEterB() + public void QueuedEntryAProcessedAfterEnterB() { var record = new List(); var sm = new StateMachine(State.A, FiringMode.Queued); @@ -76,10 +76,10 @@ public void ImmediateEntryAProcessedBeforeEterB() } /// - /// Check that the immediate fireing modes executes entry/exit out of order. + /// Check that the immediate firing modes executes entry/exit out of order. /// [Fact] - public void ImmediateFireingOnEntryEndsUpInCorrectState() + public void ImmediateFiringOnEntryEndsUpInCorrectState() { var record = new List(); var sm = new StateMachine(State.A, FiringMode.Immediate); From a74dc78c34e0a2151db1844bd62d92148e717812 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Wed, 12 Apr 2023 10:44:41 +0100 Subject: [PATCH 09/43] For StateInfo, Substates, FixedTransitions and DynamicTransitions are only initialised by a call to AddRelationships. However, for StateMachineInfo.InitialState, this never happens. Therefore StateMachineInfo.InitialState.Transitions throws a System.ArgumentNullException. The StateInfo object should not throw if "half initialised" --- src/Stateless/Reflection/StateInfo.cs | 9 ++++++++- test/Stateless.Tests/StateInfoTests.cs | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 test/Stateless.Tests/StateInfoTests.cs diff --git a/src/Stateless/Reflection/StateInfo.cs b/src/Stateless/Reflection/StateInfo.cs index d18cab1e..e98e45bb 100644 --- a/src/Stateless/Reflection/StateInfo.cs +++ b/src/Stateless/Reflection/StateInfo.cs @@ -143,7 +143,14 @@ private void AddRelationships( /// /// Transitions defined for this state. /// - public IEnumerable Transitions { get { return FixedTransitions.Concat(DynamicTransitions); } } + public IEnumerable Transitions + { + get { + return FixedTransitions == null // A quick way to check if AddRelationships has been called. + ? null + : FixedTransitions.Concat(DynamicTransitions); + } + } /// /// Transitions defined for this state. diff --git a/test/Stateless.Tests/StateInfoTests.cs b/test/Stateless.Tests/StateInfoTests.cs new file mode 100644 index 00000000..edd8f33e --- /dev/null +++ b/test/Stateless.Tests/StateInfoTests.cs @@ -0,0 +1,25 @@ +using Stateless.Reflection; +using Xunit; + +namespace Stateless.Tests; + +public class StateInfoTests +{ + /// + /// For StateInfo, Substates, FixedTransitions and DynamicTransitions are only initialised by a call to AddRelationships. + /// However, for StateMachineInfo.InitialState, this never happens. Therefore StateMachineInfo.InitialState.Transitions + /// throws a System.ArgumentNullException. + /// + [Fact] + public void StateInfo_transitions_should_default_to_empty() + { + // ARRANGE + var stateInfo = StateInfo.CreateStateInfo(new StateMachine.StateRepresentation(State.A)); + + // ACT + var stateInfoTransitions = stateInfo.Transitions; + + // ASSERT + Assert.Null(stateInfoTransitions); + } +} \ No newline at end of file From 1cbd658c42be5c41c9b56948de3b46692abaee3f Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Fri, 14 Apr 2023 14:48:06 +0100 Subject: [PATCH 10/43] Restore the newline at the end of ActionInfo.cs --- src/Stateless/Reflection/ActionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stateless/Reflection/ActionInfo.cs b/src/Stateless/Reflection/ActionInfo.cs index 26427880..cf0391a9 100644 --- a/src/Stateless/Reflection/ActionInfo.cs +++ b/src/Stateless/Reflection/ActionInfo.cs @@ -37,4 +37,4 @@ public ActionInfo(InvocationInfo method, string fromTrigger) FromTrigger = fromTrigger; } } -} \ No newline at end of file +} From 8c03b2bfddc726aff82d7b6b1158217e219303f9 Mon Sep 17 00:00:00 2001 From: celloza Date: Sat, 15 Apr 2023 00:09:46 +0100 Subject: [PATCH 11/43] Implemented an example for an alarm. --- Stateless.sln | 11 +- example/AlarmExample/Alarm.cs | 175 +++++++++++++++++++++++ example/AlarmExample/AlarmCommand.cs | 13 ++ example/AlarmExample/AlarmExample.csproj | 14 ++ example/AlarmExample/AlarmState.cs | 14 ++ example/AlarmExample/Program.cs | 115 +++++++++++++++ example/AlarmExample/StateDiagram.png | Bin 0 -> 142822 bytes 7 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 example/AlarmExample/Alarm.cs create mode 100644 example/AlarmExample/AlarmCommand.cs create mode 100644 example/AlarmExample/AlarmExample.csproj create mode 100644 example/AlarmExample/AlarmState.cs create mode 100644 example/AlarmExample/Program.cs create mode 100644 example/AlarmExample/StateDiagram.png diff --git a/Stateless.sln b/Stateless.sln index be9741b1..00115b05 100644 --- a/Stateless.sln +++ b/Stateless.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29505.145 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{8DE7A8AE-D87D-46A0-9757-88BA4AF7EDA5}" ProjectSection(SolutionItems) = preProject @@ -36,6 +36,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelephoneCallExample", "exa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonExample", "example\JsonExample\JsonExample.csproj", "{809A7873-DD78-4D5D-A432-9718C929BECA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlarmExample", "example\AlarmExample\AlarmExample.csproj", "{4E44B325-F791-4C24-872B-D1454DBBA30D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,6 +68,10 @@ Global {809A7873-DD78-4D5D-A432-9718C929BECA}.Debug|Any CPU.Build.0 = Debug|Any CPU {809A7873-DD78-4D5D-A432-9718C929BECA}.Release|Any CPU.ActiveCfg = Release|Any CPU {809A7873-DD78-4D5D-A432-9718C929BECA}.Release|Any CPU.Build.0 = Release|Any CPU + {4E44B325-F791-4C24-872B-D1454DBBA30D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E44B325-F791-4C24-872B-D1454DBBA30D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E44B325-F791-4C24-872B-D1454DBBA30D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E44B325-F791-4C24-872B-D1454DBBA30D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -77,6 +83,7 @@ Global {19ABDDFE-C040-404E-897B-37BE6C248ED7} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0} {5182CA95-8E6F-4D16-9790-8F7D1C5A9C87} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0} {809A7873-DD78-4D5D-A432-9718C929BECA} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0} + {4E44B325-F791-4C24-872B-D1454DBBA30D} = {45C09CCA-6C76-4E10-B386-5D95A7610FE0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A73ADDC-8150-4AFC-AAF0-BA8B4D7A94D7} diff --git a/example/AlarmExample/Alarm.cs b/example/AlarmExample/Alarm.cs new file mode 100644 index 00000000..063710cb --- /dev/null +++ b/example/AlarmExample/Alarm.cs @@ -0,0 +1,175 @@ +using AlarmExample; +using Stateless; +using System.Diagnostics; + +namespace AlarmExample +{ + /// + /// A sample class that implements an alarm as a state machine using Stateless + /// (https://github.com/dotnet-state-machine/stateless). + /// + /// It also shows one way that temporary states can be implemented with the use of + /// Timers. PreArmed, PreTriggered, Triggered, and ArmPaused are "temporary" states with + /// a configurable delay (i.e. to allow for an "arm delay"... a delay between Disarmed + /// and Armed). The Triggered state is also considered temporary, since if an alarm + /// sounds for a certain period of time and no-one Acknowledges it, the state machine + /// returns to the Armed state. + /// + /// Timers are triggered via OnEntry() and OnExit() methods. Transitions are written to + /// the Trace in order to show what happens. + /// + /// The included PNG file shows what the state flow looks like. + /// + /// + public partial class Alarm + { + /// + /// Moves the Alarm into the provided via the defined . + /// + /// The to execute on the current . + /// The new . + public AlarmState ExecuteTransition(AlarmCommand command) + { + if (_machine.CanFire(command)) + { + _machine.Fire(command); + } + else + { + throw new InvalidOperationException($"Cannot transition from {CurrentState} via {command}"); + } + + return CurrentState(); + } + + /// + /// The current of the alarm. + /// + public AlarmState CurrentState() + { + if (_machine != null) + return _machine.State; + else + throw new InvalidOperationException("Alarm hasn't been configured yet."); + } + + /// + /// Defines whether the has been configured. + /// + public bool IsConfigured { get; private set; } + + /// + /// Returns whether the provided command is a valid transition from the Current State. + /// + /// + /// + public bool CanFireCommand(AlarmCommand command) + { + return _machine.CanFire(command); + } + + /// + /// Default constructor. + /// + /// The time (in seconds) the alarm will spend in the + /// Prearmed status before continuing to the Armed status (if not transitioned to + /// Disarmed via Disarm). + /// The time (in seconds) the alarm will spend in the + /// ArmPaused status before returning to Armed (if not transitioned to Triggered + /// via Trigger). + /// The time (in seconds) the alarm will spend in the + /// PreTriggered status before continuing to the Triggered status (if not + /// transitioned to Disarmed via Disarm). + /// The time (in seconds) the alarm will spend in the + /// Triggered status before returning to the Armed status (if not transitioned to + /// Disarmed via Disarm). + public Alarm(int armDelay, int pauseDelay, int triggerDelay, int triggerTimeOut) + { + _machine = new StateMachine(AlarmState.Undefined); + + preArmTimer = new System.Timers .Timer(armDelay * 1000) { AutoReset = false, Enabled = false }; + preArmTimer.Elapsed += TimeoutTimerElapsed; + pauseTimer = new System.Timers.Timer(pauseDelay * 1000) { AutoReset = false, Enabled = false }; + pauseTimer.Elapsed += TimeoutTimerElapsed; + triggerDelayTimer = new System.Timers.Timer(triggerDelay * 1000) { AutoReset = false, Enabled = false }; + triggerDelayTimer.Elapsed += TimeoutTimerElapsed; + triggerTimeOutTimer = new System.Timers.Timer(triggerTimeOut * 1000) { AutoReset = false, Enabled = false }; + triggerTimeOutTimer.Elapsed += TimeoutTimerElapsed; + + _machine.OnTransitioned(OnTransition); + + _machine.Configure(AlarmState.Undefined) + .Permit(AlarmCommand.Startup, AlarmState.Disarmed) + .OnExit(() => IsConfigured = true); + + _machine.Configure(AlarmState.Disarmed) + .Permit(AlarmCommand.Arm, AlarmState.Prearmed); + + _machine.Configure(AlarmState.Armed) + .Permit(AlarmCommand.Disarm, AlarmState.Disarmed) + .Permit(AlarmCommand.Trigger, AlarmState.PreTriggered) + .Permit(AlarmCommand.Pause, AlarmState.ArmPaused); + + _machine.Configure(AlarmState.Prearmed) + .OnEntry(() => ConfigureTimer(true, preArmTimer, "Pre-arm")) + .OnExit(() => ConfigureTimer(false, preArmTimer, "Pre-arm")) + .Permit(AlarmCommand.TimeOut, AlarmState.Armed) + .Permit(AlarmCommand.Disarm, AlarmState.Disarmed); + + _machine.Configure(AlarmState.ArmPaused) + .OnEntry(() => ConfigureTimer(true, pauseTimer, "Pause delay")) + .OnExit(() => ConfigureTimer(false, pauseTimer, "Pause delay")) + .Permit(AlarmCommand.TimeOut, AlarmState.Armed) + .Permit(AlarmCommand.Trigger, AlarmState.PreTriggered); + + _machine.Configure(AlarmState.Triggered) + .OnEntry(() => ConfigureTimer(true, triggerTimeOutTimer, "Trigger timeout")) + .OnExit(() => ConfigureTimer(false, triggerTimeOutTimer, "Trigger timeout")) + .Permit(AlarmCommand.TimeOut, AlarmState.Armed) + .Permit(AlarmCommand.Acknowledge, AlarmState.Acknowledged); + + _machine.Configure(AlarmState.PreTriggered) + .OnEntry(() => ConfigureTimer(true, triggerDelayTimer, "Trigger delay")) + .OnExit(() => ConfigureTimer(false, triggerDelayTimer, "Trigger delay")) + .Permit(AlarmCommand.TimeOut, AlarmState.Triggered) + .Permit(AlarmCommand.Disarm, AlarmState.Disarmed); + + _machine.Configure(AlarmState.Acknowledged) + .Permit(AlarmCommand.Disarm, AlarmState.Disarmed); + + _machine.Fire(AlarmCommand.Startup); + } + + private void TimeoutTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e) + { + _machine.Fire(AlarmCommand.TimeOut); + } + + private void ConfigureTimer(bool active, System.Timers.Timer timer, string timerName) + { + if (timer != null) + if (active) + { + timer.Start(); + Trace.WriteLine($"{timerName} started."); + } + else + { + timer.Stop(); + Trace.WriteLine($"{timerName} cancelled."); + } + } + + private void OnTransition(StateMachine.Transition transition) + { + Trace.WriteLine($"Transitioned from {transition.Source} to " + + $"{transition.Destination} via {transition.Trigger}."); + } + + private StateMachine _machine; + private System.Timers.Timer? preArmTimer; + private System.Timers.Timer? pauseTimer; + private System.Timers.Timer? triggerDelayTimer; + private System.Timers.Timer? triggerTimeOutTimer; + } +} \ No newline at end of file diff --git a/example/AlarmExample/AlarmCommand.cs b/example/AlarmExample/AlarmCommand.cs new file mode 100644 index 00000000..0439372a --- /dev/null +++ b/example/AlarmExample/AlarmCommand.cs @@ -0,0 +1,13 @@ +namespace AlarmExample +{ + public enum AlarmCommand + { + Startup, + Arm, + Disarm, + Trigger, + Acknowledge, + Pause, + TimeOut + } +} diff --git a/example/AlarmExample/AlarmExample.csproj b/example/AlarmExample/AlarmExample.csproj new file mode 100644 index 00000000..4d45f646 --- /dev/null +++ b/example/AlarmExample/AlarmExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/example/AlarmExample/AlarmState.cs b/example/AlarmExample/AlarmState.cs new file mode 100644 index 00000000..5c6c5a59 --- /dev/null +++ b/example/AlarmExample/AlarmState.cs @@ -0,0 +1,14 @@ +namespace AlarmExample +{ + public enum AlarmState + { + Undefined, + Disarmed, + Prearmed, + Armed, + Triggered, + ArmPaused, + PreTriggered, + Acknowledged + } +} diff --git a/example/AlarmExample/Program.cs b/example/AlarmExample/Program.cs new file mode 100644 index 00000000..49a01a44 --- /dev/null +++ b/example/AlarmExample/Program.cs @@ -0,0 +1,115 @@ +namespace AlarmExample +{ + /// + /// A simple Console Application that allows for interactive input + /// to test the implemented as a Stateless state + /// machine. + /// + internal class Program + { + static Alarm? _alarm; + + static void Main(string[] args) + { + _alarm = new Alarm(10, 10, 10, 10); + + string input = ""; + + WriteHeader(); + + while (input != "q") + { + Console.Write("> "); + + input = Console.ReadLine(); + + if (!string.IsNullOrWhiteSpace(input)) + switch (input.Split(" ")[0]) + { + case "q": + Console.WriteLine("Exiting..."); + break; + case "fire": + WriteFire(input); + break; + case "canfire": + WriteCanFire(); + break; + case "state": + WriteState(); + break; + case "h": + case "help": + WriteHelp(); + break; + case "c": + case "clear": + Console.Clear(); + WriteHeader(); + break; + default: + Console.WriteLine("Invalid command. Type 'h' or 'help' for valid commands."); + break; + } + } + } + + static void WriteHelp() + { + Console.WriteLine("Valid commands:"); + Console.WriteLine("q - Exit"); + Console.WriteLine("fire - Tries to fire the provided commands"); + Console.WriteLine("canfire - Returns a list of fireable commands"); + Console.WriteLine("state - Returns the current state"); + Console.WriteLine("c / clear - Clear the window"); + Console.WriteLine("h / help - Show this again"); + } + + static void WriteHeader() + { + Console.WriteLine("Stateless-based alarm test application:"); + Console.WriteLine("---------------------------------------"); + Console.WriteLine(""); + } + + static void WriteCanFire() + { + foreach (AlarmCommand command in (AlarmCommand[])Enum.GetValues(typeof(AlarmCommand))) + if (_alarm != null && _alarm.CanFireCommand(command)) + Console.WriteLine($"{Enum.GetName(typeof(AlarmCommand), command)}"); + } + + static void WriteState() + { + if(_alarm != null ) + Console.WriteLine($"The current state is {Enum.GetName(typeof(AlarmState), _alarm.CurrentState())}"); + } + + static void WriteFire(string input) + { + if (input.Split(" ").Length == 2) + { + try + { + if (Enum.TryParse(input.Split(" ")[1], out AlarmCommand command)) + { + if (_alarm != null) + _alarm.ExecuteTransition(command); + } + else + { + Console.WriteLine($"{input.Split(" ")[1]} is not a valid AlarmCommand."); + } + } + catch (InvalidOperationException ex) + { + Console.WriteLine($"{input.Split(" ")[1]} is not a valid AlarmCommand to the current state."); + } + } + else + { + Console.WriteLine("fire requires you to specify the command you want to fire."); + } + } + } +} \ No newline at end of file diff --git a/example/AlarmExample/StateDiagram.png b/example/AlarmExample/StateDiagram.png new file mode 100644 index 0000000000000000000000000000000000000000..752793d7cf3b77822e8ce8de2725b114ef9cd154 GIT binary patch literal 142822 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 Date: Mon, 24 Apr 2023 16:23:31 +0100 Subject: [PATCH 12/43] Add an option to retain the synchronization context --- src/Stateless/OnTransitionedEvent.cs | 6 +- src/Stateless/StateMachine.Async.cs | 18 +- src/Stateless/StateMachine.cs | 6 +- src/Stateless/StateRepresentation.Async.cs | 30 +- src/Stateless/StateRepresentation.cs | 4 +- .../SynchronizationContextFixture.cs | 341 ++++++++++++++++++ 6 files changed, 376 insertions(+), 29 deletions(-) create mode 100644 test/Stateless.Tests/SynchronizationContextFixture.cs diff --git a/src/Stateless/OnTransitionedEvent.cs b/src/Stateless/OnTransitionedEvent.cs index c8dbbc2e..4ede1525 100644 --- a/src/Stateless/OnTransitionedEvent.cs +++ b/src/Stateless/OnTransitionedEvent.cs @@ -10,7 +10,7 @@ class OnTransitionedEvent { event Action _onTransitioned; readonly List> _onTransitionedAsync = new List>(); - + public void Invoke(Transition transition) { if (_onTransitionedAsync.Count != 0) @@ -22,12 +22,12 @@ public void Invoke(Transition transition) } #if TASKS - public async Task InvokeAsync(Transition transition) + public async Task InvokeAsync(Transition transition, bool retainSynchronizationContext) { _onTransitioned?.Invoke(transition); foreach (var callback in _onTransitionedAsync) - await callback(transition).ConfigureAwait(false); + await callback(transition).ConfigureAwait(retainSynchronizationContext); } #endif diff --git a/src/Stateless/StateMachine.Async.cs b/src/Stateless/StateMachine.Async.cs index 4d8b0151..6e8be1ae 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -144,12 +144,12 @@ async Task InternalFireQueuedAsync(TTrigger trigger, params object[] args) { _firing = true; - await InternalFireOneAsync(trigger, args).ConfigureAwait(false); + await InternalFireOneAsync(trigger, args).ConfigureAwait(RetainSynchronizationContext); while (_eventQueue.Count != 0) { var queuedEvent = _eventQueue.Dequeue(); - await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(false); + await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(RetainSynchronizationContext); } } finally @@ -225,15 +225,15 @@ private async Task HandleReentryTriggerAsync(object[] args, StateRepresentation transition = new Transition(transition.Destination, transition.Destination, transition.Trigger, args); await newRepresentation.ExitAsync(transition); - await _onTransitionedEvent.InvokeAsync(transition); + await _onTransitionedEvent.InvokeAsync(transition, RetainSynchronizationContext); representation = await EnterStateAsync(newRepresentation, transition, args); - await _onTransitionCompletedEvent.InvokeAsync(transition); + await _onTransitionCompletedEvent.InvokeAsync(transition, RetainSynchronizationContext); } else { - await _onTransitionedEvent.InvokeAsync(transition); + await _onTransitionedEvent.InvokeAsync(transition, RetainSynchronizationContext); representation = await EnterStateAsync(newRepresentation, transition, args); - await _onTransitionCompletedEvent.InvokeAsync(transition); + await _onTransitionCompletedEvent.InvokeAsync(transition, RetainSynchronizationContext); } State = representation.UnderlyingState; } @@ -246,7 +246,7 @@ private async Task HandleTransitioningTriggerAsync(object[] args, StateRepresent var newRepresentation = GetRepresentation(transition.Destination); //Alert all listeners of state transition - await _onTransitionedEvent.InvokeAsync(transition); + await _onTransitionedEvent.InvokeAsync(transition, RetainSynchronizationContext); var representation =await EnterStateAsync(newRepresentation, transition, args); // Check if state has changed by entering new state (by firing triggers in OnEntry or such) @@ -256,7 +256,7 @@ private async Task HandleTransitioningTriggerAsync(object[] args, StateRepresent State = representation.UnderlyingState; } - await _onTransitionCompletedEvent.InvokeAsync(new Transition(transition.Source, State, transition.Trigger, transition.Parameters)); + await _onTransitionCompletedEvent.InvokeAsync(new Transition(transition.Source, State, transition.Trigger, transition.Parameters), RetainSynchronizationContext); } @@ -287,7 +287,7 @@ private async Task EnterStateAsync(StateRepresentation repr representation = GetRepresentation(representation.InitialTransitionTarget); // Alert all listeners of initial state transition - await _onTransitionedEvent.InvokeAsync(new Transition(transition.Destination, initialTransition.Destination, transition.Trigger, transition.Parameters)); + await _onTransitionedEvent.InvokeAsync(new Transition(transition.Destination, initialTransition.Destination, transition.Trigger, transition.Parameters), RetainSynchronizationContext); representation = await EnterStateAsync(representation, initialTransition, args); } diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 57ef4c50..081e1cf2 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -89,6 +89,10 @@ public StateMachine(TState initialState, FiringMode firingMode) : this() _firingMode = firingMode; } + /// + /// For certain situations, it is essential that the SynchronizationContext is retained for all delegate calls. + /// + public bool RetainSynchronizationContext { get; set; } = false; /// /// Default constructor @@ -186,7 +190,7 @@ StateRepresentation GetRepresentation(TState state) { if (!_stateConfiguration.TryGetValue(state, out StateRepresentation result)) { - result = new StateRepresentation(state); + result = new StateRepresentation(state, RetainSynchronizationContext); _stateConfiguration.Add(state, result); } diff --git a/src/Stateless/StateRepresentation.Async.cs b/src/Stateless/StateRepresentation.Async.cs index 6a48ca9f..66e5e546 100644 --- a/src/Stateless/StateRepresentation.Async.cs +++ b/src/Stateless/StateRepresentation.Async.cs @@ -46,29 +46,29 @@ public void AddExitAction(Func action, Reflection.InvocationIn public async Task ActivateAsync() { if (_superstate != null) - await _superstate.ActivateAsync().ConfigureAwait(false); + await _superstate.ActivateAsync().ConfigureAwait(_retainSynchronizationContext); - await ExecuteActivationActionsAsync().ConfigureAwait(false); + await ExecuteActivationActionsAsync().ConfigureAwait(_retainSynchronizationContext); } public async Task DeactivateAsync() { - await ExecuteDeactivationActionsAsync().ConfigureAwait(false); + await ExecuteDeactivationActionsAsync().ConfigureAwait(_retainSynchronizationContext); if (_superstate != null) - await _superstate.DeactivateAsync().ConfigureAwait(false); + await _superstate.DeactivateAsync().ConfigureAwait(_retainSynchronizationContext); } async Task ExecuteActivationActionsAsync() { foreach (var action in ActivateActions) - await action.ExecuteAsync().ConfigureAwait(false); + await action.ExecuteAsync().ConfigureAwait(_retainSynchronizationContext); } async Task ExecuteDeactivationActionsAsync() { foreach (var action in DeactivateActions) - await action.ExecuteAsync().ConfigureAwait(false); + await action.ExecuteAsync().ConfigureAwait(_retainSynchronizationContext); } @@ -76,14 +76,14 @@ public async Task EnterAsync(Transition transition, params object[] entryArgs) { if (transition.IsReentry) { - await ExecuteEntryActionsAsync(transition, entryArgs).ConfigureAwait(false); + await ExecuteEntryActionsAsync(transition, entryArgs).ConfigureAwait(_retainSynchronizationContext); } else if (!Includes(transition.Source)) { if (_superstate != null && !(transition is InitialTransition)) - await _superstate.EnterAsync(transition, entryArgs).ConfigureAwait(false); + await _superstate.EnterAsync(transition, entryArgs).ConfigureAwait(_retainSynchronizationContext); - await ExecuteEntryActionsAsync(transition, entryArgs).ConfigureAwait(false); + await ExecuteEntryActionsAsync(transition, entryArgs).ConfigureAwait(_retainSynchronizationContext); } } @@ -91,11 +91,11 @@ public async Task ExitAsync(Transition transition) { if (transition.IsReentry) { - await ExecuteExitActionsAsync(transition).ConfigureAwait(false); + await ExecuteExitActionsAsync(transition).ConfigureAwait(_retainSynchronizationContext); } else if (!Includes(transition.Destination)) { - await ExecuteExitActionsAsync(transition).ConfigureAwait(false); + await ExecuteExitActionsAsync(transition).ConfigureAwait(_retainSynchronizationContext); // Must check if there is a superstate, and if we are leaving that superstate if (_superstate != null) @@ -106,13 +106,13 @@ public async Task ExitAsync(Transition transition) // Destination state is within the list, exit first superstate only if it is NOT the first if (!_superstate.UnderlyingState.Equals(transition.Destination)) { - return await _superstate.ExitAsync(transition).ConfigureAwait(false); + return await _superstate.ExitAsync(transition).ConfigureAwait(_retainSynchronizationContext); } } else { // Exit the superstate as well - return await _superstate.ExitAsync(transition).ConfigureAwait(false); + return await _superstate.ExitAsync(transition).ConfigureAwait(_retainSynchronizationContext); } } } @@ -122,13 +122,13 @@ public async Task ExitAsync(Transition transition) async Task ExecuteEntryActionsAsync(Transition transition, object[] entryArgs) { foreach (var action in EntryActions) - await action.ExecuteAsync(transition, entryArgs).ConfigureAwait(false); + await action.ExecuteAsync(transition, entryArgs).ConfigureAwait(_retainSynchronizationContext); } async Task ExecuteExitActionsAsync(Transition transition) { foreach (var action in ExitActions) - await action.ExecuteAsync(transition).ConfigureAwait(false); + await action.ExecuteAsync(transition).ConfigureAwait(_retainSynchronizationContext); } } } diff --git a/src/Stateless/StateRepresentation.cs b/src/Stateless/StateRepresentation.cs index c08f797f..a532fa24 100644 --- a/src/Stateless/StateRepresentation.cs +++ b/src/Stateless/StateRepresentation.cs @@ -9,6 +9,7 @@ public partial class StateMachine internal partial class StateRepresentation { readonly TState _state; + private readonly bool _retainSynchronizationContext; internal IDictionary> TriggerBehaviours { get; } = new Dictionary>(); internal ICollection EntryActions { get; } = new List(); @@ -21,9 +22,10 @@ internal partial class StateRepresentation readonly ICollection _substates = new List(); public TState InitialTransitionTarget { get; private set; } = default; - public StateRepresentation(TState state) + public StateRepresentation(TState state, bool retainSynchronizationContext = false) { _state = state; + _retainSynchronizationContext = retainSynchronizationContext; } internal ICollection GetSubstates() diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs new file mode 100644 index 00000000..513a6958 --- /dev/null +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Sdk; + +namespace Stateless.Tests; + +public class SynchronizationContextFixture +{ + private readonly MaxConcurrencySyncContext _syncContext = new(3); + private readonly List _capturedSyncContext = new(); + + private StateMachine GetSut(State initialState = State.A) + { + var sm = new StateMachine(initialState, FiringMode.Queued); + sm.RetainSynchronizationContext = true; + return sm; + } + + private void SetSyncContext() + { + SynchronizationContext.SetSynchronizationContext(_syncContext); + } + + private async Task CaptureSyncContext() + { + _capturedSyncContext.Add(SynchronizationContext.Current); + await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); + } + + private void AssertSyncContextAlwaysRetained(int? numberOfExpectedCalls = null) + { + Assert.NotEmpty(_capturedSyncContext); + if (numberOfExpectedCalls is not null) + Assert.Equal(numberOfExpectedCalls, _capturedSyncContext.Count); + + Assert.All(_capturedSyncContext, actual => Assert.Equal(_syncContext, actual)); + } + + [Fact] + public async Task Ensure_XUnit_is_using_SyncContext() + { + SetSyncContext(); + await CaptureSyncContext(); + AssertSyncContextAlwaysRetained(); + } + + [Fact] + public async Task ConfigureAwait_false_should_lose_sync_context() + { + SetSyncContext(); + await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); + Assert.NotEqual(_syncContext, SynchronizationContext.Current); + } + + [Fact] + public async Task ConfigureAwait_true_should_retain_sync_context() + { + SetSyncContext(); + await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(true); + Assert.Equal(_syncContext, SynchronizationContext.Current); + } + + [Fact] + public async Task Single_activation_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .OnActivateAsync(CaptureSyncContext); + + // ACT + await sm.ActivateAsync(); + + // ASSERT + AssertSyncContextAlwaysRetained(1); + } + + [Fact] + public async Task Activation_of_state_with_superstate_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .OnActivateAsync(CaptureSyncContext) + .SubstateOf(State.B); + ; + sm.Configure(State.B) + .OnActivateAsync(CaptureSyncContext); + + // ACT + await sm.ActivateAsync(); + + // ASSERT + AssertSyncContextAlwaysRetained(2); + } + + [Fact] + public async Task Deactivation_of_state_with_superstate_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .OnDeactivateAsync(CaptureSyncContext) + .SubstateOf(State.B); + ; + sm.Configure(State.B) + .OnDeactivateAsync(CaptureSyncContext); + + // ACT + await sm.DeactivateAsync(); + + // ASSERT + AssertSyncContextAlwaysRetained(2); + } + + [Fact] + public async Task Multiple_activations_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .OnActivateAsync(CaptureSyncContext) + .OnActivateAsync(CaptureSyncContext) + .OnActivateAsync(CaptureSyncContext); + + // ACT + await sm.ActivateAsync(); + + // ASSERT + AssertSyncContextAlwaysRetained(3); + } + + [Fact] + public async Task Multiple_Deactivations_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .OnDeactivateAsync(CaptureSyncContext) + .OnDeactivateAsync(CaptureSyncContext) + .OnDeactivateAsync(CaptureSyncContext); + + // ACT + await sm.DeactivateAsync(); + + // ASSERT + AssertSyncContextAlwaysRetained(3); + } + + [Fact] + public async Task OnEntry_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A).Permit(Trigger.X, State.B); + sm.Configure(State.B) + .OnEntryAsync(CaptureSyncContext); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(1); + } + + [Fact] + public async Task Multiple_OnEntry_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A).Permit(Trigger.X, State.B); + sm.Configure(State.B) + .OnEntryAsync(CaptureSyncContext) + .OnEntryAsync(CaptureSyncContext) + .OnEntryAsync(CaptureSyncContext); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(3); + } + + [Fact] + public async Task Multiple_OnExit_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .Permit(Trigger.X, State.B) + .OnExitAsync(CaptureSyncContext) + .OnExitAsync(CaptureSyncContext) + .OnExitAsync(CaptureSyncContext); + sm.Configure(State.B); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(3); + } + + [Fact] + public async Task OnExit_state_with_superstate_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(State.B); + sm.Configure(State.A) + .OnExitAsync(CaptureSyncContext) + ; + + sm.Configure(State.B) + .SubstateOf(State.A) + .Permit(Trigger.X, State.C) + .OnExitAsync(CaptureSyncContext) + ; + sm.Configure(State.C); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(2); + } + + [Fact] + public async Task OnExit_state_and_superstate_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(State.C); + sm.Configure(State.A); + + sm.Configure(State.B) + .SubstateOf(State.A) + .OnExitAsync(CaptureSyncContext); + + sm.Configure(State.C) + .SubstateOf(State.B) + .Permit(Trigger.X, State.A) + .OnExitAsync(CaptureSyncContext); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(2); + } + + [Fact] + public async Task Multiple_OnEntry_on_Reentry_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A).PermitReentry(Trigger.X) + .OnEntryAsync(CaptureSyncContext) + .OnEntryAsync(CaptureSyncContext); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(2); + } + + [Fact] + public async Task Multiple_OnExit_on_Reentry_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A).PermitReentry(Trigger.X) + .OnExitAsync(CaptureSyncContext) + .OnExitAsync(CaptureSyncContext); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(2); + } + + [Fact] + public async Task Trigger_firing_another_Trigger_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .InternalTransitionAsync(Trigger.X, async () => + { + await CaptureSyncContext(); + await sm.FireAsync(Trigger.Y); + }) + .Permit(Trigger.Y, State.B) + ; + sm.Configure(State.B) + .OnEntryAsync(CaptureSyncContext); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(2); + } + + [Fact] + public async Task OnTransition_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .Permit(Trigger.X, State.B); + + sm.Configure(State.B); + + sm.OnTransitionedAsync(_ => CaptureSyncContext()); + sm.OnTransitionedAsync(_ => CaptureSyncContext()); + sm.OnTransitionedAsync(_ => CaptureSyncContext()); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(3); + } +} \ No newline at end of file From e465b902fef531ebcb7c2c7ad25714a5a8b1646b Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Mon, 24 Apr 2023 16:31:29 +0100 Subject: [PATCH 13/43] Make specifying whether to retain the sync context on StateRepresentation mandatory --- src/Stateless/StateMachine.cs | 4 ++-- src/Stateless/StateRepresentation.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 081e1cf2..c0d403e0 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -162,7 +162,7 @@ StateRepresentation CurrentRepresentation /// public StateMachineInfo GetInfo() { - var initialState = StateInfo.CreateStateInfo(new StateRepresentation(_initialState)); + var initialState = StateInfo.CreateStateInfo(new StateRepresentation(_initialState, RetainSynchronizationContext)); var representations = _stateConfiguration.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -172,7 +172,7 @@ public StateMachineInfo GetInfo() var reachable = behaviours .Distinct() .Except(representations.Keys) - .Select(underlying => new StateRepresentation(underlying)) + .Select(underlying => new StateRepresentation(underlying, RetainSynchronizationContext)) .ToArray(); foreach (var representation in reachable) diff --git a/src/Stateless/StateRepresentation.cs b/src/Stateless/StateRepresentation.cs index a532fa24..372e11c1 100644 --- a/src/Stateless/StateRepresentation.cs +++ b/src/Stateless/StateRepresentation.cs @@ -22,7 +22,7 @@ internal partial class StateRepresentation readonly ICollection _substates = new List(); public TState InitialTransitionTarget { get; private set; } = default; - public StateRepresentation(TState state, bool retainSynchronizationContext = false) + public StateRepresentation(TState state, bool retainSynchronizationContext) { _state = state; _retainSynchronizationContext = retainSynchronizationContext; From cefd21c10810dcc23a67e662a4d359362dfde8bf Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Mon, 24 Apr 2023 16:33:16 +0100 Subject: [PATCH 14/43] Specify whether to retain the sync context on StateRepresentation everywhere within StateMachine --- src/Stateless/StateRepresentation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stateless/StateRepresentation.cs b/src/Stateless/StateRepresentation.cs index 372e11c1..a532fa24 100644 --- a/src/Stateless/StateRepresentation.cs +++ b/src/Stateless/StateRepresentation.cs @@ -22,7 +22,7 @@ internal partial class StateRepresentation readonly ICollection _substates = new List(); public TState InitialTransitionTarget { get; private set; } = default; - public StateRepresentation(TState state, bool retainSynchronizationContext) + public StateRepresentation(TState state, bool retainSynchronizationContext = false) { _state = state; _retainSynchronizationContext = retainSynchronizationContext; From c9ffd2c7c032b25573ed000a6a785ef9636863ed Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Mon, 24 Apr 2023 16:48:02 +0100 Subject: [PATCH 15/43] Also apply the sync context to a sync action internal trigger --- src/Stateless/StateMachine.Async.cs | 7 +- .../SynchronizationContextFixture.cs | 87 ++++++++++++------- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/Stateless/StateMachine.Async.cs b/src/Stateless/StateMachine.Async.cs index 6e8be1ae..9bf20cbb 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Stateless @@ -205,7 +206,11 @@ async Task InternalFireOneAsync(TTrigger trigger, params object[] args) if (itb is InternalTriggerBehaviour.Async ita) await ita.ExecuteAsync(transition, args); else - await Task.Run(() => itb.Execute(transition, args)); + if (RetainSynchronizationContext) + await Task.Factory.StartNew(() => itb.Execute(transition, args), + CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.FromCurrentSynchronizationContext()); + else + await Task.Run(() => itb.Execute(transition, args)); break; } default: diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index 513a6958..d5c87fa0 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -24,12 +24,17 @@ private void SetSyncContext() SynchronizationContext.SetSynchronizationContext(_syncContext); } - private async Task CaptureSyncContext() + private async Task CaptureSyncContextAsync() { _capturedSyncContext.Add(SynchronizationContext.Current); await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); } + private void CaptureSyncContext() + { + _capturedSyncContext.Add(SynchronizationContext.Current); + } + private void AssertSyncContextAlwaysRetained(int? numberOfExpectedCalls = null) { Assert.NotEmpty(_capturedSyncContext); @@ -43,7 +48,7 @@ private void AssertSyncContextAlwaysRetained(int? numberOfExpectedCalls = null) public async Task Ensure_XUnit_is_using_SyncContext() { SetSyncContext(); - await CaptureSyncContext(); + await CaptureSyncContextAsync(); AssertSyncContextAlwaysRetained(); } @@ -70,7 +75,7 @@ public async Task Single_activation_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnActivateAsync(CaptureSyncContext); + .OnActivateAsync(CaptureSyncContextAsync); // ACT await sm.ActivateAsync(); @@ -86,11 +91,11 @@ public async Task Activation_of_state_with_superstate_should_retain_SyncContext( SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnActivateAsync(CaptureSyncContext) + .OnActivateAsync(CaptureSyncContextAsync) .SubstateOf(State.B); ; sm.Configure(State.B) - .OnActivateAsync(CaptureSyncContext); + .OnActivateAsync(CaptureSyncContextAsync); // ACT await sm.ActivateAsync(); @@ -106,11 +111,11 @@ public async Task Deactivation_of_state_with_superstate_should_retain_SyncContex SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnDeactivateAsync(CaptureSyncContext) + .OnDeactivateAsync(CaptureSyncContextAsync) .SubstateOf(State.B); ; sm.Configure(State.B) - .OnDeactivateAsync(CaptureSyncContext); + .OnDeactivateAsync(CaptureSyncContextAsync); // ACT await sm.DeactivateAsync(); @@ -126,9 +131,9 @@ public async Task Multiple_activations_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnActivateAsync(CaptureSyncContext) - .OnActivateAsync(CaptureSyncContext) - .OnActivateAsync(CaptureSyncContext); + .OnActivateAsync(CaptureSyncContextAsync) + .OnActivateAsync(CaptureSyncContextAsync) + .OnActivateAsync(CaptureSyncContextAsync); // ACT await sm.ActivateAsync(); @@ -144,9 +149,9 @@ public async Task Multiple_Deactivations_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnDeactivateAsync(CaptureSyncContext) - .OnDeactivateAsync(CaptureSyncContext) - .OnDeactivateAsync(CaptureSyncContext); + .OnDeactivateAsync(CaptureSyncContextAsync) + .OnDeactivateAsync(CaptureSyncContextAsync) + .OnDeactivateAsync(CaptureSyncContextAsync); // ACT await sm.DeactivateAsync(); @@ -163,7 +168,7 @@ public async Task OnEntry_should_retain_SyncContext() var sm = GetSut(); sm.Configure(State.A).Permit(Trigger.X, State.B); sm.Configure(State.B) - .OnEntryAsync(CaptureSyncContext); + .OnEntryAsync(CaptureSyncContextAsync); // ACT await sm.FireAsync(Trigger.X); @@ -180,9 +185,9 @@ public async Task Multiple_OnEntry_should_retain_SyncContext() var sm = GetSut(); sm.Configure(State.A).Permit(Trigger.X, State.B); sm.Configure(State.B) - .OnEntryAsync(CaptureSyncContext) - .OnEntryAsync(CaptureSyncContext) - .OnEntryAsync(CaptureSyncContext); + .OnEntryAsync(CaptureSyncContextAsync) + .OnEntryAsync(CaptureSyncContextAsync) + .OnEntryAsync(CaptureSyncContextAsync); // ACT await sm.FireAsync(Trigger.X); @@ -199,9 +204,9 @@ public async Task Multiple_OnExit_should_retain_SyncContext() var sm = GetSut(); sm.Configure(State.A) .Permit(Trigger.X, State.B) - .OnExitAsync(CaptureSyncContext) - .OnExitAsync(CaptureSyncContext) - .OnExitAsync(CaptureSyncContext); + .OnExitAsync(CaptureSyncContextAsync) + .OnExitAsync(CaptureSyncContextAsync) + .OnExitAsync(CaptureSyncContextAsync); sm.Configure(State.B); // ACT @@ -218,13 +223,13 @@ public async Task OnExit_state_with_superstate_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(State.B); sm.Configure(State.A) - .OnExitAsync(CaptureSyncContext) + .OnExitAsync(CaptureSyncContextAsync) ; sm.Configure(State.B) .SubstateOf(State.A) .Permit(Trigger.X, State.C) - .OnExitAsync(CaptureSyncContext) + .OnExitAsync(CaptureSyncContextAsync) ; sm.Configure(State.C); @@ -245,12 +250,12 @@ public async Task OnExit_state_and_superstate_should_retain_SyncContext() sm.Configure(State.B) .SubstateOf(State.A) - .OnExitAsync(CaptureSyncContext); + .OnExitAsync(CaptureSyncContextAsync); sm.Configure(State.C) .SubstateOf(State.B) .Permit(Trigger.X, State.A) - .OnExitAsync(CaptureSyncContext); + .OnExitAsync(CaptureSyncContextAsync); // ACT await sm.FireAsync(Trigger.X); @@ -266,8 +271,8 @@ public async Task Multiple_OnEntry_on_Reentry_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A).PermitReentry(Trigger.X) - .OnEntryAsync(CaptureSyncContext) - .OnEntryAsync(CaptureSyncContext); + .OnEntryAsync(CaptureSyncContextAsync) + .OnEntryAsync(CaptureSyncContextAsync); // ACT await sm.FireAsync(Trigger.X); @@ -283,8 +288,8 @@ public async Task Multiple_OnExit_on_Reentry_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A).PermitReentry(Trigger.X) - .OnExitAsync(CaptureSyncContext) - .OnExitAsync(CaptureSyncContext); + .OnExitAsync(CaptureSyncContextAsync) + .OnExitAsync(CaptureSyncContextAsync); // ACT await sm.FireAsync(Trigger.X); @@ -302,13 +307,13 @@ public async Task Trigger_firing_another_Trigger_should_retain_SyncContext() sm.Configure(State.A) .InternalTransitionAsync(Trigger.X, async () => { - await CaptureSyncContext(); + await CaptureSyncContextAsync(); await sm.FireAsync(Trigger.Y); }) .Permit(Trigger.Y, State.B) ; sm.Configure(State.B) - .OnEntryAsync(CaptureSyncContext); + .OnEntryAsync(CaptureSyncContextAsync); // ACT await sm.FireAsync(Trigger.X); @@ -328,9 +333,9 @@ public async Task OnTransition_should_retain_SyncContext() sm.Configure(State.B); - sm.OnTransitionedAsync(_ => CaptureSyncContext()); - sm.OnTransitionedAsync(_ => CaptureSyncContext()); - sm.OnTransitionedAsync(_ => CaptureSyncContext()); + sm.OnTransitionedAsync(_ => CaptureSyncContextAsync()); + sm.OnTransitionedAsync(_ => CaptureSyncContextAsync()); + sm.OnTransitionedAsync(_ => CaptureSyncContextAsync()); // ACT await sm.FireAsync(Trigger.X); @@ -338,4 +343,20 @@ public async Task OnTransition_should_retain_SyncContext() // ASSERT AssertSyncContextAlwaysRetained(3); } + + [Fact] + public async Task InternalTransition_firing_a_sync_action_should_retain_SyncContext() + { + // ARRANGE + SetSyncContext(); + var sm = GetSut(); + sm.Configure(State.A) + .InternalTransition(Trigger.X, CaptureSyncContext); + + // ACT + await sm.FireAsync(Trigger.X); + + // ASSERT + AssertSyncContextAlwaysRetained(1); + } } \ No newline at end of file From 4625bcd00b40ce529fa79aeabaae69c3a4e0c6fb Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Mon, 24 Apr 2023 17:08:25 +0100 Subject: [PATCH 16/43] Make test clearer --- .../SynchronizationContextFixture.cs | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index d5c87fa0..154e63de 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -24,17 +26,23 @@ private void SetSyncContext() SynchronizationContext.SetSynchronizationContext(_syncContext); } - private async Task CaptureSyncContextAsync() + private async Task CaptureThenLoseSyncContext() { - _capturedSyncContext.Add(SynchronizationContext.Current); - await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); + CaptureSyncContext(); + await LoseSyncContext().ConfigureAwait(false); } private void CaptureSyncContext() { _capturedSyncContext.Add(SynchronizationContext.Current); } - + + private async Task LoseSyncContext() + { + await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); + Assert.NotEqual(_syncContext, SynchronizationContext.Current); + } + private void AssertSyncContextAlwaysRetained(int? numberOfExpectedCalls = null) { Assert.NotEmpty(_capturedSyncContext); @@ -45,29 +53,21 @@ private void AssertSyncContextAlwaysRetained(int? numberOfExpectedCalls = null) } [Fact] - public async Task Ensure_XUnit_is_using_SyncContext() + public void Ensure_XUnit_is_using_SyncContext() { SetSyncContext(); - await CaptureSyncContextAsync(); + CaptureSyncContext(); AssertSyncContextAlwaysRetained(); } [Fact] - public async Task ConfigureAwait_false_should_lose_sync_context() + public async Task Ensure_XUnit_can_lose_sync_context() { SetSyncContext(); - await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); + await LoseSyncContext().ConfigureAwait(false); Assert.NotEqual(_syncContext, SynchronizationContext.Current); } - [Fact] - public async Task ConfigureAwait_true_should_retain_sync_context() - { - SetSyncContext(); - await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(true); - Assert.Equal(_syncContext, SynchronizationContext.Current); - } - [Fact] public async Task Single_activation_should_retain_SyncContext() { @@ -75,7 +75,7 @@ public async Task Single_activation_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnActivateAsync(CaptureSyncContextAsync); + .OnActivateAsync(CaptureThenLoseSyncContext); // ACT await sm.ActivateAsync(); @@ -91,11 +91,11 @@ public async Task Activation_of_state_with_superstate_should_retain_SyncContext( SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnActivateAsync(CaptureSyncContextAsync) + .OnActivateAsync(CaptureThenLoseSyncContext) .SubstateOf(State.B); ; sm.Configure(State.B) - .OnActivateAsync(CaptureSyncContextAsync); + .OnActivateAsync(CaptureThenLoseSyncContext); // ACT await sm.ActivateAsync(); @@ -111,11 +111,11 @@ public async Task Deactivation_of_state_with_superstate_should_retain_SyncContex SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnDeactivateAsync(CaptureSyncContextAsync) + .OnDeactivateAsync(CaptureThenLoseSyncContext) .SubstateOf(State.B); ; sm.Configure(State.B) - .OnDeactivateAsync(CaptureSyncContextAsync); + .OnDeactivateAsync(CaptureThenLoseSyncContext); // ACT await sm.DeactivateAsync(); @@ -131,9 +131,9 @@ public async Task Multiple_activations_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnActivateAsync(CaptureSyncContextAsync) - .OnActivateAsync(CaptureSyncContextAsync) - .OnActivateAsync(CaptureSyncContextAsync); + .OnActivateAsync(CaptureThenLoseSyncContext) + .OnActivateAsync(CaptureThenLoseSyncContext) + .OnActivateAsync(CaptureThenLoseSyncContext); // ACT await sm.ActivateAsync(); @@ -149,9 +149,9 @@ public async Task Multiple_Deactivations_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A) - .OnDeactivateAsync(CaptureSyncContextAsync) - .OnDeactivateAsync(CaptureSyncContextAsync) - .OnDeactivateAsync(CaptureSyncContextAsync); + .OnDeactivateAsync(CaptureThenLoseSyncContext) + .OnDeactivateAsync(CaptureThenLoseSyncContext) + .OnDeactivateAsync(CaptureThenLoseSyncContext); // ACT await sm.DeactivateAsync(); @@ -168,7 +168,7 @@ public async Task OnEntry_should_retain_SyncContext() var sm = GetSut(); sm.Configure(State.A).Permit(Trigger.X, State.B); sm.Configure(State.B) - .OnEntryAsync(CaptureSyncContextAsync); + .OnEntryAsync(CaptureThenLoseSyncContext); // ACT await sm.FireAsync(Trigger.X); @@ -185,9 +185,9 @@ public async Task Multiple_OnEntry_should_retain_SyncContext() var sm = GetSut(); sm.Configure(State.A).Permit(Trigger.X, State.B); sm.Configure(State.B) - .OnEntryAsync(CaptureSyncContextAsync) - .OnEntryAsync(CaptureSyncContextAsync) - .OnEntryAsync(CaptureSyncContextAsync); + .OnEntryAsync(CaptureThenLoseSyncContext) + .OnEntryAsync(CaptureThenLoseSyncContext) + .OnEntryAsync(CaptureThenLoseSyncContext); // ACT await sm.FireAsync(Trigger.X); @@ -204,9 +204,9 @@ public async Task Multiple_OnExit_should_retain_SyncContext() var sm = GetSut(); sm.Configure(State.A) .Permit(Trigger.X, State.B) - .OnExitAsync(CaptureSyncContextAsync) - .OnExitAsync(CaptureSyncContextAsync) - .OnExitAsync(CaptureSyncContextAsync); + .OnExitAsync(CaptureThenLoseSyncContext) + .OnExitAsync(CaptureThenLoseSyncContext) + .OnExitAsync(CaptureThenLoseSyncContext); sm.Configure(State.B); // ACT @@ -223,13 +223,13 @@ public async Task OnExit_state_with_superstate_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(State.B); sm.Configure(State.A) - .OnExitAsync(CaptureSyncContextAsync) + .OnExitAsync(CaptureThenLoseSyncContext) ; sm.Configure(State.B) .SubstateOf(State.A) .Permit(Trigger.X, State.C) - .OnExitAsync(CaptureSyncContextAsync) + .OnExitAsync(CaptureThenLoseSyncContext) ; sm.Configure(State.C); @@ -250,12 +250,12 @@ public async Task OnExit_state_and_superstate_should_retain_SyncContext() sm.Configure(State.B) .SubstateOf(State.A) - .OnExitAsync(CaptureSyncContextAsync); + .OnExitAsync(CaptureThenLoseSyncContext); sm.Configure(State.C) .SubstateOf(State.B) .Permit(Trigger.X, State.A) - .OnExitAsync(CaptureSyncContextAsync); + .OnExitAsync(CaptureThenLoseSyncContext); // ACT await sm.FireAsync(Trigger.X); @@ -271,8 +271,8 @@ public async Task Multiple_OnEntry_on_Reentry_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A).PermitReentry(Trigger.X) - .OnEntryAsync(CaptureSyncContextAsync) - .OnEntryAsync(CaptureSyncContextAsync); + .OnEntryAsync(CaptureThenLoseSyncContext) + .OnEntryAsync(CaptureThenLoseSyncContext); // ACT await sm.FireAsync(Trigger.X); @@ -288,8 +288,8 @@ public async Task Multiple_OnExit_on_Reentry_should_retain_SyncContext() SetSyncContext(); var sm = GetSut(); sm.Configure(State.A).PermitReentry(Trigger.X) - .OnExitAsync(CaptureSyncContextAsync) - .OnExitAsync(CaptureSyncContextAsync); + .OnExitAsync(CaptureThenLoseSyncContext) + .OnExitAsync(CaptureThenLoseSyncContext); // ACT await sm.FireAsync(Trigger.X); @@ -307,13 +307,13 @@ public async Task Trigger_firing_another_Trigger_should_retain_SyncContext() sm.Configure(State.A) .InternalTransitionAsync(Trigger.X, async () => { - await CaptureSyncContextAsync(); + await CaptureThenLoseSyncContext(); await sm.FireAsync(Trigger.Y); }) .Permit(Trigger.Y, State.B) ; sm.Configure(State.B) - .OnEntryAsync(CaptureSyncContextAsync); + .OnEntryAsync(CaptureThenLoseSyncContext); // ACT await sm.FireAsync(Trigger.X); @@ -333,9 +333,9 @@ public async Task OnTransition_should_retain_SyncContext() sm.Configure(State.B); - sm.OnTransitionedAsync(_ => CaptureSyncContextAsync()); - sm.OnTransitionedAsync(_ => CaptureSyncContextAsync()); - sm.OnTransitionedAsync(_ => CaptureSyncContextAsync()); + sm.OnTransitionedAsync(_ => CaptureThenLoseSyncContext()); + sm.OnTransitionedAsync(_ => CaptureThenLoseSyncContext()); + sm.OnTransitionedAsync(_ => CaptureThenLoseSyncContext()); // ACT await sm.FireAsync(Trigger.X); From 1a185a7815e91f23d75e7a16afe9cc907bea4a8b Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Mon, 24 Apr 2023 17:19:40 +0100 Subject: [PATCH 17/43] Added comments --- .../SynchronizationContextFixture.cs | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index 154e63de..fece2d0d 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -11,25 +11,30 @@ namespace Stateless.Tests; public class SynchronizationContextFixture { - private readonly MaxConcurrencySyncContext _syncContext = new(3); + // Define a custom SynchronizationContext. All calls made to delegates should within this context. + private readonly MaxConcurrencySyncContext _customSynchronizationContext = new(3); private readonly List _capturedSyncContext = new(); private StateMachine GetSut(State initialState = State.A) { - var sm = new StateMachine(initialState, FiringMode.Queued); - sm.RetainSynchronizationContext = true; - return sm; + return new StateMachine(initialState, FiringMode.Queued) + { + RetainSynchronizationContext = true + }; } private void SetSyncContext() { - SynchronizationContext.SetSynchronizationContext(_syncContext); + SynchronizationContext.SetSynchronizationContext(_customSynchronizationContext); } - + + /// + /// Simulate a call that loses the synchronization context + /// private async Task CaptureThenLoseSyncContext() { CaptureSyncContext(); - await LoseSyncContext().ConfigureAwait(false); + await LoseSyncContext().ConfigureAwait(false); // ConfigureAwait false here to ensure we continue using the sync context returned by LoseSyncContext } private void CaptureSyncContext() @@ -39,17 +44,18 @@ private void CaptureSyncContext() private async Task LoseSyncContext() { - await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); - Assert.NotEqual(_syncContext, SynchronizationContext.Current); + await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); // Switch synchronization context and continue + Assert.NotEqual(_customSynchronizationContext, SynchronizationContext.Current); } - private void AssertSyncContextAlwaysRetained(int? numberOfExpectedCalls = null) + /// + /// + /// + /// + private void AssertSyncContextAlwaysRetained(int numberOfExpectedCalls) { - Assert.NotEmpty(_capturedSyncContext); - if (numberOfExpectedCalls is not null) - Assert.Equal(numberOfExpectedCalls, _capturedSyncContext.Count); - - Assert.All(_capturedSyncContext, actual => Assert.Equal(_syncContext, actual)); + Assert.Equal(numberOfExpectedCalls, _capturedSyncContext.Count); + Assert.All(_capturedSyncContext, actual => Assert.Equal(_customSynchronizationContext, actual)); } [Fact] @@ -57,7 +63,7 @@ public void Ensure_XUnit_is_using_SyncContext() { SetSyncContext(); CaptureSyncContext(); - AssertSyncContextAlwaysRetained(); + AssertSyncContextAlwaysRetained(1); } [Fact] @@ -65,7 +71,7 @@ public async Task Ensure_XUnit_can_lose_sync_context() { SetSyncContext(); await LoseSyncContext().ConfigureAwait(false); - Assert.NotEqual(_syncContext, SynchronizationContext.Current); + Assert.NotEqual(_customSynchronizationContext, SynchronizationContext.Current); } [Fact] From a5a35cae3a1c6dbdb90ccf99caeb3bba05e3b602 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Mon, 24 Apr 2023 17:22:25 +0100 Subject: [PATCH 18/43] Remove unused using declarations --- test/Stateless.Tests/SynchronizationContextFixture.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index fece2d0d..c693d32c 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Xunit; From ce952236e1af99f325156976b0d5e328e6aafeb9 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 25 Apr 2023 09:16:59 +0100 Subject: [PATCH 19/43] Added documentation to the README --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c2e3ef4..3d571caa 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ phoneCall.Configure(State.OffHook) Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate. -The guard clauses will be evaluated whenever a trigger is fired. Guards should therefor be made side effect free. +The guard clauses will be evaluated whenever a trigger is fired. Guards should therefore be made side effect free. ### Parameterised Triggers @@ -225,6 +225,20 @@ await stateMachine.FireAsync(Trigger.Assigned); **Note:** while `StateMachine` may be used _asynchronously_, it remains single-threaded and may not be used _concurrently_ by multiple threads. +## Advanced Features ## + +### Retaining the SynchronizationContext ### +In specific situations where all handler methods must be invoked with the consumer's SynchronizationContext, set the _RetainSynchronizationContext_ property on creation: + +```csharp +var stateMachine = new StateMachine(initialState) +{ + RetainSynchronizationContext = true +}; +``` + +Setting this is vital within a Microsoft Orleans Grain for example, which requires the SynchronizationContext in order to make calls to other Grains. + ## Building Stateless runs on .NET 4.0+ and practically all modern .NET platforms by targeting .NET Standard 1.0 and .NET Standard2.0. Visual Studio 2017 or later is required to build the solution. From b2b3a404105cae959b37faf3c4988a84d4e2614e Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 25 Apr 2023 09:30:14 +0100 Subject: [PATCH 20/43] Further documentation of the tests --- .../SynchronizationContextFixture.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index c693d32c..cd955114 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -9,7 +9,7 @@ namespace Stateless.Tests; public class SynchronizationContextFixture { - // Define a custom SynchronizationContext. All calls made to delegates should within this context. + // Define a custom SynchronizationContext. All calls made to delegates should be with this context. private readonly MaxConcurrencySyncContext _customSynchronizationContext = new(3); private readonly List _capturedSyncContext = new(); @@ -47,15 +47,20 @@ private async Task LoseSyncContext() } /// - /// + /// Tests capture the SynchronizationContext at various points through out their execution. + /// This asserts that every capture is the expected SynchronizationContext instance and that is hasn't been lost. /// - /// + /// Ensure that we have the expected number of captures private void AssertSyncContextAlwaysRetained(int numberOfExpectedCalls) { Assert.Equal(numberOfExpectedCalls, _capturedSyncContext.Count); Assert.All(_capturedSyncContext, actual => Assert.Equal(_customSynchronizationContext, actual)); } + /// + /// XUnit uses its own SynchronizationContext to execute each test. Therefore, placing SetSyncContext() in the constructor instead of + /// at the start of every test does not work as desired. This test ensures XUnit's behaviour has not changed. + /// [Fact] public void Ensure_XUnit_is_using_SyncContext() { @@ -64,6 +69,10 @@ public void Ensure_XUnit_is_using_SyncContext() AssertSyncContextAlwaysRetained(1); } + /// + /// SynchronizationContext are funny things. The way that they are lost varies depending on their implementation. + /// This test ensures that our mechanism for losing the SynchronizationContext works. + /// [Fact] public async Task Ensure_XUnit_can_lose_sync_context() { From 12e0b113c50efd183bd898d2d1b3805e23c22808 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 25 Apr 2023 09:43:17 +0100 Subject: [PATCH 21/43] Update example apps to net6.0 --- example/BugTrackerExample/BugTrackerExample.csproj | 2 +- example/JsonExample/JsonExample.csproj | 2 +- example/OnOffExample/OnOffExample.csproj | 2 +- example/TelephoneCallExample/TelephoneCallExample.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/BugTrackerExample/BugTrackerExample.csproj b/example/BugTrackerExample/BugTrackerExample.csproj index 84cf0d5b..7185d83e 100644 --- a/example/BugTrackerExample/BugTrackerExample.csproj +++ b/example/BugTrackerExample/BugTrackerExample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + net6.0 BugTrackerExample Exe BugTrackerExample diff --git a/example/JsonExample/JsonExample.csproj b/example/JsonExample/JsonExample.csproj index 289d4eb7..2dd5cb6b 100644 --- a/example/JsonExample/JsonExample.csproj +++ b/example/JsonExample/JsonExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + net6.0 diff --git a/example/OnOffExample/OnOffExample.csproj b/example/OnOffExample/OnOffExample.csproj index 725615a9..94652a1f 100644 --- a/example/OnOffExample/OnOffExample.csproj +++ b/example/OnOffExample/OnOffExample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + net6.0 OnOffExample Exe OnOffExample diff --git a/example/TelephoneCallExample/TelephoneCallExample.csproj b/example/TelephoneCallExample/TelephoneCallExample.csproj index 6a798040..666893eb 100644 --- a/example/TelephoneCallExample/TelephoneCallExample.csproj +++ b/example/TelephoneCallExample/TelephoneCallExample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + net6.0 TelephoneCallExample Exe TelephoneCallExample From 487842cca5d97c70077c11c97d9cdf3a08bedf63 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 25 Apr 2023 10:55:09 +0100 Subject: [PATCH 22/43] Fix typo --- test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs b/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs index 6d6ecb5e..6c739560 100644 --- a/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs @@ -7,8 +7,8 @@ public class TransitioningTriggerBehaviourFixture [Fact] public void TransitionsToDestinationState() { - var transtioning = new StateMachine.TransitioningTriggerBehaviour(Trigger.X, State.C, null); - Assert.True(transtioning.ResultsInTransitionFrom(State.B, new object[0], out State destination)); + var transitioning = new StateMachine.TransitioningTriggerBehaviour(Trigger.X, State.C, null); + Assert.True(transitioning.ResultsInTransitionFrom(State.B, new object[0], out State destination)); Assert.Equal(State.C, destination); } } From d9f577828240c09a0ab21c6f84056f04496e9e9f Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 25 Apr 2023 11:06:49 +0100 Subject: [PATCH 23/43] Fix typos --- src/Stateless/GuardCondition.cs | 2 +- src/Stateless/Reflection/DynamicTransitionInfo.cs | 2 +- src/Stateless/Reflection/StateInfo.cs | 2 +- src/Stateless/StateConfiguration.cs | 4 ++-- src/Stateless/StateMachine.Async.cs | 6 +++--- src/Stateless/StateRepresentation.cs | 2 +- test/Stateless.Tests/InternalTransitionFixture.cs | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Stateless/GuardCondition.cs b/src/Stateless/GuardCondition.cs index 775588b3..1a4b2320 100644 --- a/src/Stateless/GuardCondition.cs +++ b/src/Stateless/GuardCondition.cs @@ -10,7 +10,7 @@ internal class GuardCondition /// /// Constructor that takes in a guard with no argument. - /// This is needed because we wrap the no-arg guard with a lamba and therefore method description won't match what was origianlly passed in. + /// This is needed because we wrap the no-arg guard with a lambda and therefore method description won't match what was originally passed in. /// We need to preserve the method description before wrapping so Reflection methods will work. /// /// No Argument Guard Condition diff --git a/src/Stateless/Reflection/DynamicTransitionInfo.cs b/src/Stateless/Reflection/DynamicTransitionInfo.cs index 0b6d924c..a8179256 100644 --- a/src/Stateless/Reflection/DynamicTransitionInfo.cs +++ b/src/Stateless/Reflection/DynamicTransitionInfo.cs @@ -59,7 +59,7 @@ public void Add(TState destinationState, string criterion) public class DynamicTransitionInfo : TransitionInfo { /// - /// Gets method informtion for the destination state selector. + /// Gets method information for the destination state selector. /// public InvocationInfo DestinationStateSelectorDescription { get; private set; } diff --git a/src/Stateless/Reflection/StateInfo.cs b/src/Stateless/Reflection/StateInfo.cs index e98e45bb..1a7cd009 100644 --- a/src/Stateless/Reflection/StateInfo.cs +++ b/src/Stateless/Reflection/StateInfo.cs @@ -136,7 +136,7 @@ private void AddRelationships( public IEnumerable DeactivateActions { get; private set; } /// - /// Actions that are defined to be exectuted on state-exit. + /// Actions that are defined to be executed on state-exit. /// public IEnumerable ExitActions { get; private set; } diff --git a/src/Stateless/StateConfiguration.cs b/src/Stateless/StateConfiguration.cs index 3ddd78d3..beec0168 100644 --- a/src/Stateless/StateConfiguration.cs +++ b/src/Stateless/StateConfiguration.cs @@ -1120,7 +1120,7 @@ public StateConfiguration OnExit(Action exitAction, string exitActio /// Substates inherit the allowed transitions of their superstate. /// When entering directly into a substate from outside of the superstate, /// entry actions for the superstate are executed. - /// Likewise when leaving from the substate to outside the supserstate, + /// Likewise when leaving from the substate to outside the superstate, /// exit actions for the superstate will execute. /// /// The superstate. @@ -1774,7 +1774,7 @@ StateConfiguration InternalPermitDynamicIf(TTrigger trigger, FuncA stateConfiguration object public StateConfiguration InitialTransition(TState targetState) { - if (_representation.HasInitialTransition) throw new InvalidOperationException($"This state has already been configured with an inital transition ({_representation.InitialTransitionTarget})."); + if (_representation.HasInitialTransition) throw new InvalidOperationException($"This state has already been configured with an initial transition ({_representation.InitialTransitionTarget})."); if (targetState.Equals(State)) throw new ArgumentException("Setting the current state as the target destination state is not allowed.", nameof(targetState)); _representation.SetInitialTransition(targetState); diff --git a/src/Stateless/StateMachine.Async.cs b/src/Stateless/StateMachine.Async.cs index 4d8b0151..1202297b 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -10,7 +10,7 @@ namespace Stateless public partial class StateMachine { /// - /// Activates current state in asynchronous fashion. Actions associated with activating the currrent state + /// Activates current state in asynchronous fashion. Actions associated with activating the current state /// will be invoked. The activation is idempotent and subsequent activation of the same current state /// will not lead to re-execution of activation callbacks. /// @@ -21,7 +21,7 @@ public Task ActivateAsync() } /// - /// Deactivates current state in asynchronous fashion. Actions associated with deactivating the currrent state + /// Deactivates current state in asynchronous fashion. Actions associated with deactivating the current state /// will be invoked. The deactivation is idempotent and subsequent deactivation of the same current state /// will not lead to re-execution of deactivation callbacks. /// @@ -256,7 +256,7 @@ private async Task HandleTransitioningTriggerAsync(object[] args, StateRepresent State = representation.UnderlyingState; } - await _onTransitionCompletedEvent.InvokeAsync(new Transition(transition.Source, State, transition.Trigger, transition.Parameters)); + await _onTransitionCompletedEvent.InvokeAsync(new Transition(transition.Source, State, transition.Trigger, transition.Parameters)); } diff --git a/src/Stateless/StateRepresentation.cs b/src/Stateless/StateRepresentation.cs index c08f797f..bee126e4 100644 --- a/src/Stateless/StateRepresentation.cs +++ b/src/Stateless/StateRepresentation.cs @@ -230,7 +230,7 @@ internal void InternalAction(Transition transition, object[] args) { InternalTriggerBehaviour.Sync internalTransition = null; - // Look for actions in superstate(s) recursivly until we hit the topmost superstate, or we actually find some trigger handlers. + // Look for actions in superstate(s) recursively until we hit the topmost superstate, or we actually find some trigger handlers. StateRepresentation aStateRep = this; while (aStateRep != null) { diff --git a/test/Stateless.Tests/InternalTransitionFixture.cs b/test/Stateless.Tests/InternalTransitionFixture.cs index f54fb3a0..fac13f62 100644 --- a/test/Stateless.Tests/InternalTransitionFixture.cs +++ b/test/Stateless.Tests/InternalTransitionFixture.cs @@ -8,7 +8,7 @@ public class InternalTransitionFixture { /// - /// The expected behaviour of the internal transistion is that the state does not change. + /// The expected behaviour of the internal transition is that the state does not change. /// This will fail if the state changes after the trigger has fired. /// [Fact] From fa6bb4ecd1996ae44e4d39d491a41bcb4c3c7c02 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Tue, 25 Apr 2023 13:25:35 +0100 Subject: [PATCH 24/43] Remove redundant parentheses --- example/JsonExample/Member.cs | 2 +- src/Stateless/Graph/StateGraph.cs | 8 ++++---- src/Stateless/Graph/UmlDotGraphStyle.cs | 4 ++-- src/Stateless/Reflection/FixedTransitionInfo.cs | 2 +- src/Stateless/Reflection/IgnoredTransitionInfo.cs | 2 +- src/Stateless/Reflection/InvocationInfo.cs | 2 +- src/Stateless/Reflection/StateInfo.cs | 6 +++--- src/Stateless/StateMachine.Async.cs | 4 ++-- src/Stateless/StateMachine.cs | 4 ++-- src/Stateless/StateRepresentation.cs | 4 ++-- test/Stateless.Tests/ReflectionFixture.cs | 12 ++++++------ 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/example/JsonExample/Member.cs b/example/JsonExample/Member.cs index a3db89d8..4bc86d3d 100644 --- a/example/JsonExample/Member.cs +++ b/example/JsonExample/Member.cs @@ -81,7 +81,7 @@ public static Member FromJson(string jsonString) public bool Equals(Member anotherMember) { - return ((State == anotherMember.State) && (Name == anotherMember.Name)); + return State == anotherMember.State && Name == anotherMember.Name; } } diff --git a/src/Stateless/Graph/StateGraph.cs b/src/Stateless/Graph/StateGraph.cs index bf01ddff..74a2d9ca 100644 --- a/src/Stateless/Graph/StateGraph.cs +++ b/src/Stateless/Graph/StateGraph.cs @@ -69,7 +69,7 @@ public string ToGraph(GraphStyleBase style) // Next process all non-cluster states foreach (var state in States.Values) { - if ((state is SuperState) || (state is Decision) || (state.SuperState != null)) + if (state is SuperState || state is Decision || state.SuperState != null) continue; dirgraphText += style.FormatOneState(state).Replace("\n", System.Environment.NewLine); } @@ -115,8 +115,8 @@ void ProcessOnEntryFrom(StateMachineInfo machineInfo) // Does it have any incoming transitions that specify that trigger? foreach (var transit in state.Arriving) { - if ((transit.ExecuteEntryExitActions) - && (transit.Trigger.UnderlyingTrigger.ToString() == entryAction.FromTrigger)) + if (transit.ExecuteEntryExitActions + && transit.Trigger.UnderlyingTrigger.ToString() == entryAction.FromTrigger) { transit.DestinationEntryActions.Add(entryAction); } @@ -208,7 +208,7 @@ void AddSingleStates(StateMachineInfo machineInfo) /// void AddSuperstates(StateMachineInfo machineInfo) { - foreach (var stateInfo in machineInfo.States.Where(sc => (sc.Substates?.Count() > 0) && (sc.Superstate == null))) + foreach (var stateInfo in machineInfo.States.Where(sc => sc.Substates?.Count() > 0 && sc.Superstate == null)) { SuperState state = new SuperState(stateInfo); States[stateInfo.UnderlyingState.ToString()] = state; diff --git a/src/Stateless/Graph/UmlDotGraphStyle.cs b/src/Stateless/Graph/UmlDotGraphStyle.cs index 0d47b17d..b21f0599 100644 --- a/src/Stateless/Graph/UmlDotGraphStyle.cs +++ b/src/Stateless/Graph/UmlDotGraphStyle.cs @@ -33,7 +33,7 @@ public override string FormatOneCluster(SuperState stateInfo) StringBuilder label = new StringBuilder($"{sourceName}"); - if ((stateInfo.EntryActions.Count > 0) || (stateInfo.ExitActions.Count > 0)) + if (stateInfo.EntryActions.Count > 0 || stateInfo.ExitActions.Count > 0) { label.Append("\\n----------"); label.Append(string.Concat(stateInfo.EntryActions.Select(act => "\\nentry / " + act))); @@ -62,7 +62,7 @@ public override string FormatOneCluster(SuperState stateInfo) /// public override string FormatOneState(State state) { - if ((state.EntryActions.Count == 0) && (state.ExitActions.Count == 0)) + if (state.EntryActions.Count == 0 && state.ExitActions.Count == 0) return $"\"{state.StateName}\" [label=\"{state.StateName}\"];\n"; string f = $"\"{state.StateName}\" [label=\"{state.StateName}|"; diff --git a/src/Stateless/Reflection/FixedTransitionInfo.cs b/src/Stateless/Reflection/FixedTransitionInfo.cs index 72530186..0aed3ec9 100644 --- a/src/Stateless/Reflection/FixedTransitionInfo.cs +++ b/src/Stateless/Reflection/FixedTransitionInfo.cs @@ -14,7 +14,7 @@ internal static FixedTransitionInfo Create(StateMachine() : behaviour.Guard.Conditions.Select(c => c.MethodDescription) }; diff --git a/src/Stateless/Reflection/IgnoredTransitionInfo.cs b/src/Stateless/Reflection/IgnoredTransitionInfo.cs index ca41bbb1..6e669950 100644 --- a/src/Stateless/Reflection/IgnoredTransitionInfo.cs +++ b/src/Stateless/Reflection/IgnoredTransitionInfo.cs @@ -13,7 +13,7 @@ internal static IgnoredTransitionInfo Create(StateMachine() : behaviour.Guard.Conditions.Select(c => c.MethodDescription) }; diff --git a/src/Stateless/Reflection/InvocationInfo.cs b/src/Stateless/Reflection/InvocationInfo.cs index 4693aae9..eeb88642 100644 --- a/src/Stateless/Reflection/InvocationInfo.cs +++ b/src/Stateless/Reflection/InvocationInfo.cs @@ -72,6 +72,6 @@ public string Description /// /// Returns true if the method is invoked asynchronously. /// - public bool IsAsync => (_timing == Timing.Asynchronous); + public bool IsAsync => _timing == Timing.Asynchronous; } } diff --git a/src/Stateless/Reflection/StateInfo.cs b/src/Stateless/Reflection/StateInfo.cs index e98e45bb..468f32e8 100644 --- a/src/Stateless/Reflection/StateInfo.cs +++ b/src/Stateless/Reflection/StateInfo.cs @@ -51,18 +51,18 @@ internal static void AddRelationships(StateInfo info, StateMac foreach (var triggerBehaviours in stateRepresentation.TriggerBehaviours) { // First add all the deterministic transitions - foreach (var item in triggerBehaviours.Value.Where(behaviour => (behaviour is StateMachine.TransitioningTriggerBehaviour))) + foreach (var item in triggerBehaviours.Value.Where(behaviour => behaviour is StateMachine.TransitioningTriggerBehaviour)) { var destinationInfo = lookupState(((StateMachine.TransitioningTriggerBehaviour)item).Destination); fixedTransitions.Add(FixedTransitionInfo.Create(item, destinationInfo)); } - foreach (var item in triggerBehaviours.Value.Where(behaviour => (behaviour is StateMachine.ReentryTriggerBehaviour))) + foreach (var item in triggerBehaviours.Value.Where(behaviour => behaviour is StateMachine.ReentryTriggerBehaviour)) { var destinationInfo = lookupState(((StateMachine.ReentryTriggerBehaviour)item).Destination); fixedTransitions.Add(FixedTransitionInfo.Create(item, destinationInfo)); } //Then add all the internal transitions - foreach (var item in triggerBehaviours.Value.Where(behaviour => (behaviour is StateMachine.InternalTriggerBehaviour))) + foreach (var item in triggerBehaviours.Value.Where(behaviour => behaviour is StateMachine.InternalTriggerBehaviour)) { var destinationInfo = lookupState(stateRepresentation.UnderlyingState); fixedTransitions.Add(FixedTransitionInfo.Create(item, destinationInfo)); diff --git a/src/Stateless/StateMachine.Async.cs b/src/Stateless/StateMachine.Async.cs index 4d8b0151..c35e64ed 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -188,8 +188,8 @@ async Task InternalFireOneAsync(TTrigger trigger, params object[] args) await HandleReentryTriggerAsync(args, representativeState, transition); break; } - case DynamicTriggerBehaviour _ when (result.Handler.ResultsInTransitionFrom(source, args, out var destination)): - case TransitioningTriggerBehaviour _ when (result.Handler.ResultsInTransitionFrom(source, args, out destination)): + case DynamicTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination): + case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out destination): { // Handle transition, and set new state var transition = new Transition(source, destination, trigger, args); diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 57ef4c50..95f640bf 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -418,8 +418,8 @@ void InternalFireOne(TTrigger trigger, params object[] args) HandleReentryTrigger(args, representativeState, transition); break; } - case DynamicTriggerBehaviour _ when (result.Handler.ResultsInTransitionFrom(source, args, out var destination)): - case TransitioningTriggerBehaviour _ when (result.Handler.ResultsInTransitionFrom(source, args, out destination)): + case DynamicTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination): + case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out destination): { // Handle transition, and set new state var transition = new Transition(source, destination, trigger, args); diff --git a/src/Stateless/StateRepresentation.cs b/src/Stateless/StateRepresentation.cs index c08f797f..c372172c 100644 --- a/src/Stateless/StateRepresentation.cs +++ b/src/Stateless/StateRepresentation.cs @@ -47,8 +47,8 @@ public bool TryFindHandler(TTrigger trigger, object[] args, out TriggerBehaviour { TriggerBehaviourResult superStateHandler = null; - bool handlerFound = (TryFindLocalHandler(trigger, args, out TriggerBehaviourResult localHandler) || - (Superstate != null && Superstate.TryFindHandler(trigger, args, out superStateHandler))); + bool handlerFound = TryFindLocalHandler(trigger, args, out TriggerBehaviourResult localHandler) || + (Superstate != null && Superstate.TryFindHandler(trigger, args, out superStateHandler)); // If no handler for super state, replace by local handler (see issue #398) handler = superStateHandler ?? localHandler; diff --git a/test/Stateless.Tests/ReflectionFixture.cs b/test/Stateless.Tests/ReflectionFixture.cs index 2726760b..aebe54e9 100644 --- a/test/Stateless.Tests/ReflectionFixture.cs +++ b/test/Stateless.Tests/ReflectionFixture.cs @@ -587,7 +587,7 @@ void VerifyMethodNames(IEnumerable methods, string prefix, strin InvocationInfo method = methods.First(); if (state == State.A) - Assert.Equal(prefix + body + ((timing == InvocationInfo.Timing.Asynchronous) ? "Async" : ""), method.Description); + Assert.Equal(prefix + body + (timing == InvocationInfo.Timing.Asynchronous ? "Async" : ""), method.Description); else if (state == State.B) Assert.Equal(UserDescription + "B-" + body, method.Description); else if (state == State.C) @@ -612,15 +612,15 @@ void VerifyMethodNameses(IEnumerable methods, string prefix, str { if (state == State.A) { - matches = (method.Description == (prefix + body - + ((timing == InvocationInfo.Timing.Asynchronous) ? "Async" : "" + suffix))); + matches = method.Description == prefix + body + + (timing == InvocationInfo.Timing.Asynchronous ? "Async" : "" + suffix); } else if (state == State.B) - matches = (UserDescription + "B-" + body + suffix == method.Description); + matches = UserDescription + "B-" + body + suffix == method.Description; else if (state == State.C) - matches = (InvocationInfo.DefaultFunctionDescription == method.Description); + matches = InvocationInfo.DefaultFunctionDescription == method.Description; else if (state == State.D) - matches = (UserDescription + "D-" + body + suffix == method.Description); + matches = UserDescription + "D-" + body + suffix == method.Description; // if (matches) { From 3408c7d693c03cd726f2a2247819a485ef9a4cd8 Mon Sep 17 00:00:00 2001 From: Pent Ploompuu Date: Tue, 25 Apr 2023 21:51:32 +0300 Subject: [PATCH 25/43] Remove obsolete TargetFrameworks --- .../ParameterConversionResources.Designer.cs | 2 +- src/Stateless/Reflection/InvocationInfo.cs | 2 +- src/Stateless/ReflectionExtensions.cs | 53 ------------------- .../StateConfigurationResources.Designer.cs | 2 +- src/Stateless/StateMachine.cs | 2 - .../StateMachineResources.Designer.cs | 2 +- .../StateRepresentationResources.Designer.cs | 2 +- src/Stateless/Stateless.csproj | 8 +-- 8 files changed, 7 insertions(+), 66 deletions(-) delete mode 100644 src/Stateless/ReflectionExtensions.cs diff --git a/src/Stateless/ParameterConversionResources.Designer.cs b/src/Stateless/ParameterConversionResources.Designer.cs index 840edf10..4d39085f 100644 --- a/src/Stateless/ParameterConversionResources.Designer.cs +++ b/src/Stateless/ParameterConversionResources.Designer.cs @@ -37,7 +37,7 @@ internal ParameterConversionResources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.ParameterConversionResources", typeof(ParameterConversionResources).GetAssembly()); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.ParameterConversionResources", typeof(ParameterConversionResources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/Stateless/Reflection/InvocationInfo.cs b/src/Stateless/Reflection/InvocationInfo.cs index 4693aae9..222b4b7b 100644 --- a/src/Stateless/Reflection/InvocationInfo.cs +++ b/src/Stateless/Reflection/InvocationInfo.cs @@ -24,7 +24,7 @@ public enum Timing internal static InvocationInfo Create(Delegate method, string description, Timing timing = Timing.Synchronous) { - return new InvocationInfo(method?.TryGetMethodName(), description, timing); + return new InvocationInfo(method?.Method?.Name, description, timing); } /// diff --git a/src/Stateless/ReflectionExtensions.cs b/src/Stateless/ReflectionExtensions.cs deleted file mode 100644 index 191c520d..00000000 --- a/src/Stateless/ReflectionExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Reflection; - -namespace Stateless -{ - internal static class ReflectionExtensions - { - public static Assembly GetAssembly(this Type type) - { -#if PORTABLE_REFLECTION - return type.GetTypeInfo().Assembly; -#else - return type.Assembly; -#endif - } - public static bool IsAssignableFrom(this Type type, Type otherType) - { -#if PORTABLE_REFLECTION - return type.GetTypeInfo().IsAssignableFrom(otherType.GetTypeInfo()); -#else - return type.IsAssignableFrom(otherType); -#endif - } - - - - - - /// - /// Convenience method to get for different PCL profiles. - /// - /// Delegate whose method info is desired - /// Null if is null, otherwise . - public static MethodInfo TryGetMethodInfo(this Delegate del) - { -#if PORTABLE_REFLECTION - return del?.GetMethodInfo(); -#else - return del?.Method; -#endif - } - - /// - /// Convenience method to get method name for different PCL profiles. - /// - /// Delegate whose method name is desired - /// Null if is null, otherwise . - public static string TryGetMethodName(this Delegate del) - { - return TryGetMethodInfo(del)?.Name; - } - } -} \ No newline at end of file diff --git a/src/Stateless/StateConfigurationResources.Designer.cs b/src/Stateless/StateConfigurationResources.Designer.cs index 0d9894f8..2eb924f5 100644 --- a/src/Stateless/StateConfigurationResources.Designer.cs +++ b/src/Stateless/StateConfigurationResources.Designer.cs @@ -37,7 +37,7 @@ internal StateConfigurationResources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.StateConfigurationResources", typeof(StateConfigurationResources).GetAssembly()); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.StateConfigurationResources", typeof(StateConfigurationResources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 57ef4c50..c60c33bf 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -134,7 +134,6 @@ public IEnumerable GetPermittedTriggers(params object[] args) return CurrentRepresentation.GetPermittedTriggers(args); } -#if !NETSTANDARD1_0 /// /// Gets the currently-permissible triggers with any configured parameters. /// @@ -143,7 +142,6 @@ public IEnumerable> GetDetailedPermittedTrigger return CurrentRepresentation.GetPermittedTriggers(args) .Select(trigger => new TriggerDetails(trigger, _triggerConfiguration)); } -#endif StateRepresentation CurrentRepresentation { diff --git a/src/Stateless/StateMachineResources.Designer.cs b/src/Stateless/StateMachineResources.Designer.cs index d80571de..4fb03195 100644 --- a/src/Stateless/StateMachineResources.Designer.cs +++ b/src/Stateless/StateMachineResources.Designer.cs @@ -38,7 +38,7 @@ internal StateMachineResources() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.StateMachineResources", typeof(StateMachineResources).GetAssembly()); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.StateMachineResources", typeof(StateMachineResources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/Stateless/StateRepresentationResources.Designer.cs b/src/Stateless/StateRepresentationResources.Designer.cs index 9b262f2c..f3fdb295 100644 --- a/src/Stateless/StateRepresentationResources.Designer.cs +++ b/src/Stateless/StateRepresentationResources.Designer.cs @@ -37,7 +37,7 @@ internal StateRepresentationResources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.StateRepresentationResources", typeof(StateRepresentationResources).GetAssembly()); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stateless.StateRepresentationResources", typeof(StateRepresentationResources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/Stateless/Stateless.csproj b/src/Stateless/Stateless.csproj index 7205a4ef..5bd6d5fd 100644 --- a/src/Stateless/Stateless.csproj +++ b/src/Stateless/Stateless.csproj @@ -4,7 +4,7 @@ Stateless Stateless Stateless - netstandard2.0;netstandard1.0;net45;net40;net472;net5.0;net6.0 + netstandard2.0;net45;net472;net6.0 Create state machines and lightweight state machine-based workflows directly in .NET code Copyright © Stateless Contributors 2009-$([System.DateTime]::Now.ToString(yyyy)) en-US @@ -28,11 +28,7 @@ snupkg - - $(DefineConstants);PORTABLE_REFLECTION;TASKS - - - + $(DefineConstants);TASKS From 3b64d8f8fb07c09e11963eacd999330313b4fb34 Mon Sep 17 00:00:00 2001 From: Pent Ploompuu Date: Tue, 25 Apr 2023 23:03:10 +0300 Subject: [PATCH 26/43] Replace net45 and net472 with net462 --- src/Stateless/Stateless.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stateless/Stateless.csproj b/src/Stateless/Stateless.csproj index 5bd6d5fd..d1b0b97f 100644 --- a/src/Stateless/Stateless.csproj +++ b/src/Stateless/Stateless.csproj @@ -4,7 +4,7 @@ Stateless Stateless Stateless - netstandard2.0;net45;net472;net6.0 + netstandard2.0;net462;net6.0 Create state machines and lightweight state machine-based workflows directly in .NET code Copyright © Stateless Contributors 2009-$([System.DateTime]::Now.ToString(yyyy)) en-US From 5bef9ad00d2b0c225ea1f8a18ff75401f10c8124 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Sat, 29 Apr 2023 10:06:08 +0100 Subject: [PATCH 27/43] Remove the two tests that do not prove that the sync context is lost to avoid confusion. --- .../SynchronizationContextFixture.cs | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index cd955114..bacc8e1e 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -81,22 +81,6 @@ public async Task Ensure_XUnit_can_lose_sync_context() Assert.NotEqual(_customSynchronizationContext, SynchronizationContext.Current); } - [Fact] - public async Task Single_activation_should_retain_SyncContext() - { - // ARRANGE - SetSyncContext(); - var sm = GetSut(); - sm.Configure(State.A) - .OnActivateAsync(CaptureThenLoseSyncContext); - - // ACT - await sm.ActivateAsync(); - - // ASSERT - AssertSyncContextAlwaysRetained(1); - } - [Fact] public async Task Activation_of_state_with_superstate_should_retain_SyncContext() { @@ -173,23 +157,6 @@ public async Task Multiple_Deactivations_should_retain_SyncContext() AssertSyncContextAlwaysRetained(3); } - [Fact] - public async Task OnEntry_should_retain_SyncContext() - { - // ARRANGE - SetSyncContext(); - var sm = GetSut(); - sm.Configure(State.A).Permit(Trigger.X, State.B); - sm.Configure(State.B) - .OnEntryAsync(CaptureThenLoseSyncContext); - - // ACT - await sm.FireAsync(Trigger.X); - - // ASSERT - AssertSyncContextAlwaysRetained(1); - } - [Fact] public async Task Multiple_OnEntry_should_retain_SyncContext() { From 7a84e2be4e44f005a001c3e730b5b24f63b84f96 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Sat, 29 Apr 2023 13:03:36 +0100 Subject: [PATCH 28/43] Bump solution to VS 2022 --- Stateless.sln | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Stateless.sln b/Stateless.sln index be9741b1..8820c908 100644 --- a/Stateless.sln +++ b/Stateless.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29505.145 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{8DE7A8AE-D87D-46A0-9757-88BA4AF7EDA5}" ProjectSection(SolutionItems) = preProject From 1c3635677faf4d04d311081f35f3c77463a5e0cd Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Sat, 29 Apr 2023 21:55:38 +0100 Subject: [PATCH 29/43] There is a race condition where Task.Delay could theoretically finish synchronously. Change to a mechanism that ensures a sync context change. --- test/Stateless.Tests/SynchronizationContextFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index bacc8e1e..26eaccf6 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -42,7 +42,7 @@ private void CaptureSyncContext() private async Task LoseSyncContext() { - await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false); // Switch synchronization context and continue + await Task.Run(() => { }).ConfigureAwait(false); // Switch synchronization context and continue Assert.NotEqual(_customSynchronizationContext, SynchronizationContext.Current); } From 24738724bbb5292cff6aa12aef96427dbc2a8fc6 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Sun, 30 Apr 2023 06:45:32 +0100 Subject: [PATCH 30/43] Use Task.Yield instead --- test/Stateless.Tests/SynchronizationContextFixture.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index 26eaccf6..3778c5cd 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -42,8 +42,7 @@ private void CaptureSyncContext() private async Task LoseSyncContext() { - await Task.Run(() => { }).ConfigureAwait(false); // Switch synchronization context and continue - Assert.NotEqual(_customSynchronizationContext, SynchronizationContext.Current); + await Task.Yield(); } /// From 860de43291b8b6e136c0f51591d89320882df908 Mon Sep 17 00:00:00 2001 From: Lee Oades Date: Sun, 30 Apr 2023 06:45:32 +0100 Subject: [PATCH 31/43] Revert "Use Task.Yield instead" This reverts commit 24738724bbb5292cff6aa12aef96427dbc2a8fc6. --- test/Stateless.Tests/SynchronizationContextFixture.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index 3778c5cd..26eaccf6 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -42,7 +42,8 @@ private void CaptureSyncContext() private async Task LoseSyncContext() { - await Task.Yield(); + await Task.Run(() => { }).ConfigureAwait(false); // Switch synchronization context and continue + Assert.NotEqual(_customSynchronizationContext, SynchronizationContext.Current); } /// From c5ad66382cb76e8cf933fe93aaf1d5567ce3f27f Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 2 Jun 2023 16:24:13 +0100 Subject: [PATCH 32/43] Restore and deprecate old InternalTransitionAsyncIf signatures. --- src/Stateless/InternalTriggerBehaviour.cs | 17 ++- src/Stateless/StateConfiguration.Async.cs | 96 +++++++++++- .../InternalTransitionAsyncFixture.cs | 144 +++++++++++++++++- 3 files changed, 245 insertions(+), 12 deletions(-) diff --git a/src/Stateless/InternalTriggerBehaviour.cs b/src/Stateless/InternalTriggerBehaviour.cs index 8885bd91..3966a945 100644 --- a/src/Stateless/InternalTriggerBehaviour.cs +++ b/src/Stateless/InternalTriggerBehaviour.cs @@ -20,8 +20,7 @@ public override bool ResultsInTransitionFrom(TState source, object[] args, out T return false; } - - public class Sync: InternalTriggerBehaviour + public class Sync : InternalTriggerBehaviour { public Action InternalAction { get; } @@ -29,6 +28,7 @@ public class Sync: InternalTriggerBehaviour { InternalAction = internalAction; } + public override void Execute(Transition transition, object[] args) { InternalAction(transition, args); @@ -45,7 +45,13 @@ public class Async : InternalTriggerBehaviour { readonly Func InternalAction; - public Async(TTrigger trigger, Func guard,Func internalAction, string guardDescription = null) : base(trigger, new TransitionGuard(guard, guardDescription)) + public Async(TTrigger trigger, Func guard, Func internalAction, string guardDescription = null) : base(trigger, new TransitionGuard(guard, guardDescription)) + { + InternalAction = internalAction; + } + + [Obsolete] + public Async(TTrigger trigger, Func guard, Func internalAction, string guardDescription = null) : base(trigger, new TransitionGuard(guard, guardDescription)) { InternalAction = internalAction; } @@ -61,10 +67,7 @@ public override Task ExecuteAsync(Transition transition, object[] args) { return InternalAction(transition, args); } - } - - } } -} \ No newline at end of file +} diff --git a/src/Stateless/StateConfiguration.Async.cs b/src/Stateless/StateConfiguration.Async.cs index 93fac07b..53a8c918 100644 --- a/src/Stateless/StateConfiguration.Async.cs +++ b/src/Stateless/StateConfiguration.Async.cs @@ -9,6 +9,98 @@ public partial class StateMachine { public partial class StateConfiguration { + /// + /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine + /// + /// + /// The accepted trigger + /// Function that must return true in order for the trigger to be accepted. + /// The asynchronous action performed by the internal transition + /// + [Obsolete("Use InternalTransitionAsyncIf(TTrigger, Func, Func) instead.")] + public StateConfiguration InternalTransitionAsyncIf(TTrigger trigger, Func guard, Func internalAction) + { + if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); + + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger, guard, (t, args) => internalAction(t))); + return this; + } + + /// + /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine + /// + /// + /// The accepted trigger + /// Function that must return true in order for the trigger to be accepted. + /// The asynchronous action performed by the internal transition + /// + [Obsolete("Use InternalTransitionAsyncIf(TriggerWithParameters, Func, Func) instead.")] + public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); + + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, guard, (t, args) => internalAction(ParameterConversion.Unpack(args, 0), t))); + return this; + } + + /// + /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine + /// + /// + /// + /// The accepted trigger + /// Function that must return true in order for the trigger to be accepted. + /// The asynchronous action performed by the internal transition + /// + [Obsolete("Use InternalTransitionAsyncIf(TriggerWithParameters, Func, Func) instead.")] + public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); + + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, guard, (t, args) => internalAction( + ParameterConversion.Unpack(args, 0), + ParameterConversion.Unpack(args, 1), t))); + return this; + } + + /// + /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine + /// + /// + /// + /// + /// The accepted trigger + /// Function that must return true in order for the trigger to be accepted. + /// The asynchronous action performed by the internal transition + /// + [Obsolete("Use InternalTransitionAsyncIf(TriggerWithParameters, Func, Func) instead.")] + public StateConfiguration InternalTransitionAsyncIf(TriggerWithParameters trigger, Func guard, Func internalAction) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + if (internalAction == null) throw new ArgumentNullException(nameof(internalAction)); + + _representation.AddTriggerBehaviour(new InternalTriggerBehaviour.Async(trigger.Trigger, guard, (t, args) => internalAction( + ParameterConversion.Unpack(args, 0), + ParameterConversion.Unpack(args, 1), + ParameterConversion.Unpack(args, 2), t))); + return this; + } + + /// + /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine + /// + /// + /// The accepted trigger + /// The asynchronous action performed by the internal transition + /// + [Obsolete("Use InternalTransitionAsync(TTrigger, Func) instead.")] + public StateConfiguration InternalTransitionAsync(TTrigger trigger, Func internalAction) + { + return InternalTransitionAsyncIf(trigger, () => true, internalAction); + } + /// /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine /// @@ -98,7 +190,6 @@ public StateConfiguration InternalTransitionAsyncIf(Trigger return this; } - /// /// Add an internal transition to the state machine. An internal action does not cause the Exit and Entry actions to be triggered, and does not change the state of the state machine /// @@ -203,7 +294,6 @@ public StateConfiguration OnEntryAsync(Func entryAction, string entryActio (t, args) => entryAction(), Reflection.InvocationInfo.Create(entryAction, entryActionDescription, Reflection.InvocationInfo.Timing.Asynchronous)); return this; - } /// @@ -435,4 +525,4 @@ public StateConfiguration OnExitAsync(Func exitAction, string } } } -#endif \ No newline at end of file +#endif diff --git a/test/Stateless.Tests/InternalTransitionAsyncFixture.cs b/test/Stateless.Tests/InternalTransitionAsyncFixture.cs index df2092a7..1cc9e68e 100644 --- a/test/Stateless.Tests/InternalTransitionAsyncFixture.cs +++ b/test/Stateless.Tests/InternalTransitionAsyncFixture.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Xunit; namespace Stateless.Tests @@ -97,7 +98,146 @@ public async Task InternalTransitionAsyncIf_AllowGuardWithThreeParameters() Assert.True(guardInvoked); Assert.True(callbackInvoked); } - + + [Fact] + [Obsolete] + public async Task InternalTransitionAsyncIf_DeprecatedOverload_AllowGuardWithoutParameter() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + const int intParam = 5; + var guardInvoked = false; + var callbackInvoked = false; + + sm.Configure(State.A) + .InternalTransitionAsyncIf(trigger, () => + { + guardInvoked = true; + return true; + }, (i, transition) => + { + callbackInvoked = true; + Assert.Equal(intParam, i); + return Task.CompletedTask; + }); + + await sm.FireAsync(trigger, intParam); + + Assert.True(guardInvoked); + Assert.True(callbackInvoked); + } + + [Fact] + [Obsolete] + public async Task InternalTransitionAsyncIf_DeprecatedOverload_AllowGuardWithParameter() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + const int intParam = 5; + var guardInvoked = false; + var callbackInvoked = false; + + sm.Configure(State.A) + .InternalTransitionAsyncIf(trigger, () => + { + guardInvoked = true; + return true; + }, (i, transition) => + { + callbackInvoked = true; + Assert.Equal(intParam, i); + return Task.CompletedTask; + }); + + await sm.FireAsync(trigger, intParam); + + Assert.True(guardInvoked); + Assert.True(callbackInvoked); + } + + [Fact] + [Obsolete] + public async Task InternalTransitionAsyncIf_DeprecatedOverload_AllowGuardWithTwoParameters() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + const int intParam = 5; + const string stringParam = "5"; + var guardInvoked = false; + var callbackInvoked = false; + + sm.Configure(State.A) + .InternalTransitionAsyncIf(trigger, () => + { + guardInvoked = true; + return true; + }, (i, s, transition) => + { + callbackInvoked = true; + Assert.Equal(intParam, i); + Assert.Equal(stringParam, s); + return Task.CompletedTask; + }); + + await sm.FireAsync(trigger, intParam, stringParam); + + Assert.True(guardInvoked); + Assert.True(callbackInvoked); + } + + [Fact] + [Obsolete] + public async Task InternalTransitionAsyncIf_DeprecatedOverload_AllowGuardWithThreeParameters() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + const int intParam = 5; + const string stringParam = "5"; + const bool boolParam = true; + var guardInvoked = false; + var callbackInvoked = false; + + sm.Configure(State.A) + .InternalTransitionAsyncIf(trigger, () => + { + guardInvoked = true; + return true; + }, (i, s, b, transition) => + { + callbackInvoked = true; + Assert.Equal(intParam, i); + Assert.Equal(stringParam, s); + Assert.Equal(boolParam, b); + return Task.CompletedTask; + }); + + await sm.FireAsync(trigger, intParam, stringParam, boolParam); + + Assert.True(guardInvoked); + Assert.True(callbackInvoked); + } + + [Fact] + [Obsolete] + public async Task InternalTransitionAsyncIf_DeprecatedOverload_GuardExecutedOnlyOnce() + { + var guardCalls = 0; + var order = new Order + { + Status = OrderStatus.OrderPlaced, + PaymentStatus = PaymentStatus.Pending, + }; + var stateMachine = new StateMachine(order.Status); + stateMachine.Configure(OrderStatus.OrderPlaced) + .InternalTransitionAsyncIf(OrderStateTrigger.PaymentCompleted, + () => PreCondition(ref guardCalls), + _ => ChangePaymentState(order, PaymentStatus.Completed)); + + await stateMachine.FireAsync(OrderStateTrigger.PaymentCompleted); + + Assert.Equal(1, guardCalls); + } + /// /// This unit test demonstrated bug report #417 /// From 2ab545b3eca3af1533ef19460ecf38503b91176e Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 2 Jun 2023 17:59:35 +0100 Subject: [PATCH 33/43] bugfix: Execute OnEntryFromAsync actions asynchronously --- src/Stateless/EntryActionBehaviour.cs | 9 ++++-- test/Stateless.Tests/AsyncActionsFixture.cs | 36 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Stateless/EntryActionBehaviour.cs b/src/Stateless/EntryActionBehaviour.cs index 280452ef..5fb38275 100644 --- a/src/Stateless/EntryActionBehaviour.cs +++ b/src/Stateless/EntryActionBehaviour.cs @@ -95,13 +95,16 @@ public AsyncFrom(TTriggerType trigger, Func action, public override void Execute(Transition transition, object[] args) { - if (transition.Trigger.Equals(Trigger)) - base.Execute(transition, args); + ExecuteAsync(transition, args); } public override Task ExecuteAsync(Transition transition, object[] args) { - Execute(transition, args); + if (transition.Trigger.Equals(Trigger)) + { + return base.ExecuteAsync(transition, args); + } + return TaskResult.Done; } } diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 4e9b63ba..f44eac22 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -472,6 +472,42 @@ public void VerifyNotEnterSuperstateWhenDoingInitialTransition() Assert.Equal(State.D, sm.State); } + + [Fact] + public async Task OnEntryFromAsync_WhenTriggered_InvokesAction() + { + bool wasInvoked = false; + + var sm = new StateMachine(State.A); + + sm.Configure(State.A).Permit(Trigger.X, State.B); + + sm.Configure(State.B) + .OnEntryFromAsync(Trigger.X, async () => await Task.Run(() => { wasInvoked = true; })); + + await sm.FireAsync(Trigger.X); + + Assert.True(wasInvoked); + } + + [Fact] + public async Task OnEntryFromAsync_WhenEnteringByAnotherTrigger_InvokesAction() + { + bool wasInvoked = false; + + var sm = new StateMachine(State.A); + + sm.Configure(State.A) + .Permit(Trigger.X, State.B) + .Permit(Trigger.Y, State.B); ; + + sm.Configure(State.B) + .OnEntryFromAsync(Trigger.X, async () => await Task.Run(() => { wasInvoked = true; })); + + await sm.FireAsync(Trigger.Y); + + Assert.False(wasInvoked); + } } } From 5bcedd687aa3043fd807f254fdd137adc0f52f80 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 2 Jun 2023 18:20:29 +0100 Subject: [PATCH 34/43] Throw InvalidOperationException when firing async action synchronously. --- src/Stateless/EntryActionBehaviour.cs | 5 ++- test/Stateless.Tests/AsyncActionsFixture.cs | 34 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Stateless/EntryActionBehaviour.cs b/src/Stateless/EntryActionBehaviour.cs index 5fb38275..8023cebc 100644 --- a/src/Stateless/EntryActionBehaviour.cs +++ b/src/Stateless/EntryActionBehaviour.cs @@ -95,7 +95,10 @@ public AsyncFrom(TTriggerType trigger, Func action, public override void Execute(Transition transition, object[] args) { - ExecuteAsync(transition, args); + if (transition.Trigger.Equals(Trigger)) + { + base.Execute(transition, args); + } } public override Task ExecuteAsync(Transition transition, object[] args) diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index f44eac22..e8dd5dba 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -473,6 +473,19 @@ public void VerifyNotEnterSuperstateWhenDoingInitialTransition() Assert.Equal(State.D, sm.State); } + [Fact] + public void OnEntryFromAsync_WhenTriggeredSynchronously_Throws() + { + var sm = new StateMachine(State.A); + + sm.Configure(State.A).Permit(Trigger.X, State.B); + + sm.Configure(State.B) + .OnEntryFromAsync(Trigger.X, async () => await Task.Run(() => { })); + + Assert.Throws(() => sm.Fire(Trigger.X)); + } + [Fact] public async Task OnEntryFromAsync_WhenTriggered_InvokesAction() { @@ -490,6 +503,25 @@ public async Task OnEntryFromAsync_WhenTriggered_InvokesAction() Assert.True(wasInvoked); } + [Fact] + public void OnEntryFromAsync_WhenEnteringByAnotherTriggerSynchronously_DoesNotThrow() + { + bool wasInvoked = false; + + var sm = new StateMachine(State.A); + + sm.Configure(State.A) + .Permit(Trigger.X, State.B) + .Permit(Trigger.Y, State.B); + + sm.Configure(State.B) + .OnEntryFromAsync(Trigger.X, async () => await Task.Run(() => { wasInvoked = true; })); + + sm.Fire(Trigger.Y); + + Assert.False(wasInvoked); + } + [Fact] public async Task OnEntryFromAsync_WhenEnteringByAnotherTrigger_InvokesAction() { @@ -499,7 +531,7 @@ public async Task OnEntryFromAsync_WhenEnteringByAnotherTrigger_InvokesAction() sm.Configure(State.A) .Permit(Trigger.X, State.B) - .Permit(Trigger.Y, State.B); ; + .Permit(Trigger.Y, State.B); sm.Configure(State.B) .OnEntryFromAsync(Trigger.X, async () => await Task.Run(() => { wasInvoked = true; })); From 63c8d51fc45454bb94810a63617514e43de62fba Mon Sep 17 00:00:00 2001 From: DeepakParamkusam Date: Fri, 14 Jul 2023 17:31:26 +0200 Subject: [PATCH 35/43] Added FireAsync(TriggerWithParameters, params object[]) overload This is async counterpart to existing Fire(TriggerWithParameters, params object[]) method. Required for firing triggers with more than 3 parameters asynchronously. --- src/Stateless/StateMachine.Async.cs | 17 ++++++++++++++ test/Stateless.Tests/AsyncActionsFixture.cs | 25 +++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/Stateless/StateMachine.Async.cs b/src/Stateless/StateMachine.Async.cs index b319813a..c4e61e98 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -46,6 +46,23 @@ public Task FireAsync(TTrigger trigger) return InternalFireAsync(trigger, new object[0]); } + /// + /// Transition from the current state via the specified trigger in async fashion. + /// The target state is determined by the configuration of the current state. + /// Actions associated with leaving the current state and entering the new one + /// will be invoked. + /// + /// The trigger to fire. + /// A variable-length parameters list containing arguments. + /// The current state does + /// not allow the trigger to be fired. + public Task FireAsync(TriggerWithParameters trigger, params object[] args) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + + return InternalFireAsync(trigger.Trigger, args); + } + /// /// Transition from the current state via the specified trigger in async fashion. /// The target state is determined by the configuration of the current state. diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index e8dd5dba..79ff95f5 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -540,6 +540,31 @@ public async Task OnEntryFromAsync_WhenEnteringByAnotherTrigger_InvokesAction() Assert.False(wasInvoked); } + + [Fact] + public async Task FireAsync_TriggerWithMoreThanThreeParameters() + { + const string expectedParam = "42-Stateless-True-420.69-Y"; + string actualParam = null; + + var sm = new StateMachine(State.A); + + sm.Configure(State.A) + .Permit(Trigger.X, State.B); + + sm.Configure(State.B) + .OnEntryAsync(t => + { + actualParam = string.Join("-", t.Parameters); + return Task.CompletedTask; + }); + + var parameterizedX = sm.SetTriggerParameters(Trigger.X, typeof(int), typeof(string), typeof(bool), typeof(double), typeof(Trigger)); + + await sm.FireAsync(parameterizedX, 42, "Stateless", true, 420.69, Trigger.Y); + + Assert.Equal(expectedParam, actualParam); + } } } From f9b291b785674f683de9fb8ebe57a83e0ffe25fb Mon Sep 17 00:00:00 2001 From: Jay McGaffigan Date: Thu, 5 Oct 2023 09:26:38 -0400 Subject: [PATCH 36/43] In the case of a superstate having a trigger that moves the state to a substate, if the machine is already in that substate then the entry action shouldn't trigger again --- src/Stateless/StateMachine.cs | 4 ++++ test/Stateless.Tests/StateMachineFixture.cs | 23 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 270cc620..5d5a0e5f 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -423,6 +423,10 @@ void InternalFireOne(TTrigger trigger, params object[] args) case DynamicTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination): case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out destination): { + //If a trigger was found on a superstate that would cause unintended reentry, don't trigger. + if (source.Equals(destination)) + break; + // Handle transition, and set new state var transition = new Transition(source, destination, trigger, args); HandleTransitioningTrigger(args, representativeState, transition); diff --git a/test/Stateless.Tests/StateMachineFixture.cs b/test/Stateless.Tests/StateMachineFixture.cs index 5f2803b9..f89b8560 100644 --- a/test/Stateless.Tests/StateMachineFixture.cs +++ b/test/Stateless.Tests/StateMachineFixture.cs @@ -108,6 +108,29 @@ public void WhenInSubstate_TriggerIgnoredInSuperstate_RemainsInSubstate() Assert.Equal(State.B, sm.State); } + [Fact] + public void WhenInSubstate_TriggerSuperStateTwiceToSameSubstate_DoesNotReenterSubstate() + { + var sm = new StateMachine(State.A); + var eCount = 0; + + sm.Configure(State.B) + .OnEntry(() => { eCount++;}) + .SubstateOf(State.C); + + sm.Configure(State.A) + .SubstateOf(State.C); + + + sm.Configure(State.C) + .Permit(Trigger.X, State.B); + + sm.Fire(Trigger.X); + sm.Fire(Trigger.X); + + Assert.Equal(1, eCount); + } + [Fact] public void PermittedTriggersIncludeSuperstatePermittedTriggers() { From 1f9a302ef55cba1b456d7b1c3edec5c292bb0b74 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Sat, 7 Oct 2023 18:08:50 +1000 Subject: [PATCH 37/43] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d571caa..cca35def 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ phoneCall.Fire(Trigger.CallDialled); Assert.AreEqual(State.Ringing, phoneCall.State); ``` -This project, as well as the example above, was inspired by [Simple State Machine](http://simplestatemachine.codeplex.com/). +This project, as well as the example above, was inspired by [Simple State Machine (Archived)](https://web.archive.org/web/20170814020207/http://simplestatemachine.codeplex.com/). ## Features From 996bb50001a0a27378e2ee3cbb9e1ebf133f741e Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Wed, 8 Nov 2023 16:41:57 +0000 Subject: [PATCH 38/43] Release 5.14.0 --- CHANGELOG.md | 16 ++++++++++++++++ src/Stateless/Stateless.csproj | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07a1e42f..4d67f42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 5.14.0 - 2022.11.08 +### Added + - Enable Source Link & Deterministic Builds [#501] + - Added optional `RetainSynchronizationContext` property [#519] + - Update example apps to `net6.0` [#520] + - Bump solution Visual Studio version to 2022 [#526] + - Remove obsolete TargetFrameworks [#524] +### Fixed + - `StateMachineInfo.InitialState.Transitions` throws if `AddRelationships` not called [#514] + - Trigger information is missing for `OnEntryFromAsync` [#511] + - Fixed typos & redundant parentheses [#512], [#521], [#522] + - Change mechanism for losing the synchronization context [#528] + - `InvalidOperationException` thrown from call to `FireAsync` [#532] + - Added missing guard function parameter support from `InternalTransitionAsyncIf` [#530] + - Using `PermitIf` on a state with substates leads to reentry [#544] + ## 5.13.0 - 2022.12.29 ### Added - Add method to get permitted triggers with parameter information [#494] diff --git a/src/Stateless/Stateless.csproj b/src/Stateless/Stateless.csproj index d1b0b97f..0151d144 100644 --- a/src/Stateless/Stateless.csproj +++ b/src/Stateless/Stateless.csproj @@ -8,7 +8,7 @@ Create state machines and lightweight state machine-based workflows directly in .NET code Copyright © Stateless Contributors 2009-$([System.DateTime]::Now.ToString(yyyy)) en-US - 5.13.0 + 5.14.0 Stateless Contributors true true From d1f08d60ab4b15f5ea06ad9c3d5d508c4f6b682c Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Thu, 9 Nov 2023 12:22:32 +0000 Subject: [PATCH 39/43] Bump date in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d67f42e..a4d929df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 5.14.0 - 2022.11.08 +## 5.14.0 - 2022.11.09 ### Added - Enable Source Link & Deterministic Builds [#501] - Added optional `RetainSynchronizationContext` property [#519] From bdcf10fb7f2bff747eb253cb22921c5ce03f602a Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Thu, 9 Nov 2023 14:33:50 +0000 Subject: [PATCH 40/43] Fix typo in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d929df..16dd49da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 5.14.0 - 2022.11.09 +## 5.14.0 - 2023.11.09 ### Added - Enable Source Link & Deterministic Builds [#501] - Added optional `RetainSynchronizationContext` property [#519] From 3cf3106602345eb33471bb4218c70643de5bb393 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 10 Nov 2023 09:48:19 +0000 Subject: [PATCH 41/43] Bump date in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16dd49da..f14f684d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 5.14.0 - 2023.11.09 +## 5.14.0 - 2023.11.10 ### Added - Enable Source Link & Deterministic Builds [#501] - Added optional `RetainSynchronizationContext` property [#519] From e28496c6250441c266e0c46bcfffb30ef98bb016 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Mon, 13 Nov 2023 14:54:04 +0000 Subject: [PATCH 42/43] Added PR to ChangeLog; bumped date. --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f14f684d..96c96f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 5.14.0 - 2023.11.10 +## 5.14.0 - 2023.11.13 ### Added - Enable Source Link & Deterministic Builds [#501] - Added optional `RetainSynchronizationContext` property [#519] - Update example apps to `net6.0` [#520] - Bump solution Visual Studio version to 2022 [#526] - Remove obsolete TargetFrameworks [#524] + - Added `FireAsync(TriggerWithParameters, params object[])` overload [#536] ### Fixed - `StateMachineInfo.InitialState.Transitions` throws if `AddRelationships` not called [#514] - Trigger information is missing for `OnEntryFromAsync` [#511] From b936479df82110e7865f091f2d403d19c0869300 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Tue, 14 Nov 2023 07:16:44 +0000 Subject: [PATCH 43/43] Bump date in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c96f8b..ee89471e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 5.14.0 - 2023.11.13 +## 5.14.0 - 2023.11.14 ### Added - Enable Source Link & Deterministic Builds [#501] - Added optional `RetainSynchronizationContext` property [#519]