# Stateless: A Lightweight State Machine Library for .NET

> Stateless is a C# library for defining finite state machines directly in code, with support for hierarchical states, parameterised triggers, guard clauses, introspection, and export to DOT or Mermaid graph formats. It stores state externally by default, which makes it compatible with ORMs and UI frameworks that manage their own bindable properties. The library is available on NuGet as the `stateless` package, and the repository includes five example projects covering real-world scenarios from telephone calls to alarm systems and bug trackers.

**dotnet-state-machine/stateless** — A simple library for creating state machines in C# code

- Repository: https://github.com/dotnet-state-machine/stateless
- Stars: 6,267 · Forks: 805
- Language: C#
- License: NOASSERTION
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/dotnet-state-machine-stateless

## What Stateless Solves in .NET Applications

State machines appear in .NET code whenever an entity has a defined lifecycle: a phone call that moves from ringing to connected to on-hold, a bug tracker ticket that moves from open to assigned to resolved, an alarm that cycles between armed, triggered, and acknowledged. Without a dedicated library, developers implement these transitions as switch statements or chains of if-else blocks scattered through event handlers, which makes the valid transitions hard to see and easy to break. Stateless externalises the state definition and the transition rules into a fluent configuration API. Every transition is explicit, and firing a trigger that has no configured transition throws an exception by default, so unintended state changes surface immediately rather than silently corrupting application state. The configuration is code, not XML or a database table, which means the state machine definition lives alongside the domain logic, benefits from static analysis, and can be refactored with normal IDE tooling.

## Core Concepts: States, Triggers, and the Configuration API

A Stateless machine takes two generic type parameters: the state type and the trigger type. Both can be any .NET type, including enums, strings, or integers. The README example uses an enum for both:

```csharp
var phoneCall = new StateMachine<State, Trigger>(State.OffHook);

phoneCall.Configure(State.OffHook)
    .Permit(Trigger.CallDialled, State.Ringing);
```

Each call to `Configure` returns a configuration object for a specific state. `Permit` adds a transition from that state to a destination state when a specific trigger fires. `OnEntry` and `OnExit` attach callbacks that run when the machine enters or leaves a state. `InternalTransition` handles a trigger without changing state, which is useful for actions like toggling mute in a connected call without ending the call.

## Hierarchical States and Substates

Stateless supports parent-child state relationships through `SubstateOf`. A substate inherits the transitions of its parent unless it overrides them. The README example shows an OnHold state that is a substate of Connected:

```csharp
phoneCall.Configure(State.OnHold)
    .SubstateOf(State.Connected)
    .Permit(Trigger.TakenOffHold, State.Connected)
    .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);
```

When the machine is in the OnHold state, `IsInState(State.Connected)` returns true because OnHold is a substate of Connected. The entry and exit actions on the parent state do not fire when transitioning between Connected and OnHold, which prevents double-calling timer start and stop methods in the call example. A substate can also be marked as an initial substate with `InitialTransition`, so that entering the parent state automatically enters a specific child.

## Guard Clauses, Parameterised Triggers, and External Storage

Guard clauses let one trigger map to different destination states depending on a runtime condition:

```csharp
phoneCall.Configure(State.OffHook)
    .PermitIf(Trigger.CallDialled, State.Ringing, () => IsValidNumber)
    .PermitIf(Trigger.CallDialled, State.Beeping, () => !IsValidNumber);
```

Guards within a state must be mutually exclusive: only one can be true at a time. Async guards are supported with `PermitIfAsync`. Triggers can carry strongly typed parameters using `SetTriggerParameters<T>`, allowing data to be passed along with a state change. For ORMs and UI frameworks, the machine constructor accepts lambda functions for reading and writing state, so the state value can live in an ORM-tracked property or a bindable field rather than inside the machine itself:

```csharp
var stateMachine = new StateMachine<State, Trigger>(
    () => myState.Value,
    s => myState.Value = s);
```

## Introspection, Graph Export, and Ignored Transitions

The `StateMachine.PermittedTriggers` property returns a list of triggers that can fire successfully from the current state, which is useful for driving UI: a button can be enabled only when its trigger is permitted. `StateMachine.GetInfo()` returns the full configuration as an object graph for inspection or documentation. Stateless can export the state machine definition to a DOT graph (for tools like Graphviz) or to Mermaid format (for rendering in Markdown documents and GitHub READMEs). This export capability is a practical documentation tool: the same configuration that drives runtime behaviour produces the diagram that describes it. To suppress the default exception on an unhandled trigger, `Ignore(TTrigger)` marks specific triggers as intentionally ignored in certain states. This is useful for states where a trigger is valid to receive but should simply have no effect, such as ignoring a 'CallDialled' trigger when the phone is already connected.

## Limitations: No Built-In Persistence, Mutually Exclusive Guards, and Initial State Handling

Stateless does not persist state across process restarts. External storage is supported through the lambda constructor, but the developer must connect it to a database or serialisation layer. The library does not know when it is 'started', which means there is no automatic initial transition when a machine is created. The README documents a workaround: add a dummy initial state and call `Activate()` to trigger the first transition. Guard clauses within a state must be mutually exclusive; if two guards can both be true at the same time, the machine throws an exception at configuration time. There is no mechanism to specify priority between guards. Substates cannot disallow transitions that are permitted by their parent, which can be a constraint in workflows where a specific substate needs to block a parent-level transition. The licence file is listed as NOASSERTION in the repository metadata, which means automated tools cannot resolve it; the actual CHANGELOG.md and LICENSE file in the repository should be reviewed directly to confirm the exact licence terms before use in a commercial product. A SECURITY.md is also present in the repository root for reporting vulnerabilities.

## Activation, Reentrant States, and Dynamic Transitions

Stateless provides `Activate()` and `Deactivate()` methods for lifecycle management. The README describes `Deactivate` as a call to make before storing state (such as when persisting to a database), and `Activate` as a call to make once when restoring state before normal operation resumes. Because Stateless does not know when a machine is conceptually 'started', an initial transition from a dummy state requires `Activate()` to fire the first trigger:

```csharp
sm.Configure(InitialState)
    .OnActivate(() => sm.Fire(LetsGo))
    .Permit(LetsGo, StateA)
```

For cases where the destination state is not known at configuration time, `PermitDynamic()` allows the destination to be computed at runtime. Parameterised triggers can pass typed data into that function. Reentrant states offer another option when a trigger should reset the current state without changing it: `PermitReentry(TTrigger)` causes the machine to exit and re-enter the current state, running exit and entry actions again. This differs from `Ignore`, which handles the trigger with no state change and no action. Substates cannot disallow a transition that the parent state permits; they can only override the destination. Teams that need to block a parent-level trigger from a specific substate must restructure the hierarchy to make the restriction explicit.

## Comparison to Windows Workflow Foundation

Windows Workflow Foundation (WF) is the other state machine option in the .NET ecosystem with Microsoft backing. WF is a full workflow runtime with persistence, tracking, designer support, and a XAML-based configuration format. It carries substantial infrastructure overhead and is primarily designed for long-running workflows that span hours or days and require a persistence store. Stateless is the opposite: a small in-process library with no persistence infrastructure and no designer. A state machine that fits in a few hundred lines of C# and lives in the same codebase as the domain it models is exactly what Stateless is designed for. If the workflow needs to survive server restarts, span multiple services, or be defined by non-developers in a visual tool, WF or a dedicated workflow engine is the more appropriate choice. The repository includes five example projects under the `example/` directory: AlarmExample, BugTrackerExample, JsonExample, OnOffExample, and TelephoneCallExample, each demonstrating a different real-world state machine scenario in working C# code. The JsonExample shows how to serialise and restore state machine configuration to and from JSON, which is directly relevant to the external state storage use case. The BugTrackerExample models a ticket workflow with states like Open, Assigned, Resolved, and Closed, which is a representative pattern for teams evaluating Stateless for issue tracking or approval flows in a .NET application.

## Conclusion

Stateless suits .NET developers who need to encode business logic as a finite state machine directly in C# without a code generation step or an external DSL. It is a good fit for workflows with clear states, explicit transitions, and guard conditions that need to be readable alongside the rest of the codebase. It is the wrong choice when the state machine needs to survive process restarts without custom persistence code: external state storage is supported but requires the developer to wire it explicitly. The library is available on NuGet as `stateless`. The last push to the repository was on 2026-04-04. Before adopting, check whether your project requires async guard clauses (`PermitIfAsync`) or Mermaid output for documentation, since those features are present and documented in the README.

## FAQ

### Does Stateless support async guard clauses?

Yes. The README documents PermitIfAsync for defining async guard clauses on transitions. These are used the same way as synchronous guards but return a Task from the predicate.

### Can Stateless export a state machine diagram?

Yes. The library can export the state machine configuration to DOT graph format for use with Graphviz or to Mermaid format for rendering in Markdown documents. Both export options are listed in the README's feature list.

### How does Stateless handle state that must be stored in a database?

The StateMachine constructor accepts two lambda functions: one to read the current state and one to write it. This allows the state value to live in an ORM-tracked property or any external store. The README shows an example using a myState.Value property.

## Sources

- [dotnet-state-machine/stateless on GitHub](https://github.com/dotnet-state-machine/stateless)
- [Issues](https://github.com/dotnet-state-machine/stateless/issues)
- [README](https://github.com/dotnet-state-machine/stateless/blob/dev/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dotnet-state-machine-stateless
