# Saga State Machine Foundation

## Goal

Build a saga state machine that handles long-running workflows safely under duplicate and out-of-order delivery.

## Agent enforcement rules

### Do

- Prefer `record` message contracts for saga events/commands. Classes and interfaces are supported when required by an existing contract or interoperability.
- Correlate every event explicitly and model transitions deterministically.
- Configure saga persistence/repository explicitly for the environment.

### Do not

- Do not assume event ordering or ignore duplicate delivery.
- Do not create implicit state transitions outside the state machine.

## Build checklist

- Define saga state with `CorrelationId` and `CurrentState`.
- Capture domain identifiers/timestamps needed for recovery and diagnostics.
- Correlate every event explicitly.
- Model all important transitions: start, progress, complete, fail, timeout.
- Define missing-instance behavior where request/response messages are involved.
- Configure persistence/repository explicitly for your transport and environment.

## Example: state + state machine

```csharp
public record SubmitOrder(Guid OrderId);
public record OrderAccepted(Guid OrderId);

public class OrderState : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; } = default!;
    public DateTime? SubmittedAt { get; set; }
}

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

    public Event<SubmitOrder> SubmitOrder { get; private set; } = default!;
    public Event<OrderAccepted> OrderAccepted { get; private set; } = default!;

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

        Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
        Event(() => OrderAccepted, x => x.CorrelateById(context => context.Message.OrderId));

        Initially(
            When(SubmitOrder)
                .Then(context => context.Saga.SubmittedAt = InVar.Timestamp)
                .TransitionTo(Submitted));

        During(Submitted,
            When(OrderAccepted)
                .TransitionTo(Accepted)
                .Finalize(),
            Ignore(SubmitOrder));

        SetCompletedWhenFinalized();
    }
}
```

## Example: registration + persistence

```csharp
services.AddMassTransit(x =>
{
    x.AddSagaStateMachine<OrderStateMachine, OrderState>()
        .EntityFrameworkRepository(r =>
        {
            r.ExistingDbContext<OrderStateDbContext>();
            r.UsePostgres();
        });

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.ConfigureEndpoints(context);
    });
});
```

## Example: schedule timeout in a state machine

```csharp
public record OrderCompletionTimeoutExpired(Guid OrderId);

public class OrderStateWithTimeout : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; } = default!;
    public Guid? OrderCompletionTimeoutTokenId { get; set; }
}

public class OrderStateMachineWithTimeout : MassTransitStateMachine<OrderStateWithTimeout>
{
    public Schedule<OrderStateWithTimeout, OrderCompletionTimeoutExpired> OrderCompletionTimeout { get; private set; } = default!;
    public Event<SubmitOrder> SubmitOrder { get; private set; } = default!;

    public OrderStateMachineWithTimeout()
    {
        Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
        Schedule(() => OrderCompletionTimeout, x => x.OrderCompletionTimeoutTokenId, s =>
        {
            s.Delay = TimeSpan.FromDays(30);
            s.Received = r => r.CorrelateById(context => context.Message.OrderId);
        });

        Initially(When(SubmitOrder)
            .Schedule(OrderCompletionTimeout, context => context.Init<OrderCompletionTimeoutExpired>(context.Message)));

        DuringAny(When(OrderCompletionTimeout.Received).Finalize());
        SetCompletedWhenFinalized();
    }
}
```

Configure a transport message scheduler or a scheduler integration before scheduling saga events. See the [scheduler configuration](/configuration/schedulers).

## Guardrails

- Never assume event ordering unless guaranteed by design.
- Always handle duplicate events idempotently.
- Keep transition logic deterministic and explicit.
- Do not create implicit state transitions in ad-hoc consumer code.

## Default implementation shape

1. Saga state class (`SagaStateMachineInstance`).
2. State machine class (`MassTransitStateMachine<TState>`).
3. Registration + persistence configuration.
4. Tests for progression, duplicate delivery, and timeout/fault paths.

## Verification

- `dotnet build`
- `dotnet test --filter Saga`
- `dotnet test --filter Correlation`

## References

- [Saga State Machines (Concepts)](/concepts/saga-state-machines)
- [Saga Configuration](/configuration/saga)
- [Saga State Machines Guide](/guides/saga-state-machines)
- [Schedule Event Guide](/guides/saga-state-machines/schedule-event)
