Skip to content

State Machine States

Saga state machines are stateful consumers designed to retain the outcome of preceding events when a subsequent event is consumed. The current state is stored within a saga state machine instance. A saga state machine instance can only be in one state at a given time.

A newly created saga state machine instance starts in the internally defined Initial state. When a saga state machine has completed, an instance should transition to the Final state (which is also already defined).

The Initial state is the starting point of all saga state machine instances. When an existing saga state machine instance cannot be found that correlates to an event behavior defined for the Initial state, a new instance is created. The defined behavior should initialize the newly created instance and transition the instance to the next state.

The Final state is the last (or terminal) state that a saga state machine instance should transition to when the instance has completed. When an instance is in the Final state, no further events should be handled.

To remove the saga state machine instance from the repository when the instance is in the Final state, specify SetCompletedWhenFinalized() in the saga state machine.

public class OrderStateMachine :
MassTransitStateMachine<OrderState>
{
public State Submitted { get; private set; } = null!;
public State Accepted { get; private set; } = null!;
public OrderStateMachine()
{
SetCompletedWhenFinalized();
}
}

To remove the saga state machine instance from the repository when the instance is in any other state, use the SetCompleted() method and pass a predicate to determine if the state machine instance is completed.

protected void SetCompleted(Func<TInstance, Task<bool>> completed);
protected void SetCompleted(Func<BehaviorContext<TInstance>, Task<bool>> completed);

States are declared as public properties on the saga state machine with the State property type. In the example below, two states are defined: Submitted and Accepted.

public class OrderState :
SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
/// <summary>
/// The saga state machine instance current state
/// </summary>
public string CurrentState { get; set; }
}
public class OrderStateMachine :
MassTransitStateMachine<OrderState>
{
public State Submitted { get; private set; } = null!;
public State Accepted { get; private set; } = null!;
}

When configuring saga state machine behavior for an event, the last activity configured is usually TransitionTo, signaling the transition of the state machine instance to the next state. By transitioning to another state, the saga state machine adapts its behavior so that the next event consumed will pick up in the new state and execute the appropriate behavior for the event in that state.

For example, when an OrderSubmitted event is consumed by a new saga state machine instance, the saga state machine will transition to the Submitted state.

public record OrderSubmitted(Guid OrderId, string CustomerNumber);
public class OrderStateMachine :
MassTransitStateMachine<OrderState>
{
public Event OrderSubmitted<OrderSubmitted> { get; private set; } = null!;
public State Submitted { get; private set; } = null!;
public State Accepted { get; private set; } = null!;
public OrderStateMachine()
{
Initially(
// Event is consumed, new instance is created in Initial state
When(OrderSubmitted)
// copy some data from the event to the saga
.Then(context => context.Saga.CustomerNumber = context.Message.CustomerNumber)
// transition to the Submitted state
.TransitionTo(Submitted)
);
}
}

Saga state machine instances are persisted after all state machine activities have completed. If there were additional activities after the TransitionTo, such as a Publish, those activities execute before the instance is persisted. After all the activities have completed, the instance is persisted and the message is acknowledged with the message broker.

In the example below, the newly created saga state machine instance would not be persisted because of the exception thrown by the activity following the first TransitionTo call.

public class OrderStateMachine :
MassTransitStateMachine<OrderState>
{
public State Submitted { get; private set; } = null!;
public State Accepted { get; private set; } = null!;
public OrderStateMachine()
{
Initially(
When(OnSubmit)
.Then(context => context.Saga.CustomerNumber = context.Message.CustomerNumber)
.TransitionTo(Submitted)
.Then(context => throw new InvalidOperationException())
.TransitionTo(Accepted)
);
}
}