ZeroAlloc.StateMachine
1.1.1
dotnet add package ZeroAlloc.StateMachine --version 1.1.1
NuGet\Install-Package ZeroAlloc.StateMachine -Version 1.1.1
<PackageReference Include="ZeroAlloc.StateMachine" Version="1.1.1" />
<PackageVersion Include="ZeroAlloc.StateMachine" Version="1.1.1" />
<PackageReference Include="ZeroAlloc.StateMachine" />
paket add ZeroAlloc.StateMachine --version 1.1.1
#r "nuget: ZeroAlloc.StateMachine, 1.1.1"
#:package ZeroAlloc.StateMachine@1.1.1
#addin nuget:?package=ZeroAlloc.StateMachine&version=1.1.1
#tool nuget:?package=ZeroAlloc.StateMachine&version=1.1.1
ZeroAlloc.StateMachine
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.
Quick start
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
Features
| 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 |
Attribute overview
[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.
Generated code
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);
}
Hooks
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}");
}
Guards
[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;
}
Concurrent mode
[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).
Diagnostics
| 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) |
Documentation
Full docs live in docs/:
- Getting Started
- Attribute Reference
- Source Generator
- Testing
- AOT & Trimming
- Performance
- Core concepts: States & Triggers · Transitions · Concurrent Mode
- Guides: Guards · Entry/Exit Actions · Terminal States · Circuit Breaker Example
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (4)
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. |
GitHub repositories
This package is not used by any popular GitHub repositories.