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