Stateless: The .NET State Machine That Doesn’t Have to Own State
A fluent C# library that turns external objects into disciplined workflow engines, with hierarchical transitions, queued execution, and code-first graph export.

Stateless is still one of my favorite libraries, but since it's not used in any of the applications I spend my days on, I'm no longer a good custodian of the project. It's time for me to hand the reigns to others with more head space, inspiration and new ideas.
- Stateless is compelling because it separates transition logic from where state is stored, so the machine can be recreated whenever the domain object is still the source of truth.
- Its fluent API keeps business rules readable while queued firing and hierarchical states protect the engine from re-entrancy and duplicated branching logic.
- The project becomes more useful once you see it as an embedded transition brain for ordinary .NET objects, not as a heavyweight workflow runtime.
- Graph export closes the loop by making the same configuration both executable and documentable.
The odd thing about Stateless is right there in the name. It is not a state container pretending to be disciplined. It is a transition engine that can be created, used, discarded, and created again while the actual state lives somewhere else.
That split matters. In a real .NET app, state often already belongs to an ORM entity, a message payload, or a plain object that the rest of the system understands. Stateless lets you keep that ownership where it belongs and still force the transitions through explicit rules.
The interesting part was that, because the actual _state field was mapped to a column by our ORM, the state machine didn’t actually store the current state. Instead, when constructing it, you could provide a pair of delegates to get the current state and set the new state after a transition.
The machine can disappear, and the state stays
That is the core trick. Stateless can wrap an object through delegates, read the current state, fire a trigger, and write the new value back out. The machine itself does not need to persist, which makes it a good fit for request-scoped services, tests, and ORM-backed domain objects.
That design is also why it feels more like a rules engine than a framework. You are not teaching a big runtime how to run your app. You are describing a finite set of legal moves, then letting the library enforce them.
Why that design beats another layer of if/else
Without a state machine, this logic usually degenerates into nested conditionals, flags, and a few brittle helper methods. Stateless keeps the control flow explicit: a state, a trigger, and a permitted transition. The code reads like a policy instead of a rescue operation.
| Approach | Strength | Trade-off |
|---|---|---|
| Nested if/else | Fast to start | Hard to verify, easy to break as states multiply |
| Stateless | Readable transitions with explicit rules | You still model your own domain states and triggers |
| Workflow engine | Good for long-running orchestration | Heavier than most in-process business rules need |
var phoneCall = new StateMachine<State, Trigger>(
getState: () => call.State,
setState: s => call.State = s);
phoneCall.Configure(State.OffHook)
.Permit(Trigger.CallDialled, State.Ringing);
phoneCall.Configure(State.Ringing)
.Permit(Trigger.Catch, State.Connected)
.Permit(Trigger.Timeout, State.OffHook);
That is the whole pitch in miniature. The object stays ordinary. The transitions become formal.
Inside the fluent API
The public surface is intentionally small. `StateMachine
The result is declarative without being magical. You are not configuring a separate designer file. You are writing executable rules in C# that can be reviewed, unit tested, and refactored like any other code.
Queued firing is what keeps it honest
One subtle design choice does a lot of work here: queued firing. If a trigger is fired from inside an entry action, Stateless can queue it instead of letting the machine re-enter itself mid-transition. That protects the current transition from being corrupted by accidental recursion.
In practice, that means the engine behaves like a run-to-completion model. It finishes one move cleanly before it starts the next one. For business logic, that is usually the difference between predictable and mysterious.
Hierarchical states are the feature that makes it feel grown up
This is where Stateless moves beyond a simple finite state machine. With `SubstateOf`, a child state can inherit behavior from a superstate. If the child does not handle a trigger, the library walks upward and asks the parent.
That means common behavior lives once. You do not repeat the same cancel, timeout, or escape logic across every leaf state. The hierarchy becomes the place where structure lives, and the trigger resolver becomes the mechanism that respects it.
That is also why the library feels safer than a pile of mutually aware states. The relationship is explicit. The resolver knows where to look next, and the child does not need to know every rule the parent already owns.
The code-first documentation loop
The `Graph` namespace closes a nice loop. The same configuration that runs the state machine can also export Mermaid or DOT. In other words, the code is not merely the source of truth. It is also the source of the diagram.
That is more than convenience. It reduces drift between implementation and documentation, which is where a lot of modeling tools quietly fail. If the graph can be generated from the rules, the picture stops being a stale artifact.
| Representation | What it gives you | Where it falls short |
|---|---|---|
| Hand-drawn diagram | Easy to discuss | Drifts from code fast |
| Executable Stateless config | Always matches behavior | Requires reading code |
| Generated Mermaid or DOT | Readable and reproducible | Still needs a place to render |
Where Stateless sits among alternatives
Stateless occupies a very specific middle ground. It is lighter than workflow engines, easier to approach than more configurable FSM libraries, and better suited to ordinary domain objects than orchestration-heavy platforms.
| Library | Best fit | Why you would choose it |
|---|---|---|
| Stateless | Embedded domain transitions | Fluent, lightweight, delegate-backed state |
| Automatonymous | Distributed sagas | Message-driven orchestration inside MassTransit |
| Appccelerate.StateMachine | Highly configurable FSMs | More knobs, more setup |
| Workflow-Core | Long-running workflows | Persistence and step orchestration |
| LiquidState | Async and performance-heavy paths | Different trade-offs, smaller adoption |
That is the right frame for the comparison. Stateless is not trying to win every category. It is trying to stay small enough that you can trust it inside a normal .NET codebase.
Why this project lasted
Projects like this survive when the abstraction keeps paying rent. Nicholas Blumhardt stepped back from active stewardship and handed the project forward, but the original motivation still explains why people keep using it: avoid heavyweight workflow complexity, keep the rules explicit, and let the domain object remain itself.