Skip to content

Commit

Permalink
Merge pull request #548 from dotnet-state-machine/dev
Browse files Browse the repository at this point in the history
Release 5.14.0
  • Loading branch information
mclift authored Nov 14, 2023
2 parents d5f6563 + b936479 commit 4a4ac06
Show file tree
Hide file tree
Showing 52 changed files with 1,477 additions and 223 deletions.
16 changes: 7 additions & 9 deletions .github/workflows/BuildAndTestOnPullRequests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,33 @@ 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
with:
dotnet-version: 3.1.101
- uses: actions/checkout@v3

- 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 }}
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ 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.14
### 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]
- 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]
Expand Down
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -225,11 +225,30 @@ 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<State, Trigger>(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.


## 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.
Expand Down
5 changes: 5 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 9 additions & 2 deletions Stateless.sln
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
175 changes: 175 additions & 0 deletions example/AlarmExample/Alarm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using AlarmExample;
using Stateless;
using System.Diagnostics;

namespace AlarmExample
{
/// <summary>
/// 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.
///
/// </summary>
public partial class Alarm
{
/// <summary>
/// Moves the Alarm into the provided <see cref="AlarmState" /> via the defined <see cref="AlarmCommand" />.
/// </summary>
/// <param name="command">The <see cref="AlarmCommand" /> to execute on the current <see cref="AlarmState" />.</param>
/// <returns>The new <see cref="AlarmState" />.</returns>
public AlarmState ExecuteTransition(AlarmCommand command)
{
if (_machine.CanFire(command))
{
_machine.Fire(command);
}
else
{
throw new InvalidOperationException($"Cannot transition from {CurrentState} via {command}");
}

return CurrentState();
}

/// <summary>
/// The current <see cref="AlarmState" /> of the alarm.
/// </summary>
public AlarmState CurrentState()
{
if (_machine != null)
return _machine.State;
else
throw new InvalidOperationException("Alarm hasn't been configured yet.");
}

/// <summary>
/// Defines whether the <see cref="Alarm"/> has been configured.
/// </summary>
public bool IsConfigured { get; private set; }

/// <summary>
/// Returns whether the provided command is a valid transition from the Current State.
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
public bool CanFireCommand(AlarmCommand command)
{
return _machine.CanFire(command);
}

/// <summary>
/// Default constructor.
/// </summary>
/// <param name="armDelay">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).</param>
/// <param name="pauseDelay">The time (in seconds) the alarm will spend in the
/// ArmPaused status before returning to Armed (if not transitioned to Triggered
/// via Trigger).</param>
/// <param name="triggerDelay">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).</param>
/// <param name="triggerTimeOut">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).</param>
public Alarm(int armDelay, int pauseDelay, int triggerDelay, int triggerTimeOut)
{
_machine = new StateMachine<AlarmState, AlarmCommand>(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<AlarmState, AlarmCommand>.Transition transition)
{
Trace.WriteLine($"Transitioned from {transition.Source} to " +
$"{transition.Destination} via {transition.Trigger}.");
}

private StateMachine<AlarmState, AlarmCommand> _machine;
private System.Timers.Timer? preArmTimer;
private System.Timers.Timer? pauseTimer;
private System.Timers.Timer? triggerDelayTimer;
private System.Timers.Timer? triggerTimeOutTimer;
}
}
13 changes: 13 additions & 0 deletions example/AlarmExample/AlarmCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace AlarmExample
{
public enum AlarmCommand
{
Startup,
Arm,
Disarm,
Trigger,
Acknowledge,
Pause,
TimeOut
}
}
14 changes: 14 additions & 0 deletions example/AlarmExample/AlarmExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Stateless\Stateless.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 4a4ac06

Please sign in to comment.