Stateful Sagas Edit on GitHub


As is so common in these docs, I would direct you to this from the old "EIP" book: Process Manager. A stateful saga in Jasper is used to coordinate long running workflows or to break large, logical transactions into a series of smaller steps. A stateful saga consists of a couple parts:

  1. A saga state document type that is persisted between saga messages
  2. A saga message handler that inherits from StatefulSagaOf<T>, where the "T" is the saga state document type
  3. A saga persistence strategy registered in Jasper that knows how to load and persist the saga state documents

Right now the only options for saga persistence are the default in memory model and an option that uses Marten and Postgresql for persistence. Other options are planned for Entity Framework and Dapper.

Inspired by Jimmy Bogard's example of ordering at a fast food restaurant, let's say that we are building a Jasper system to manage filling the order of Happy Meals at McDonald's. When you place an order in our McDonald's, various folks gather up the various parts of the order until it is completed, then calls out to the customer that the order is ready. In our system, we want to build out a saga handler for this work that can parallelize and coordinate all the work necessary to deliver the Happy Meal to our customers.

As a first step, let's say that the process of building out a Happy Meal starts with receiving this message shown below:


public class HappyMealOrder
{
    public string Drink { get; set; }
    public string Toy { get; set; }
    public string SideDish { get; set; }
    public string MainDish { get; set; }
}

Next, we need to track the current state of our happy meal order with a new document type that will be persisted across different messages being handled by our saga:


public class HappyMealOrderState
{
    // Jasper wants you to make the saga state
    // document have an "Id" property, but
    // that can be overridden
    public int Id { get; set; }
    public HappyMealOrder Order { get; set; }

    public bool DrinkReady { get; set; }
    public bool ToyReady { get; set; }
    public bool SideReady { get; set; }
    public bool MainReady { get; set; }

    // The order is complete if *everything*
    // is complete
    public bool IsOrderComplete()
    {
        return DrinkReady && ToyReady && SideReady && MainReady;
    }
}

Finally, let's add a new saga handler for our new state document and a single message handler action to start the saga:


public class HappyMealSaga : StatefulSagaOf<HappyMealOrderState>
{
    private int _orderIdSequence;

    // This is a little bit cute, but the HappyMealOrderState type
    // is known to be the saga state document, so it'll be treated as
    // the state document, while the object[] will be treated as
    // cascading messages
    public (HappyMealOrderState, object[]) Starts(HappyMealOrder order)
    {
        var state = new HappyMealOrderState
        {
            Order = order,
            Id = ++_orderIdSequence
        };

        return (state, chooseActions(order, state.Id).ToArray());
    }

    private IEnumerable<object> chooseActions(HappyMealOrder order, int stateId)
    {
        // choose the outgoing messages to other systems -- or the local
        // system tracking all this -- to start having this happy meal
        // order put together

        if (order.Drink == "Soda") yield return new SodaRequested {OrderId = stateId};

        // and others
    }
}

There's a couple things to note about the Starts() method up above:

  • When Jasper sees a method named Start or Starts, that indicates to Jasper that this is a possible beginning for a stateful saga and that the state document does not already exist
  • HappyMealOrderState is one of the types returned as a C# 7 tuple from this method, and Jasper will treat this as the state document for the saga and the saga persistence will save the document as part of executing that message. You could also just return the HappyMealOrderState and use IMessageContext to explicitly send other messages
  • The object[] part of the tuple are just cascading messages that would be sent out to initiate work like "go fill the soda" or "go find the right toy the child asked for"

If you're uncomfortable with C# tuples or just don't like the magic, you can effect the same outgoing messages by using this alternative:



public async Task<HappyMealOrderState> Starts(
    HappyMealOrder order, // The first argument is assumed to be the message type
    IMessageContext context) // Additional arguments are assumed to be services
{
    var state = new HappyMealOrderState
    {
        Order = order,
        Id = ++_orderIdSequence
    };

    if (order.Drink == "Soda") await context.Send(new SodaRequested {OrderId = state.Id});

    // And other outgoing messages to coordinate gathering up the happy meal

    return state;
}

Both the of the examples above assume that the SodaRequested messages will be sent to other systems, but it's perfectly possible to use a stateful saga to manage processing that's handled completely within your system like this:



public async Task<HappyMealOrderState> Starts(
    HappyMealOrder order, // The first argument is assumed to be the message type
    IMessageContext context) // Additional arguments are assumed to be services
{
    var state = new HappyMealOrderState
    {
        Order = order,
        Id = ++_orderIdSequence
    };

    if (order.Drink == "Soda") await context.Enqueue(new SodaRequested {OrderId = state.Id});

    // And other outgoing messages to coordinate gathering up the happy meal

    return state;
}

Updating and Completing Saga State

As we saw above, methods named Start or Starts are assumed to create a brand new state document for a logical saga. The next step is to handle additional methods that perform additional work within the saga, update the state document, and potentially close out the saga.

Related to the saga above, let's say that we receive a SodaFetched message that denotes that the soda we requested at the onset of the saga is ready. We'll need to update the state to mark that the drink is ready, and if all the parts of the happy meal are ready, we'll tell some kind of IOrderService to close the order and mark the saga as complete. That handler mthod could look like this:


public void Handle(
    SodaFetched soda, // The first argument is the message type
    HappyMealOrderState state, // This matches the state document type, so it will be the
    // state document matching the current saga identified by
    // the incoming message envelope
    IOrderService service // Additional arguments are injected services
)
{
    state.DrinkReady = true;

    // Determine if the happy meal is completely ready
    if (state.IsOrderComplete())
    {
        // Maybe you need to remove this
        // order from some kind of screen display
        service.Close(state.Id);

        // And we're done here, so let's mark the Saga as complete
        MarkCompleted();
    }
}

Things to note in the sample up above:

  • The method name can be any of the valid handler methods outlined in Message Handler Discovery
  • The saga state document HappyMealOrderState is passed in as a method argument. The surrounding saga persistence in Jasper is loading that document for you based on either the incoming envelope metadata or the message as explained in the next section
  • If the StatefulSagaOf<T>.MarkCompleted() method is called while handling the message, the state document will be deleted from storage after the message handler is called

Saga State Identity

One way or another, Jasper has to be able to correlate that the SodaFetched message coming in to the message handler above is related to a certain persisted state document by the id of that state document. Jasper has a couple ways to do that:

If the message is being passed to the local system or exchanging messages with an external Jasper system, there's an Envelope.SagaId property that gets propagated on every message and response that Jasper can use automatically to do the state document to message correlation. For example, if the SodaRequested message is sent to another system running Jasper that replies to our system with a corresponding SodaFetched message in a handler like this:


// This message handler is in another system responsible for
// filling sodas
public class SodaHandler
{
    public SodaFetched Handle(SodaRequested requested)
    {
        // get the soda, then return the update message
        return new SodaFetched();
    }
}

If you are receiving messages from an external system or don't want to or can't depend on that envelope metadata, you can pass the identity of the saga state document in the message itself. Jasper always checks the Envelope.SagaId value first, but failing that it falls back to looking for a property named SagaId on the incoming message type like this:


public class BurgerReady
{
    // By default, Jasper is going to look for a property
    // called SagaId as the identifier for the stateful
    // document
    public int SagaId { get; set; }
}

Or if you want to use a different property name, you can override that with an attribute like this:


public class ToyOnTray
{
    // There's always *some* reason to deviate,
    // so you can use this attribute to tell Jasper
    // that this property refers to the Id of the
    // Saga state document
    [SagaIdentity] public int OrderId { get; set; }
}

To add some context, let's see these two messages in context:


public void Handle(ToyOnTray toyReady, HappyMealOrderState state)
{
    state.ToyReady = true;
    if (state.IsOrderComplete()) MarkCompleted();
}

public void Handle(BurgerReady burgerReady, HappyMealOrderState state)
{
    state.MainReady = true;
    if (state.IsOrderComplete()) MarkCompleted();
}

If you were using the Marten-backed saga persistence, the code above would result in the HappyMealOrderState document being loaded with the value in BurgerReady.SagaId or ToyOnTray.OrderId as the document id.

Right now, Jasper supports the following types as valid saga state document identity types:

  • int
  • long
  • System.Guid
  • string