Table of Contents

Class StateMachineExtensions

Namespace
Trellis.StateMachine
Assembly
Trellis.StateMachine.dll

Provides extension methods for Stateless.StateMachine<TState, TTrigger> that return Result<TValue> instead of throwing on invalid transitions.

public static class StateMachineExtensions
Inheritance
StateMachineExtensions
Inherited Members

Remarks

These extensions pre-check the trigger with Stateless.StateMachine<TState, TTrigger>.CanFire(TTrigger) (which honors PermitIf/IgnoreIf guards) and translate disallowed transitions into an Error.InvariantViolation (HTTP 422) — a rejected transition is a domain-invariant breach against the aggregate's current state, not inbound-input validation or a concurrent-modification conflict. Exceptions thrown by user-supplied entry/exit/transition actions are not swallowed.

These extensions do not change the concurrency model of Stateless.StateMachine<TState, TTrigger>. Stateless state machines are not thread-safe, so concurrent calls to FireResult<TState, TTrigger>(StateMachine<TState, TTrigger>, TTrigger) on the same machine instance must still be externally synchronized. Because Stateless is single-threaded by contract, the CanFire+Fire pre-check pattern is race-free when used as documented.

Usage with Railway Oriented Programming:

var machine = new StateMachine<OrderState, OrderTrigger>(OrderState.New);
machine.Configure(OrderState.New)
    .Permit(OrderTrigger.Submit, OrderState.Submitted);

Result<OrderState> result = machine.FireResult(OrderTrigger.Submit);

Methods

FireResult<TState, TTrigger>(StateMachine<TState, TTrigger>, TTrigger)

Fires the trigger and returns the outcome as a Result<TValue>. Short-circuits with Error.InvariantViolation when the transition is not permitted in the current state (i.e., CanFire returns false), without invoking any consumer-registered OnUnhandledTrigger callback. Consumers who want their OnUnhandledTrigger callback to run must call Fire directly — FireResult is the guarded entry point that prefers a typed Error.InvariantViolation over running side-effect code.

public static Result<TState> FireResult<TState, TTrigger>(this StateMachine<TState, TTrigger> stateMachine, TTrigger trigger) where TState : notnull where TTrigger : notnull

Parameters

stateMachine StateMachine<TState, TTrigger>

The state machine to fire the trigger on.

trigger TTrigger

The trigger to fire.

Returns

Result<TState>

A Result<TValue> containing the new state if the transition is valid, or an Error.InvariantViolation with reason code state-machine.invalid-transition if the trigger cannot be fired from the current state or a guard throws InvalidOperationException.

Type Parameters

TState

The type representing the states of the state machine.

TTrigger

The type representing the triggers/events of the state machine.

Examples

var machine = new StateMachine<State, Trigger>(State.Idle);
machine.Configure(State.Idle).Permit(Trigger.Start, State.Running);

// Valid transition: Idle permits Trigger.Start, advancing the machine to Running.
Result<State> result = machine.FireResult(Trigger.Start);
// result.IsSuccess == true; result holds State.Running.

// Invalid transition: Running has no transition configured for Trigger.Pause.
Result<State> invalid = machine.FireResult(Trigger.Pause);
// invalid.IsFailure == true; invalid.Error is Error.InvariantViolation.

Remarks

Pre-checks with Stateless.StateMachine<TState, TTrigger>.CanFire(TTrigger) — which honors PermitIf/IgnoreIf guards — and only invokes Stateless.StateMachine<TState, TTrigger>.Fire(TTrigger) when the transition is permitted. This avoids both Stateless exception-message parsing and invoking consumer OnUnhandledTrigger callbacks from the typed-result path.

HTTP semantics. An invalid state-machine transition is a domain-invariant breach (the aggregate cannot honor the requested action from its current state), not inbound-input validation or a concurrent-modification conflict — retry will not succeed. The returned error is therefore Error.InvariantViolation (HTTP 422), not Error.InvalidInput or Error.Conflict (HTTP 409). Callers can distinguish state-machine rejections from other 422s by matching on the Code value state-machine.invalid-transition.

InvalidOperationException thrown while evaluating a guard is converted to Error.InvariantViolation. Exceptions thrown by user entry, exit, or transition actions are not swallowed — they propagate to the caller.

The underlying Stateless.StateMachine<TState, TTrigger> remains not thread-safe, so callers must not invoke this method concurrently on the same machine instance without synchronization.