Source-generated finite state machines for .NET, a compile-time alternative to Stateless: transitions
are a generated switch, 0 B per transition and NativeAOT-ready.
- 0 B per transition, asserted in CI.
TryFireallocates nothing on valid, invalid, guarded and concurrent transitions;AllocationTestsfails CI if it does. Stateless allocates 24 B just to reject an invalid trigger. - About 14× faster than Stateless. Rejecting an invalid trigger takes a fourteenth of Stateless' time in the same run, and a three-step cycle with reset is about 205× faster.
- Works under NativeAOT and trimming. No reflection; every PR publishes an AOT sample with
PublishAot=true. - Compile-time diagnostics instead of runtime surprises. An unreachable state, a state with no way
out or a likely trigger typo is reported at build time, from
ZSM0001toZSM0026.
dotnet add package ZeroAlloc.StateMachineusing ZeroAlloc.StateMachine;
var order = new OrderMachine();
Console.WriteLine(order.TryFire(Trigger.Submit)); // True: Idle -> Pending
Console.WriteLine(order.Current); // Pending
Console.WriteLine(order.TryFire(Trigger.Pay)); // True: Pending -> Done
Console.WriteLine(order.TryFire(Trigger.Submit)); // False: Done is terminal
public enum State { Idle, Pending, Done }
public enum Trigger { Submit, Pay }
[StateMachine(InitialState = nameof(State.Idle))]
[Transition<State, Trigger>(From = State.Idle, On = Trigger.Submit, To = State.Pending)]
[Transition<State, Trigger>(From = State.Pending, On = Trigger.Pay, To = State.Done)]
[Terminal<State>(State = State.Done)]
public partial class OrderMachine
{
partial void OnEnterDone(State from) => Console.WriteLine($"Order complete, came from {from}");
}The generator writes TryFire as a switch over (State, Trigger) and calls the OnEnter… and
OnExit… hooks you implement.
- Guards that block a transition at runtime through a generated
partial boolmethod. - Entry and exit hooks per state.
- Concurrent mode: lock-free, thread-safe transitions with one attribute property.
- Struct machines that keep even the machine itself off the heap.
- Composite and history states, timeout transitions, and concurrent parts that host several machines in one class.
- Mermaid diagram export as a generated constant.
Every attribute and property is in the attribute reference.
BenchmarkDotNet, same run against Stateless 5.20.1. Allocations are exact.
| Operation | Speed-up vs Stateless | Stateless alloc | ZA alloc |
|---|---|---|---|
| Fire valid, 3-step cycle with reset | ~205× | 7,600 B | 0 B |
| Fire invalid | ~14× | 24 B | 0 B |
| Guard blocked | ZA below resolution | 768 B | 0 B |
The cycle row includes each library's reset: a field write for ZeroAlloc, a fluent reconfiguration for Stateless. ZeroAlloc's guard time is below what BenchmarkDotNet can measure, so that row has no finite ratio. Full numbers and methodology: docs/performance.md.
- Documentation: statemachine.zeroalloc.net, including getting started, guards, concurrent mode, the generated code and diagnostics.
- Related packages: ZeroAlloc.Saga and ZeroAlloc.Scheduling, both built on it.
- Contributing: issues and pull requests are welcome on GitHub.
- CHANGELOG.
MIT. See LICENSE.