Skip to main content
Version: 2.4.1

State Machines (MassTransit Sagas)

RCommon provides a lightweight state machine abstraction that can be backed by either the Stateless library or a MassTransit-compatible dictionary-based implementation. The MassTransit adapter (RCommon.MassTransit.StateMachines) integrates with the RCommon builder pipeline and is appropriate for orchestrating multi-step workflows in messaging scenarios.

Overview

A state machine tracks the lifecycle of a long-running process. It defines:

  • States — discrete conditions the process can be in (e.g. Pending, Processing, Completed)
  • Triggers — events that cause transitions between states (e.g. Submit, Approve, Reject)
  • Transitions — rules that map a (state, trigger) pair to a destination state, optionally guarded by a condition
  • Entry/exit actions — async callbacks invoked when the machine enters or leaves a state

The MassTransit state machine adapter is independent of MassTransit Automatonymous sagas. It uses the same IStateMachineConfigurator<TState, TTrigger> abstraction as the Stateless adapter, so you can swap implementations without changing business logic.

Installation

NuGet Package
dotnet add package RCommon.MassTransit.StateMachines

Configuration

Register the MassTransit state machine adapter with WithMassTransitStateMachine:

using RCommon;

builder.Services.AddRCommon()
.WithMassTransitStateMachine();

This registers MassTransitStateMachineConfigurator<TState, TTrigger> as the IStateMachineConfigurator<TState, TTrigger> implementation. The registration is open-generic, so any combination of state and trigger enum types is supported without further configuration.

Defining states and triggers

States and triggers are plain enums:

public enum OrderState
{
Pending,
Approved,
Shipped,
Cancelled
}

public enum OrderTrigger
{
Approve,
Ship,
Cancel
}

Configuring transitions

Inject IStateMachineConfigurator<TState, TTrigger>, call ForState for each state, then call Build to produce a running machine instance:

public class OrderWorkflow
{
private readonly IStateMachineConfigurator<OrderState, OrderTrigger> _configurator;

public OrderWorkflow(IStateMachineConfigurator<OrderState, OrderTrigger> configurator)
{
_configurator = configurator;

_configurator.ForState(OrderState.Pending)
.Permit(OrderTrigger.Approve, OrderState.Approved)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled);

_configurator.ForState(OrderState.Approved)
.Permit(OrderTrigger.Ship, OrderState.Shipped)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled)
.OnEntry(async ct =>
{
// side-effect when order is approved
await NotifyWarehouseAsync(ct);
});

_configurator.ForState(OrderState.Shipped)
.OnEntry(async ct =>
{
await SendShippingConfirmationAsync(ct);
});
}

public IStateMachine<OrderState, OrderTrigger> CreateFor(OrderState currentState)
=> _configurator.Build(currentState);
}

Guarded transitions

Use PermitIf to add a condition to a transition:

_configurator.ForState(OrderState.Approved)
.PermitIf(OrderTrigger.Ship, OrderState.Shipped, () => inventoryAvailable)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled);

The guard is a synchronous Func<bool>. If the guard returns false the trigger cannot fire from that state.

Firing triggers

Once a machine instance is built, fire triggers asynchronously:

var machine = _orderWorkflow.CreateFor(order.State);

if (machine.CanFire(OrderTrigger.Approve))
{
await machine.FireAsync(OrderTrigger.Approve, cancellationToken);
order.State = machine.CurrentState; // persist new state
}

You can also pass data with a trigger:

await machine.FireAsync(OrderTrigger.Ship, shipmentDetails, cancellationToken);

Note that the MassTransit dictionary-based adapter accepts parameterized trigger calls but does not use the data parameter — the transition executes identically to a parameterless fire. Use this overload for interface compatibility.

IStateMachine members

MemberDescription
CurrentStateThe current state of the machine
CanFire(trigger)Returns true if the trigger is permitted from the current state
PermittedTriggersAll triggers that can currently be fired
FireAsync(trigger, ct)Fires a trigger, executing exit/entry actions and transitioning state
FireAsync(trigger, data, ct)Fires a trigger with associated data

IStateConfigurator members

MethodDescription
Permit(trigger, dest)Adds an unconditional transition
PermitIf(trigger, dest, guard)Adds a guarded transition that fires only when guard() returns true
OnEntry(action)Registers an async action invoked when this state is entered
OnExit(action)Registers an async action invoked when this state is exited

Comparison with Stateless

Both adapters implement the same IStateMachineConfigurator<TState, TTrigger> and IStateMachine<TState, TTrigger> interfaces. The choice affects the underlying engine:

AspectRCommon.MassTransit.StateMachinesRCommon.Stateless
Backing libraryCustom dictionary-based FSMStateless NuGet
Parameterized triggersAccepted but data is ignoredFully supported via SetTriggerParameters
Configuration reuseSingle configurator produces independent machine instancesSingle configurator produces independent machine instances
Registration methodWithMassTransitStateMachine()WithStatelessStateMachine()

For full parameterized trigger support, prefer the Stateless adapter. See Stateless.

API summary

TypePackageDescription
MassTransitStateMachineConfigurator<TState, TTrigger>RCommon.MassTransit.StateMachinesImplements IStateMachineConfigurator; builds MassTransitStateMachine instances
MassTransitStateMachine<TState, TTrigger>RCommon.MassTransit.StateMachinesDictionary-based IStateMachine implementation
MassTransitStateConfigurator<TState, TTrigger>RCommon.MassTransit.StateMachinesPer-state configuration storing transitions and actions
IStateMachineConfigurator<TState, TTrigger>RCommon.CoreAbstraction for building state machines
IStateMachine<TState, TTrigger>RCommon.CoreRunning state machine abstraction
IStateConfigurator<TState, TTrigger>RCommon.CorePer-state transition and action configuration
WithMassTransitStateMachine()RCommon.MassTransit.StateMachinesExtension method on IRCommonBuilder
RCommonRCommon