Skip to main content

Getting Started

This guide takes you from zero to a working state machine in about five minutes.


1. Install

dotnet add package ZeroAlloc.StateMachine

The package ships the runtime attributes and the Roslyn source generator as a single NuGet. No separate analyzer package is needed.


2. Define your enums

State machines need two enums: one for states, one for triggers (events that cause transitions).

public enum OrderState
{
Idle,
Pending,
Paid,
Shipped,
Cancelled
}

public enum OrderTrigger
{
Submit,
Pay,
Ship,
Cancel
}

Enums can live anywhere — same file, separate file, nested inside a class.


3. Annotate a partial class

Add [StateMachine] to set the initial state, then stack one [Transition] attribute per edge.

using ZeroAlloc.StateMachine;

[StateMachine(InitialState = nameof(OrderState.Idle))]
[Transition<OrderState, OrderTrigger>(From = OrderState.Idle, On = OrderTrigger.Submit, To = OrderState.Pending)]
[Transition<OrderState, OrderTrigger>(From = OrderState.Pending, On = OrderTrigger.Pay, To = OrderState.Paid)]
[Transition<OrderState, OrderTrigger>(From = OrderState.Paid, On = OrderTrigger.Ship, To = OrderState.Shipped)]
[Transition<OrderState, OrderTrigger>(From = OrderState.Idle, On = OrderTrigger.Cancel, To = OrderState.Cancelled)]
[Transition<OrderState, OrderTrigger>(From = OrderState.Pending, On = OrderTrigger.Cancel, To = OrderState.Cancelled)]
[Terminal<OrderState>(State = OrderState.Shipped)]
[Terminal<OrderState>(State = OrderState.Cancelled)]
public partial class OrderMachine { }

The class must be partial — the generator fills in the other half.


4. Use it

var order = new OrderMachine();

Console.WriteLine(order.Current); // OrderState.Idle

order.TryFire(OrderTrigger.Submit); // true
Console.WriteLine(order.Current); // OrderState.Pending

order.TryFire(OrderTrigger.Pay); // true
Console.WriteLine(order.Current); // OrderState.Paid

order.TryFire(OrderTrigger.Submit); // false — no transition from Paid on Submit
Console.WriteLine(order.Current); // OrderState.Paid — unchanged

TryFire returns true if the transition fired, false if no matching edge exists (or a guard blocked it). It never throws.


5. Add entry/exit hooks (optional)

The generator emits partial void stubs for every state that appears as a From (exit) or To (enter) in your transitions. Implement the ones you care about:

public partial class OrderMachine
{
partial void OnExitIdle(OrderTrigger on)
=> Console.WriteLine($"Order submitted via {on}");

partial void OnEnterPaid(OrderState from)
=> Console.WriteLine($"Payment received, was in {from}");

partial void OnEnterShipped(OrderState from)
=> Console.WriteLine("Package is on its way!");
}

Unimplemented stubs compile away to nothing — zero overhead.


6. Add a guard (optional)

Guards let you block a transition at runtime. Set When = true on a transition to get a generated Guard{Trigger} partial stub:

[Transition<OrderState, OrderTrigger>(
From = OrderState.Pending,
On = OrderTrigger.Pay,
To = OrderState.Paid,
When = true)]
public partial class OrderMachine
{
public decimal BalanceDue { get; set; }

// The generator emits: private partial bool GuardPay(OrderState from, OrderTrigger on);
private partial bool GuardPay(OrderState from, OrderTrigger on)
=> BalanceDue <= 0;
}

If GuardPay returns false, TryFire(OrderTrigger.Pay) returns false and the state does not change.


7. Dependency injection

State machines are plain classes. Register them with your DI container the same way as any other service:

// Transient — each consumer gets its own machine
services.AddTransient<OrderMachine>();

// Scoped — one machine per HTTP request
services.AddScoped<OrderMachine>();

For use-cases where the machine is shared across threads, enable concurrent mode:

[StateMachine(InitialState = nameof(OrderState.Idle), Concurrent = true)]
// ... transitions ...
public partial class OrderMachine { }

// Register as singleton — thread-safe
services.AddSingleton<OrderMachine>();

What's generated

The generator emits a single file named {ClassName}.StateMachine.g.cs in your project's intermediate output folder. You can inspect it in Visual Studio by expanding the Analyzers node in Solution Explorer, or via the Go to Definition command on any generated member.

See Source Generator for the full annotated output.


Next steps

TopicLink
All attribute propertiesAttribute Reference
Concurrent / thread-safe machinesConcurrent Mode
Guards in depthGuards
Entry and exit hooksEntry/Exit Actions
Terminal statesTerminal States
Real-world example (circuit breaker)Circuit Breaker
Compiler warningsDiagnostics