A lightweight, type-safe finite state machine library for .NET applications.
- Features
- Requirements
- Installation
- Quick Start
- Transition Lifecycle
- Advanced Usage
- API Reference
- Use Cases
- Contributing
- License
- Type-Safe — Strongly typed states and triggers using enums or custom types
- Fluent API — Intuitive builder pattern for readable state machine definitions
- Per-Transition Hooks — Execute specific actions for individual transitions
- Multiple Actions — Chain multiple entry/exit actions per transition
- Extensible — Works with enums or any type implementing
IEquatable<T> - Zero Dependencies — No external dependencies required
- .NET 9.0 or later
dotnet add package StateCoreInstall-Package StateCoreusingStateCore;// Define states and triggerspublicenumState{Paused,Playing,Stopped}publicenumTrigger{Play,Pause,Stop}// Build the state machinevarstateMachine=StateMachine<State,Trigger>.WithInitialState(State.Paused).State(State.Paused, cfg =>{cfg.OnExit(()=>Console.WriteLine("Leaving Paused")).OnEnter(()=>Console.WriteLine("Entering Playing")).On(Trigger.Play).GoTo(State.Playing);cfg.OnExit(()=>Console.WriteLine("Leaving Paused")).OnEnter(()=>Console.WriteLine("Entering Stopped")).On(Trigger.Stop).GoTo(State.Stopped);}).State(State.Playing, cfg =>cfg.OnExit(()=>Console.WriteLine("Leaving Playing")).OnEnter(()=>Console.WriteLine("Entering Paused")).On(Trigger.Pause).GoTo(State.Paused)).State(State.Stopped, cfg =>cfg.OnEnter(()=>Console.WriteLine("Entering Playing")).On(Trigger.Play).GoTo(State.Playing)).Build();// Fire a triggerstateMachine.Trigger(Trigger.Play);// Output:// Leaving Paused// Entering PlayingConsole.WriteLine(stateMachine.CurrentState);// PlayingIn StateCore, lifecycle hooks are bound to specific transitions:
OnExit()— Executes when leaving the current state (the state being configured)OnEnter()— Executes when entering the target state (the state specified inGoTo())
.State(State.Paused, cfg =>{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);})Each configuration chain represents a specific transition path with its own behavior.
When a trigger fires:
- All
OnExitactions execute (leaving the current state) - All
OnEnteractions execute (entering the target state) - State updates to the target state
Execute multiple actions in sequence during a transition:
.State(State.Stopped, cfg =>cfg.OnExit(()=>Console.WriteLine("Leaving Stopped")).OnEnter(()=>InitializeAudioSystem()).OnEnter(()=>LoadMediaFile()).OnEnter(()=>StartPlayback()).OnEnter(()=>UpdateUI()).On(Trigger.Play).GoTo(State.Playing))Define different behavior when entering the same state from different sources:
// Entering Playing from Paused.State(State.Paused, cfg =>cfg.OnExit(()=>Console.WriteLine("Resuming from pause")).OnEnter(()=>Console.WriteLine("Continuing playback")).On(Trigger.Play).GoTo(State.Playing))// Entering 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))Define distinct behavior for each outgoing transition:
.State(State.Playing, cfg =>{cfg.OnExit(()=>Console.WriteLine("Pausing")).OnEnter(()=>SavePlaybackPosition()).On(Trigger.Pause).GoTo(State.Paused);cfg.OnExit(()=>ReleaseResources()).OnEnter(()=>ResetPlaybackPosition()).On(Trigger.Stop).GoTo(State.Stopped);})Use custom classes instead of enums by implementing IEquatable<T>:
publicclassState:IEquatable<State>{publicintId{get;}publicstringName{get;}publicState(intid,stringname){Id=id;Name=name;}publicboolEquals(State?other){if(otherisnull)returnfalse;returnId==other.Id&&Name==other.Name;}publicoverrideboolEquals(object?obj)=>Equals(objasState);publicoverrideintGetHashCode()=>HashCode.Combine(Id,Name);publicstaticbooloperator==(State?left,State?right)=>Equals(left,right);publicstaticbooloperator!=(State?left,State?right)=>!Equals(left,right);}varpaused=newState(1,"Paused");varplaying=newState(2,"Playing");varstateMachine=StateMachine<State,Trigger>.WithInitialState(paused).State(paused, cfg =>cfg.OnEnter(()=>Console.WriteLine($"Entering {playing.Name}")).On(Trigger.Play).GoTo(playing)).Build();| Method | Description |
|---|---|
WithInitialState(TState state) | Creates a builder with the specified initial state |
| Method | Description |
|---|---|
State(TState state, Action<StateConfiguration> configure) | Configures transitions from the specified state |
Build() | Creates the state machine instance |
| Member | Description |
|---|---|
CurrentState | Gets the current state |
Trigger(TTrigger trigger) | Fires a trigger to execute a transition |
Represents a transition chain from the current state to a target state.
| Method | Description |
|---|---|
OnExit(Action action) | Registers an action to execute when leaving the current state |
OnEnter(Action action) | Registers an action to execute when entering the target state |
On(TTrigger trigger) | Specifies the trigger that activates this transition |
GoTo(TState nextState) | Specifies the destination state |
| Domain | Example States |
|---|---|
| Game Development | 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 |
Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature) - Commit your changes (
git commit -m 'Add your feature') - Push to the branch (
git push origin feature/your-feature) - Open a pull request
This project is licensed under the MIT License. See the LICENSE file for details.