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 StateCoreusing StateCore;
// Define states and triggers
public enum State { Paused, Playing, Stopped }
public enum Trigger { Play, Pause, Stop }
// Build the state machine
var stateMachine = 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 trigger
stateMachine.Trigger(Trigger.Play);
// Output:
// Leaving Paused
// Entering Playing
Console.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>:
public class State : IEquatable<State>
{
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);
}
var paused = new State(1, "Paused");
var playing = new State(2, "Playing");
var stateMachine = 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.