Saga State Machine Activities

There are scenarios when an event behavior may have dependencies that need to be managed at a scope level, such as a database connection, or the complexity is best encapsulated in a separate class rather than being part of the state machine itself. Developers can create their own activities for state machine use, and optionally create their own extension methods to add them to a behavior.

Key use cases include the need for dependency injection within the current scope or the desire to encapsulate complex logic into a single activity.

Calling a Custom Activity

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

    public Event<OrderClosed> OrderClosed { get; private set; } = null!;

    public OrderStateMachine() 
    {
        // Tell the saga where to store the current state
        InstanceState(x => x.CurrentState);

        Initially(
            When(OrderClosed)
                .Activity(x => x.OfType<OrderClosedActivity>())
                .TransitionTo(Submitted)
        );
    }
}

Creating a Custom Activity

To create an activity, create a class that implements IStateMachineActivity<TInstance, TData> as shown. This class has full access to the dependency container, and all services will be resolved from the current scope.

public class OrderClosedActivity :
    IStateMachineActivity<OrderState, OrderClosed>
{
    readonly ISomeService _service;

    public OrderClosedActivity(ISomeService service)
    {
        _service = service;
    }

    public async Task Execute(
        BehaviorContext<OrderState, OrderClosed> context,
        IBehavior<OrderState, SubmitOrder> next)
    {
        await _service.OnOrderClosed(context.Saga.CorrelationId);
        
        // always call the next activity in the behavior
        await next.Execute(context).ConfigureAwait(false);
    }

    public Task Faulted<TException>(
        BehaviorExceptionContext<OrderState, OrderClosed, TException> context, 
        IBehavior<OrderState, OrderClosed> next
        )
        where TException : Exception
    {
        // always call the next activity in the behavior
        return next.Faulted(context);
    }


    public void Probe(ProbeContext context)
    {
        context.CreateScope("publish-order-closed");
    }

    public void Accept(StateMachineVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Handling Any Event Type

In the above example, the event type was known in advance. If an activity for any event type is needed, it can be created without specifying the event type.

public class OrderClosedActivity :
    IStateMachineActivity<OrderState>
{
    readonly ISomeService _service;

    public OrderClosedActivity(ISomeService service)
    {
        _service = service;
    }

    public async Task Execute(BehaviorContext<OrderState> context, IBehavior<OrderState> next)
    {
        await _service.OnOrderClosed(context.Saga.CorrelationId);

        // always call the next activity in the behavior
        await next.Execute(context).ConfigureAwait(false);
    }

    public async Task Execute<T>(BehaviorContext<OrderState, T> context, IBehavior<OrderState, T> next)
    {
        await _service.OnOrderClosed(context.Saga.CorrelationId);

        // always call the next activity in the behavior
        await next.Execute(context).ConfigureAwait(false);
    }

    public Task Faulted<TException>(BehaviorExceptionContext<OrderState, TException> context, IBehavior<OrderState> next) 
        where TException : Exception
    {

        // always call the next activity in the behavior
        return next.Faulted(context);
    }

    public Task Faulted<T, TException>(BehaviorExceptionContext<OrderState, T, TException> context, IBehavior<OrderState, T> next)
        where TException : Exception
    {

        // always call the next activity in the behavior
        return next.Faulted(context);
    }

    public void Probe(ProbeContext context)
    {
        context.CreateScope("publish-order-closed");
    }

    public void Accept(StateMachineVisitor visitor)
    {
        visitor.Visit(this);
    }
}