Guidance for Stateless state machine library for .NET. USE FOR: modeling state transitions with guards and actions, workflow engines, order processing pipelines, device lifecycle management, protocol implementations, approval workflows. DO NOT USE FOR: distributed state machines (use Durable Functions or Temporal), event sourcing (use Marten), full BPMN workflow engines (use Elsa), simple boolean flags.
Guidance for Stateless state machine library for .NET. USE FOR: modeling state transitions with guards and actions, workflow engines, order processing pipelines, device lifecycle management, protocol implementations, approval workflows. DO NOT USE FOR: distributed state machines (use Durable Functions or Temporal), event sourcing (use Marten), full BPMN workflow engines (use Elsa), simple boolean flags.
Stateless is a lightweight library for creating state machines in .NET. It uses a fluent configuration API to define states, triggers, guard conditions, entry/exit actions, and sub-states. State machines built with Stateless are in-memory and synchronous by default, with support for async triggers.
Stateless is ideal for modeling domain workflows where an entity transitions through a defined set of states in response to events (triggers). Examples include order processing, device lifecycle, approval workflows, and protocol implementations.
Install via NuGet:
dotnet add package Stateless
Basic State Machine
Define states and triggers as enums, then configure valid transitions.
Stateless can export its configuration to DOT format for visualization.
using Stateless;
using Stateless.Graph;
var machine = new StateMachine<OrderState, OrderTrigger>(OrderState.Draft);
// ... configure machine ...// Generate DOT graphstring dotGraph = UmlDotGraph.Format(machine.GetInfo());
Console.WriteLine(dotGraph);
// Output can be rendered with Graphviz or online tools// Query permitted triggersvar permitted = machine.GetPermittedTriggers();
Console.WriteLine($"Permitted triggers: {string.Join(", ", permitted)}");
Best Practices
Define states and triggers as enums rather than strings to get compile-time safety and prevent typos in state/trigger names.
Use guard conditions (PermitIf) to enforce business rules at the transition level rather than checking preconditions in calling code.
Provide human-readable guard descriptions as the last parameter to PermitIf so that GetPermittedTriggers and diagram exports show why a transition is blocked.
Use OnEntry/OnExit actions for side effects (logging, notifications, database updates) rather than placing them in the code that calls Fire.
Call CanFire before Fire in UI-driven scenarios to enable/disable buttons based on valid transitions, preventing InvalidOperationException.
Use parameterized triggers to pass context data (assignee, reason, amount) into entry actions rather than setting instance fields before firing.
Use FireAsync and OnEntryAsync when entry/exit actions involve I/O operations (database, HTTP calls) to avoid blocking threads.
Externalize state storage by using the StateMachine<TState, TTrigger>(stateAccessor, stateMutator) constructor overload to persist state in a database or cache.
Export DOT diagrams with UmlDotGraph.Format during development to visually verify that the state machine matches the intended workflow design.
Handle InvalidOperationException from invalid transitions gracefully by logging the current state and attempted trigger rather than letting the exception propagate unhandled.