![]() |
VOOZH | about |
dotnet add package ZeroAlloc.StateMachine --version 1.5.1
NuGet\Install-Package ZeroAlloc.StateMachine -Version 1.5.1
<PackageReference Include="ZeroAlloc.StateMachine" Version="1.5.1" />
<PackageVersion Include="ZeroAlloc.StateMachine" Version="1.5.1" />Directory.Packages.props
<PackageReference Include="ZeroAlloc.StateMachine" />Project file
paket add ZeroAlloc.StateMachine --version 1.5.1
#r "nuget: ZeroAlloc.StateMachine, 1.5.1"
#:package ZeroAlloc.StateMachine@1.5.1
#addin nuget:?package=ZeroAlloc.StateMachine&version=1.5.1Install as a Cake Addin
#tool nuget:?package=ZeroAlloc.StateMachine&version=1.5.1Install as a Cake Tool
๐ NuGet
๐ Build
๐ AOT
๐ GitHub Sponsors
Source-generated, zero-allocation finite state machines for .NET.
Add [StateMachine] and [Transition<TState, TTrigger>] attributes to a partial class or struct. A Roslyn source generator emits a TryFire(TTrigger) method as a switch expression over (TState, TTrigger) tuples โ no dictionary, no delegate dispatch, no heap allocation on the transition path. AOT-safe.
dotnet add package ZeroAlloc.StateMachine
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 { }
var machine = new OrderMachine();
machine.TryFire(Trigger.Submit); // true โ Idle โ Pending
machine.Current; // State.Pending
machine.TryFire(Trigger.Pay); // true โ Pending โ Done
machine.TryFire(Trigger.Submit); // false โ Done has no outgoing transitions
Head-to-head vs Stateless 5.15 (the de-facto state-machine library in .NET). .NET 10.0.7, BenchmarkDotNet v0.14.0.
| Operation | Stateless | ZA.StateMachine | Speedup |
|---|---|---|---|
| Fire valid (3-step cycle) | 4,495 ns / 7,272 B | 36 ns / 24 B | 124ร faster, 303ร less alloc |
| Fire invalid | 27 ns / 24 B | 1.6 ns / 0 B | 17ร faster, 0 B alloc |
| Guard allowed | 2,718 ns / 4,160 B | 15 ns / 24 B | 178ร faster, 173ร less alloc |
| Guard blocked | 699 ns / 792 B | 0.3 ns / 0 B | 2,200ร faster, 0 B alloc |
Stateless walks a Dictionary<TTrigger, StateRepresentation> on every fire and allocates trigger/transition info objects. ZA emits a switch expression over (State, Trigger) at compile time โ single jump-table lookup, zero allocation on the dispatch path. The Fire-valid row also includes a per-iteration machine reset for both libraries; ZA's reset is one allocation because configuration is compile-time, while Stateless's reset includes its fluent Configure().Permit() rebuild โ see docs/performance.md for the full breakdown.
Full methodology + self-benchmark: docs/performance.md.
| Feature | Notes |
|---|---|
| Zero allocation on happy path | TryFire allocates 0 bytes โ the switch is a compile-time constant |
| AOT / trimmer safe | Generator emits concrete switch arms; no reflection at runtime |
| Concurrent mode | Interlocked.CompareExchange CAS loop, Volatile.Read for Current |
| Guards | partial bool Guard{Trigger}(TState, TTrigger) โ block a transition at runtime |
| Entry / exit hooks | partial void OnEnter{State} / partial void OnExit{State} โ observe every crossing |
| Terminal states | [Terminal<TState>] silences the "no outgoing transitions" diagnostic |
| Struct support | partial struct machines eliminate even the instance heap allocation |
| Diagnostics | ZSM0001โZSM0004: unreachable state, sink state, concurrent + guard, concurrent + struct |
[StateMachine][StateMachine(InitialState = nameof(State.Idle), Concurrent = false)]
public partial class MyMachine { }
| Property | Type | Default | Description |
|---|---|---|---|
InitialState |
string |
required | Name of the initial state enum value. Use nameof(...). |
Concurrent |
bool |
false |
Enables thread-safe transitions via Interlocked.CompareExchange. |
[Transition<TState, TTrigger>][Transition<State, Trigger>(From = State.Idle, On = Trigger.Submit, To = State.Pending, When = false)]
| Property | Type | Default | Description |
|---|---|---|---|
From |
TState |
required | Source state. |
On |
TTrigger |
required | Trigger that fires the transition. |
To |
TState |
required | Destination state. |
When |
bool |
false |
Emit a Guard{Trigger} partial stub and add a when clause. |
[Terminal<TState>][Terminal<State>(State = State.Done)]
Marks a state as an intentional sink (no outgoing transitions). Silences ZSM0002.
For each annotated type the generator emits one file alongside the user's source:
// <auto-generated />
partial class OrderMachine
{
private State _state = State.Idle;
public State Current => _state;
public bool TryFire(Trigger trigger)
=> (Current, trigger) switch
{
(State.Idle, Trigger.Submit) => Fire(State.Idle, State.Pending, trigger),
(State.Pending, Trigger.Pay) => Fire(State.Pending, State.Done, trigger),
_ => false
};
private bool Fire(State from, State to, Trigger trigger) { ... }
// Partial hook stubs โ implement what you need, leave the rest
partial void OnExitIdle(Trigger on);
partial void OnExitPending(Trigger on);
partial void OnEnterPending(State from);
partial void OnEnterDone(State from);
}
public partial class OrderMachine
{
partial void OnExitIdle(Trigger on)
=> Console.WriteLine($"Leaving Idle via {on}");
partial void OnEnterDone(State from)
=> Console.WriteLine($"Order complete, came from {from}");
}
[Transition<State, Trigger>(From = State.Pending, On = Trigger.Pay, To = State.Done, When = true)]
public partial class OrderMachine
{
public bool HasBalance { get; set; }
// Generator emits: private partial bool GuardPay(State from, Trigger on);
private partial bool GuardPay(State from, Trigger on) => HasBalance;
}
[StateMachine(InitialState = nameof(State.Idle), Concurrent = true)]
[Transition<State, Trigger>(From = State.Idle, On = Trigger.Start, To = State.Running)]
public partial class WorkerMachine { }
State is stored as volatile long. TryFire uses a CAS loop โ safe for concurrent callers. Guards are not generated in concurrent mode (TOCTOU risk).
| ID | Severity | Description |
|---|---|---|
| ZSM0001 | Warning | State is unreachable (no transition leads to it, not InitialState) |
| ZSM0002 | Warning | State has no outgoing transitions (use [Terminal] to acknowledge) |
| ZSM0003 | Warning | Trigger appears in only one transition (possible typo) |
| ZSM0004 | Error | Concurrent = true on a partial struct (not supported) |
Full docs live in docs/:
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 4 NuGet packages that depend on ZeroAlloc.StateMachine:
| Package | Downloads |
|---|---|
|
ZeroAlloc.Resilience
Source-generated, zero-allocation resilience policies for .NET. Add [Retry], [Timeout], [RateLimit], and [CircuitBreaker] to an interface; the generator emits a proxy composing all policies in declaration order. AOT-safe. |
|
|
ZeroAlloc.Scheduling
Source-generated zero-allocation background job scheduling for .NET. |
|
|
ZeroAlloc.Outbox
Source-generated transactional outbox for .NET. |
|
|
ZeroAlloc.EventSourcing.Aggregates
Aggregate base + generator-emitted Apply dispatch for ZeroAlloc.EventSourcing. |
This package is not used by any popular GitHub repositories.