diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index bd72499..49004c3 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,5 +1,7 @@ name: main +run-name: Initiated by ${{ github.actor }} | Branch ${{ github.ref }} | SHA ${{ github.sha }} + on: push: branches: @@ -7,7 +9,7 @@ on: workflow_dispatch: jobs: - test: + main-build: runs-on: ubuntu-latest steps: - name: checkout repository diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 84196eb..6023172 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -1,12 +1,23 @@ name: pull request + +run-name: Initiated by ${{ github.actor }} | Branch ${{ github.ref }} | SHA ${{ github.sha }} + on: pull_request: branches: - main + +# A workflow run that hasn’t been queued within 30 minutes of being triggered is discarded. +# A workflow run that has been queued but not processed by a GitHub-hosted runner within 45 minutes is discarded. +# Can only apply timeouts at job and step level +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true jobs: - test: + pull-request-build: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 @@ -21,6 +32,9 @@ jobs: - name: dotnet restore run: dotnet restore + + - name: dotnet format + run: dotnet format --verify-no-changes --no-restore - name: dotnet build [Release] run: dotnet build --no-restore --configuration Release diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 08e468c..faac3f8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,6 +7,10 @@ on: types: - published +concurrency: + cancel-in-progress: false + group: release + jobs: release: runs-on: ubuntu-latest @@ -28,8 +32,8 @@ jobs: run: dotnet build --configuration Release - name: Extract version from tag - id: get_version - run: | + id: get_version + run: | # Remove the "refs/tags/" prefix from GITHUB_REF TAG=${GITHUB_REF#refs/tags/} # Remove leading 'v' if tag looks like "v1.2.3" diff --git a/FiniteStateMachine/FiniteStateMachineBuilder.cs b/FiniteStateMachine/FiniteStateMachineBuilder.cs index b4d421f..7a87fa4 100644 --- a/FiniteStateMachine/FiniteStateMachineBuilder.cs +++ b/FiniteStateMachine/FiniteStateMachineBuilder.cs @@ -10,7 +10,7 @@ public sealed class FiniteStateMachineBuilder where TState : notnull { private readonly TState _initialState; - private readonly Dictionary> _transitions = new(); + private readonly Dictionary>> _transitions = new(); /// /// Initializes a new instance of the class. @@ -20,7 +20,7 @@ internal FiniteStateMachineBuilder(TState initialState) { _initialState = initialState; } - + /// /// Defines a state and its possible transitions. /// @@ -36,7 +36,7 @@ public FiniteStateMachineBuilder State( _transitions.Add(state, cfg.Transitions); return this; } - + /// /// Builds and returns the configured state machine. /// @@ -52,7 +52,7 @@ private Dictionary, Rule> GetTransit var rules = new Dictionary, Rule>(); foreach (var (fromState, transitions) in _transitions) { - foreach (var (trigger, targetState) in transitions) + foreach (var (trigger, transitionOption) in transitions) { var ruleKey = new RuleKey { @@ -62,8 +62,10 @@ private Dictionary, Rule> GetTransit var rule = new Rule { From = fromState, - To = targetState, - Trigger = trigger + To = transitionOption.TargetState, + Trigger = trigger, + EntryActions = transitionOption.EntryActions, + ExitActions = transitionOption.ExitActions, }; rules.Add(ruleKey, rule); } diff --git a/FiniteStateMachine/Rule.cs b/FiniteStateMachine/Rule.cs index f1d1b88..4a3a985 100644 --- a/FiniteStateMachine/Rule.cs +++ b/FiniteStateMachine/Rule.cs @@ -1,9 +1,13 @@ namespace FiniteStateMachine; -public sealed record Rule +public sealed record Rule where TTrigger : Enum { public required TState From { get; init; } public required TState To { get; init; } public required TTrigger Trigger { get; init; } + + public required List EntryActions { get; init; } + + public required List ExitActions { get; init; } } \ No newline at end of file diff --git a/FiniteStateMachine/RuleKey.cs b/FiniteStateMachine/RuleKey.cs index a25e2ba..88c9fee 100644 --- a/FiniteStateMachine/RuleKey.cs +++ b/FiniteStateMachine/RuleKey.cs @@ -1,6 +1,6 @@ namespace FiniteStateMachine; -public sealed record RuleKey +public sealed record RuleKey where TTrigger : Enum { public required TState From { get; init; } diff --git a/FiniteStateMachine/StateConfiguration.cs b/FiniteStateMachine/StateConfiguration.cs index 7d5f688..e3e1cc5 100644 --- a/FiniteStateMachine/StateConfiguration.cs +++ b/FiniteStateMachine/StateConfiguration.cs @@ -8,11 +8,17 @@ namespace FiniteStateMachine; public sealed class StateConfiguration where TTrigger : Enum { private readonly TState _state; - + + private readonly List _entryActions; + private readonly List _exitActions; + + internal IReadOnlyList EntryActions => _entryActions; + internal IReadOnlyList ExitActions => _exitActions; + /// /// Gets the transitions defined for the current state. /// - internal Dictionary Transitions { get; } = new(); + internal Dictionary> Transitions { get; } = new(); /// /// Initializes a new instance of the class. @@ -21,8 +27,16 @@ public sealed class StateConfiguration where TTrigger : Enum internal StateConfiguration(TState state) { _state = state; + _entryActions = new List(); + _exitActions = new List(); } - + + internal void ClearActions() + { + _entryActions.Clear(); + _exitActions.Clear(); + } + /// /// Starts the configuration of a transition for a given trigger. /// @@ -30,6 +44,19 @@ internal StateConfiguration(TState state) /// A configuration object to setup the transition. public TransitionConfiguration On(TTrigger trigger) { + return new TransitionConfiguration(this, trigger); } + + public StateConfiguration OnEnter(Action action) + { + _entryActions.Add(action); + return this; + } + + public StateConfiguration OnExit(Action action) + { + _exitActions.Add(action); + return this; + } } \ No newline at end of file diff --git a/FiniteStateMachine/StateMachine.cs b/FiniteStateMachine/StateMachine.cs index 3eadc64..4e1296d 100644 --- a/FiniteStateMachine/StateMachine.cs +++ b/FiniteStateMachine/StateMachine.cs @@ -12,17 +12,17 @@ public sealed class StateMachine /// A dictionary containing rules for state transitions. /// private readonly Dictionary, Rule> _rules; - + /// /// Gets the rules for state transitions as a read-only dictionary. /// public IReadOnlyDictionary, Rule> Rules => _rules; - + /// /// Gets the current state of the state machine. /// public TState CurrentState { get; private set; } - + /// /// Initializes a new instance of the class. /// @@ -43,7 +43,7 @@ public static FiniteStateMachineBuilder WithInitialState(TStat { return new FiniteStateMachineBuilder(initialState); } - + /// /// Triggers a transition based on the specified trigger, updating the current state if a rule is matched. /// @@ -57,7 +57,13 @@ public bool Trigger(TTrigger trigger) Trigger = trigger }; if (!_rules.TryGetValue(key, out var rule)) return false; + + rule.ExitActions.ForEach(action => action()); + CurrentState = rule.To; + + rule.EntryActions.ForEach(action => action()); + return true; } } \ No newline at end of file diff --git a/FiniteStateMachine/TransitionConfiguration.cs b/FiniteStateMachine/TransitionConfiguration.cs index 71722f9..5deb3f7 100644 --- a/FiniteStateMachine/TransitionConfiguration.cs +++ b/FiniteStateMachine/TransitionConfiguration.cs @@ -10,10 +10,19 @@ internal TransitionConfiguration(StateConfiguration stateConfi _stateConfiguration = stateConfiguration; _trigger = trigger; } - + public StateConfiguration GoTo(TState target) { - _stateConfiguration.Transitions[_trigger] = target; + _stateConfiguration.Transitions[_trigger] = new TransitionOption + { + Trigger = _trigger, + TargetState = target, + EntryActions = _stateConfiguration.EntryActions.ToList(), + ExitActions = _stateConfiguration.ExitActions.ToList() + }; + + _stateConfiguration.ClearActions(); + return _stateConfiguration; } } \ No newline at end of file diff --git a/FiniteStateMachine/TransitionOption.cs b/FiniteStateMachine/TransitionOption.cs new file mode 100644 index 0000000..cd7ecc1 --- /dev/null +++ b/FiniteStateMachine/TransitionOption.cs @@ -0,0 +1,9 @@ +namespace FiniteStateMachine; + +internal sealed record TransitionOption +{ + public required TTrigger Trigger { get; init; } + public required TState TargetState { get; init; } + public required List EntryActions { get; init; } + public required List ExitActions { get; init; } +} \ No newline at end of file diff --git a/FiniteStateMachineTest/ClassState.cs b/FiniteStateMachineTest/ClassState.cs index ab25917..62a0326 100644 --- a/FiniteStateMachineTest/ClassState.cs +++ b/FiniteStateMachineTest/ClassState.cs @@ -15,7 +15,7 @@ public override bool Equals(object? obj) { return false; } - + return state.Name == Name; } diff --git a/FiniteStateMachineTest/EnumState.cs b/FiniteStateMachineTest/State.cs similarity index 76% rename from FiniteStateMachineTest/EnumState.cs rename to FiniteStateMachineTest/State.cs index aaea2b7..ece8892 100644 --- a/FiniteStateMachineTest/EnumState.cs +++ b/FiniteStateMachineTest/State.cs @@ -1,6 +1,6 @@ namespace FiniteStateMachineTest; -internal enum EnumState +internal enum State { Playing, Paused, diff --git a/FiniteStateMachineTest/StateMachineTest.cs b/FiniteStateMachineTest/StateMachineTest.cs index df48f95..bb3ab6b 100644 --- a/FiniteStateMachineTest/StateMachineTest.cs +++ b/FiniteStateMachineTest/StateMachineTest.cs @@ -1,47 +1,78 @@ using FiniteStateMachine; +using Xunit.Abstractions; namespace FiniteStateMachineTest; public class StateMachineTest { + private readonly ITestOutputHelper _testOutputHelper; + + public StateMachineTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public void ShouldCreateFiniteStateMachine() { var stateMachine = BuildStateMachine(); var stateMachineCurrentState = stateMachine.CurrentState; Assert.Equal(9, stateMachine.Rules.Count); - Assert.Equal(EnumState.Paused, stateMachineCurrentState); + Assert.Equal(State.Paused, stateMachineCurrentState); } - + [Fact] public void ShouldTransitionToNextState() { var stateMachine = BuildStateMachine(); var result = stateMachine.Trigger(Trigger.Play); Assert.True(result); - Assert.Equal(EnumState.Playing, stateMachine.CurrentState); - + Assert.Equal(State.Playing, stateMachine.CurrentState); + } - - private static StateMachine BuildStateMachine() + + private static StateMachine BuildStateMachine() { - return StateMachine - .WithInitialState(EnumState.Paused) - .State(EnumState.Paused, cfg => cfg - .On(Trigger.Play).GoTo(EnumState.Playing) - .On(Trigger.Pause).GoTo(EnumState.Paused) - .On(Trigger.Stop).GoTo(EnumState.Stopped) - ) - .State(EnumState.Stopped, cfg => cfg - .On(Trigger.Play).GoTo(EnumState.Playing) - .On(Trigger.Pause).GoTo(EnumState.Stopped) - .On(Trigger.Stop).GoTo(EnumState.Stopped) - ) - .State(EnumState.Playing, cfg => cfg - .On(Trigger.Play).GoTo(EnumState.Playing) - .On(Trigger.Pause).GoTo(EnumState.Paused) - .On(Trigger.Stop).GoTo(EnumState.Stopped) - ) - .Build(); + return StateMachine + .WithInitialState(State.Paused) + .State(State.Paused, cfg => + { + cfg + .OnExit(() => Console.WriteLine("Exiting Paused")) + .OnEnter(() => Console.WriteLine("Entering Playing First Action")) + .OnEnter(() => Console.WriteLine("Entering Playing Second Action")) + .On(Trigger.Play) + .GoTo(State.Playing); + + cfg + .OnEnter(() => Console.WriteLine("Entering Stopped State")) + .OnEnter(() => Console.WriteLine("Executing Second Action")) + .OnExit(() => Console.WriteLine("Exiting Paused State")) + .On(Trigger.Pause).GoTo(State.Paused) + .On(Trigger.Stop).GoTo(State.Stopped); + }) + .State(State.Stopped, cfg => + { + cfg + .On(Trigger.Play).GoTo(State.Playing) + .On(Trigger.Pause).GoTo(State.Stopped) + .On(Trigger.Stop).GoTo(State.Stopped); + } + ) + .State(State.Playing, cfg => + { + cfg + .On(Trigger.Play).GoTo(State.Playing) + .On(Trigger.Pause).GoTo(State.Paused); + + cfg + .OnEnter(() => Console.WriteLine("Entering Stopped")) + .OnExit(() => Console.WriteLine("Exiting Playing")) + .On(Trigger.Stop) + .GoTo(State.Stopped); + + } + ) + .Build(); } } \ No newline at end of file diff --git a/README.md b/README.md index 7cd5f4a..31206e2 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,352 @@ # StateCore -The library provides a flexible framework for creating finite state machines in C#. +[![NuGet Stats](https://img.shields.io/nuget/v/StateCore.svg)](https://www.nuget.org/packages/StateCore) +![Build](https://github.com/Chitova263/StateCore/workflows/main/badge.svg) +[![NuGet Downloads](https://img.shields.io/nuget/dt/StateCore.svg)](https://www.nuget.org/packages/StateCore) + +A flexible and type-safe finite state machine library for C#. ## Features -- Define states and triggers. -- Create state machines with customizable transitions. -- Easily integrate into your projects for handling state-driven logic. +- 🎯 **Type-Safe**: Strongly typed states and triggers using enums or custom classes +- 🔄 **Flexible Transitions**: Define complex state transitions with granular control +- 🎬 **Per-Transition Lifecycle Hooks**: Execute specific actions for individual transitions +- 🔗 **Fluent API**: Intuitive builder pattern for readable state machine definitions +- ⚡ **Multiple Actions**: Chain multiple entry/exit actions per transition +- 🧩 **Extensible**: Works with enums or custom IEquatable types + +## Installation -### Basic Usage +```bash +dotnet add package StateCore +``` + +Or via Package Manager: +```powershell +Install-Package StateCore +``` -To get started with the `FiniteStateMachine` library, you need to define your states and triggers as enums: +## Quick Start ```csharp -public enum State +using StateCore; + +// Define your states and triggers +public enum State { Paused, Playing, Stopped } +public enum Trigger { Play, Pause, Stop } + +// Create the state machine +var stateMachine = StateMachine + .WithInitialState(State.Paused) + .State(State.Paused, cfg => + { + // Transition: Paused → Playing + cfg + .OnExit(() => Console.WriteLine("Leaving Paused state")) + .OnEnter(() => Console.WriteLine("Entering Playing state")) + .On(Trigger.Play) + .GoTo(State.Playing); + + // Transition: Paused → Stopped + cfg + .OnExit(() => Console.WriteLine("Leaving Paused state")) + .OnEnter(() => Console.WriteLine("Entering Stopped state")) + .On(Trigger.Stop) + .GoTo(State.Stopped); + }) + .State(State.Playing, cfg => cfg + .OnExit(() => Console.WriteLine("Leaving Playing state")) + .OnEnter(() => Console.WriteLine("Entering Paused state")) + .On(Trigger.Pause) + .GoTo(State.Paused)) + .State(State.Stopped, cfg => cfg + .OnEnter(() => Console.WriteLine("Entering Playing state from Stopped")) + .On(Trigger.Play) + .GoTo(State.Playing)) + .Build(); + +// Trigger state transitions +stateMachine.Trigger(Trigger.Play); +// Output: +// Leaving Paused state +// Entering Playing state + +Console.WriteLine($"Current state: {stateMachine.CurrentState}"); // Playing +``` + +## Understanding Transitions + +**Important**: In StateCore, the lifecycle hooks work as follows: +- **`OnExit()`** - Executes when leaving the **current state** (the state being configured) +- **`OnEnter()`** - Executes when entering the **target state** (the state in `GoTo()`) + +```csharp +.State(State.Paused, cfg => { - Paused, - Playing, - Stopped -} + cfg + .OnExit(() => Console.WriteLine("Exiting Paused")) // Runs when leaving Paused + .OnEnter(() => Console.WriteLine("Entering Playing")) // Runs when entering Playing + .On(Trigger.Play) + .GoTo(State.Playing); // ← OnEnter refers to THIS state +}) +``` + +Each configuration chain represents a **specific transition path** with its own unique behavior. + +## Advanced Usage + +### Multiple Actions Per Transition + +Chain multiple actions that execute in order when entering the target state: + +```csharp +.State(State.Stopped, cfg => cfg + .OnExit(() => Console.WriteLine("Leaving Stopped state")) + .OnEnter(() => Console.WriteLine("1. Initializing audio system")) + .OnEnter(() => Console.WriteLine("2. Loading media file")) + .OnEnter(() => Console.WriteLine("3. Starting playback")) + .OnEnter(() => Console.WriteLine("4. Updating UI to Playing")) + .On(Trigger.Play) + .GoTo(State.Playing)) +``` + +When `Trigger.Play` is fired: +``` +Output: +Leaving Stopped state +1. Initializing audio system +2. Loading media file +3. Starting playback +4. Updating UI to Playing +``` + +### Different Entry Behavior for Same Target State + +You can enter the same state from different sources with different behavior: + +```csharp +// From Paused +.State(State.Paused, cfg => cfg + .OnExit(() => Console.WriteLine("Resuming from pause")) + .OnEnter(() => Console.WriteLine("Continuing playback")) + .On(Trigger.Play) + .GoTo(State.Playing)) + +// From Stopped +.State(State.Stopped, cfg => cfg + .OnExit(() => Console.WriteLine("Starting fresh")) + .OnEnter(() => Console.WriteLine("Beginning new playback")) + .On(Trigger.Play) + .GoTo(State.Playing)) +``` + +Both transitions go to `State.Playing`, but with different `OnEnter` actions! + +### Multiple Transitions from Same State -public enum Trigger +Define different behavior for each outgoing transition: + +```csharp +.State(State.Playing, cfg => { - Play, - Pause, - Stop -} + // Transition: Playing → Paused + cfg + .OnExit(() => Console.WriteLine("Pausing playback")) + .OnEnter(() => Console.WriteLine("Now paused")) + .OnEnter(() => SavePlaybackPosition()) + .On(Trigger.Pause) + .GoTo(State.Paused); + + // Transition: Playing → Stopped + cfg + .OnExit(() => Console.WriteLine("Stopping playback")) + .OnExit(() => ReleaseResources()) + .OnEnter(() => Console.WriteLine("Fully stopped")) + .OnEnter(() => ResetPlaybackPosition()) + .On(Trigger.Stop) + .GoTo(State.Stopped); +}) ``` -Building a State Machine +### Using Custom Classes as States + +Instead of enums, use custom classes that implement `IEquatable`: ```csharp - var statemachine = StateMachine - .WithInitialState(EnumState.Paused) - .State(EnumState.Paused, cfg => cfg - .On(Trigger.Play).GoTo(EnumState.Playing) - .On(Trigger.Pause).GoTo(EnumState.Paused) - .On(Trigger.Stop).GoTo(EnumState.Stopped) - ) - .State(EnumState.Stopped, cfg => cfg - .On(Trigger.Play).GoTo(EnumState.Playing) - .On(Trigger.Pause).GoTo(EnumState.Stopped) - .On(Trigger.Stop).GoTo(EnumState.Stopped) - ) - .State(EnumState.Playing, cfg => cfg - .On(Trigger.Play).GoTo(EnumState.Playing) - .On(Trigger.Pause).GoTo(EnumState.Paused) - .On(Trigger.Stop).GoTo(EnumState.Stopped) - ) - .Build(); -``` - -Once the state machine is built, you can trigger transitions based on current state: +public class State : IEquatable +{ + public int Id { get; } + public string Name { get; } + + public State(int id, string name) + { + Id = id; + Name = name; + } + + public bool Equals(State? other) + { + if (other is null) return false; + return Id == other.Id && Name == other.Name; + } + + public override bool Equals(object? obj) => Equals(obj as State); + + public override int GetHashCode() => HashCode.Combine(Id, Name); + + public static bool operator ==(State? left, State? right) => Equals(left, right); + public static bool operator !=(State? left, State? right) => !Equals(left, right); +} + +// Define state instances +var paused = new State(1, "Paused"); +var playing = new State(2, "Playing"); + +// Use in state machine +var stateMachine = StateMachine + .WithInitialState(paused) + .State(paused, cfg => cfg + .OnExit(() => Console.WriteLine($"Leaving {paused.Name}")) + .OnEnter(() => Console.WriteLine($"Entering {playing.Name}")) + .On(Trigger.Play) + .GoTo(playing)) + .Build(); +``` + +## Complete Example: Media Player + ```csharp -var result = stateMachine.Trigger(Trigger.Play); -Console.WriteLine(stateMachine.CurrentState); // Outputs: Playing +public enum State { Stopped, Playing, Paused, Buffering } +public enum Trigger { Play, Pause, Stop, Buffer, Resume } + +var mediaPlayer = StateMachine + .WithInitialState(State.Stopped) + .State(State.Stopped, cfg => + { + // Stopped → Playing + cfg + .OnExit(() => Console.WriteLine("Starting player")) + .OnEnter(() => Console.WriteLine("Loading media")) + .OnEnter(() => Console.WriteLine("Playback started")) + .On(Trigger.Play) + .GoTo(State.Playing); + }) + .State(State.Playing, cfg => + { + // Playing → Paused + cfg + .OnExit(() => Console.WriteLine("Pausing playback")) + .OnEnter(() => Console.WriteLine("Playback paused")) + .OnEnter(() => SavePosition()) + .On(Trigger.Pause) + .GoTo(State.Paused); + + // Playing → Stopped + cfg + .OnExit(() => Console.WriteLine("Stopping playback")) + .OnExit(() => ReleaseResources()) + .OnEnter(() => Console.WriteLine("Player stopped")) + .On(Trigger.Stop) + .GoTo(State.Stopped); + + // Playing → Buffering + cfg + .OnExit(() => Console.WriteLine("Connection slow")) + .OnEnter(() => Console.WriteLine("Buffering content...")) + .OnEnter(() => ShowSpinner()) + .On(Trigger.Buffer) + .GoTo(State.Buffering); + }) + .State(State.Paused, cfg => + { + // Paused → Playing + cfg + .OnExit(() => Console.WriteLine("Resuming playback")) + .OnEnter(() => Console.WriteLine("Playback resumed")) + .On(Trigger.Play) + .GoTo(State.Playing); + + // Paused → Stopped + cfg + .OnExit(() => Console.WriteLine("Stopping from pause")) + .OnEnter(() => Console.WriteLine("Player stopped")) + .On(Trigger.Stop) + .GoTo(State.Stopped); + }) + .State(State.Buffering, cfg => + { + // Buffering → Playing + cfg + .OnExit(() => Console.WriteLine("Buffer filled")) + .OnExit(() => HideSpinner()) + .OnEnter(() => Console.WriteLine("Resuming playback")) + .On(Trigger.Resume) + .GoTo(State.Playing); + }) + .Build(); + +// Use the state machine +mediaPlayer.Trigger(Trigger.Play); +// Output: +// Starting player +// Loading media +// Playback started + +mediaPlayer.Trigger(Trigger.Pause); +// Output: +// Pausing playback +// Playback paused +// (SavePosition called) ``` +## API Reference + +### StateMachine + +#### Builder Methods + +- **`WithInitialState(TState state)`** - Sets the starting state +- **`State(TState state, Action configure)`** - Configures transitions originating from a state +- **`Build()`** - Creates the state machine instance + +#### Instance Methods + +- **`Trigger(TTrigger trigger)`** - Fires a trigger to execute a state transition +- **`CurrentState`** - Gets the current state (property) + +### StateConfiguration (Transition Chain) + +Each configuration chain represents **one specific transition** from the current state to a target state. + +#### Methods + +- **`OnExit(Action action)`** - Executes when leaving the **current state** (the state being configured) +- **`OnEnter(Action action)`** - Executes when entering the **target state** (the state specified in `GoTo()`) +- **`On(TTrigger trigger)`** - Defines which trigger activates this transition +- **`GoTo(TState nextState)`** - Specifies the destination state for this transition + +#### Execution Order + +When a trigger is fired: +1. All `OnExit` actions for the transition (leaving current state) +2. All `OnEnter` actions for the transition (entering target state) +3. State changes to the target state + +## Common Use Cases + +- **Game State Management**: Menu → Playing → Paused → GameOver +- **Workflow Engines**: Draft → Review → Approved → Published +- **Connection Management**: Disconnected → Connecting → Connected → Error +- **Media Players**: Stopped → Playing → Paused → Buffering +- **Document Lifecycle**: New → InProgress → Review → Completed +- **Order Processing**: Pending → Processing → Shipped → Delivered + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. +## Links +- [NuGet Package](https://www.nuget.org/packages/StateCore) +- [GitHub Repository](https://github.com/Chitova263/StateCore) +- [Report Issues](https://github.com/Chitova263/StateCore/issues) \ No newline at end of file