Rhino Service Bus: Saga and State
In a messaging system, a saga orchestrate a set of messages. The main benefit of using a saga is that it allows us to manage the interaction in a stateful manner (easy to think and reason about) while actually working in a distributed and asynchronous environment.
In Rhino Service Bus, I built the notion of sagas from the beginning. And I initially went with the approach that mix the saga’s behavior and the saga state in the same class. That did not turn out so well. While it works for simple matters, anything of sufficient complexity started to bring issue. Mainly, it was an issue of managing dependencies and managing state. It is possible to get this worked out, but I decided to follow Udi’s footsteps and create an explicit separation between the two.
Here is how it works, we have the state class:
1: public class BaristaState
2: {
3: public bool DrinkIsReady { get; set; }
4:
5: public bool GotPayment { get; set; }
6:
7: public string Drink { get; set; }
8: }
This is just a standard class, nothing special here. But here is the actual saga class. This contains the behavior for the saga, with the state being maintained in the state class.
1: public class BaristaSaga :
2: ISaga<BaristaState>,
3: InitiatedBy<PrepareDrink>,
4: Orchestrates<PaymentComplete>
5: {
6: private readonly IServiceBus bus;
7:
8: public BaristaState State { get; set; }
9:
10: public Guid Id { get; set; }
11:
12: public bool IsCompleted { get; set; }
13:
14: public BaristaSaga(IServiceBus bus)
15: {
16: this.bus = bus;
17: State = new BaristaState();
18: }
19:
20: public void Consume(PrepareDrink message)
21: {
22: State.Drink = message.DrinkName;
23:
24: for (int i = 0; i < 10; i++)
25: {
26: Console.WriteLine("Barista: preparing drink: " + drink);
27: Thread.Sleep(500);
28: }
29: State.DrinkIsReady = true;
30: SubmitOrderIfDone();
31: }
32:
33: public void Consume(PaymentComplete message)
34: {
35: Console.WriteLine("Barista: got payment notification");
36: State.GotPayment = true;
37: SubmitOrderIfDone();
38: }
39:
40: private void SubmitOrderIfDone()
41: {
42: if (State.GotPayment && State.DrinkIsReady)
43: {
44: Console.WriteLine("Barista: drink is ready");
45: bus.Publish(new DrinkReady
46: {
47: CorrelationId = Id,
48: Drink = State.Drink
49: });
50: IsCompleted = true;
51: }
52: }
53: }
There are a few things to notice in here. The saga class is a standard component, we use DI to inject dependencies to the class so it can perform whatever it is that it wants. The state property is used to maintain the state of the saga between message invocations.
This results in a simpler design for some parts of the code, and I think that overall it is a very simple model to talk and reason about.