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).

Initial State

The Initial state is the starting point of all sagas. 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 to the next state.

Final 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.

SetCompletedWhenFinalized

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();
    }
}

Declaring States

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.

The MassTransitStateMachine base class automatically initializes State properties in its constructor, so they don't need to be explicitly initialized.
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!;
}

Instance State

In the example above, the CurrentState property on the saga state machine instance is used to store the instance's current state. The saga state machine must be configured to use that property.

String Instance State

The InstanceState method is used to configure the property, in the example below the string property type is used.

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

    public OrderStateMachine() 
    {
        InstanceState(x => x.CurrentState);
    }
}

Integer Instance State

In addition to using a string, and int can also be used to store the current state. An integer can be more efficient to store in a database compared to a verbose string value. In the example below the int property type is used instead.

public class OrderState : 
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }

    public int CurrentState { get; set; }
}

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

    public OrderStateMachine() 
    {
        InstanceState(x => x.CurrentState, Submitted, Accepted);
    }
}

Transitioning States

When configuring saga state machine behavior for an event, the last activity configured is usually a TransitionTo a 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)
        );
    }
}

TransitionTo Anti-Pattern

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)
        );
    }
}