-
Notifications
You must be signed in to change notification settings - Fork 7
Sagas
###Overview Sagas are used for orchestrate logic involving several aggregates. Sometimes it is also called "Process Manager" for short-living sagas. GridDomain use only one term "Saga" both for short- and long-living processes. Saga consist of two pats: behavior, or flow, and data. Behavior is determined by state machine based on Automatonymous state machine. Data is any poco class available in all machine states both for reading and writing. It is highly recommended to read how to configure Automatonymous in it quick start guide
###Creating saga To create saga first we have to create saga data class, for example:
class SoftwareProgrammingSagaData: ISagaState
{
public Guid PersonId { get; set; }
public string CurrentStateName{ get; set; }
public Guid CoffeeMachineId { get; }
public Guid SofaId { get; }
}
Only restriction to saga data classes is to implement ISagaState interface. It contains property to store machine current state and persist it between saga incarnations.
Second we have to create saga behavior as a state machine. Keep in mind that saga itself does not contain any data or current state, it fully relies on external data state. Use base class InstanceSagas.Saga for your sagas, where TData is type of saga data class.
class SoftwareProgrammingSaga: Saga<SoftwareProgrammingSagaData>
{
public SoftwareProgrammingSaga()
{
Event(() => GotTired);
Event(() => CoffeReady);
Event(() => SleptWell);
Event(() => CoffeNotAvailable);
State(() => Coding);
State(() => MakingCoffee);
State(() => Sleeping);
Command<MakeCoffeCommand>();
Command<GoSleepCommand>();
During(Coding,
When(GotTired).Then(context =>
{
var sagaData = context.Instance;
var domainEvent = context.Data;
sagaData.PersonId = domainEvent.SourceId;
var soloLogger = LogManager.GetLogger();
soloLogger.Trace("Hello trace string");
Dispatch(new MakeCoffeCommand(domainEvent.SourceId,sagaData.CoffeeMachineId));
})
.TransitionTo(MakingCoffee));
During(MakingCoffee,
When(CoffeNotAvailable)
.Then(context =>
Dispatch(new GoSleepCommand(context.Data.ForPersonId, context.Instance.SofaId)))
.TransitionTo(Sleeping),
When(CoffeReady)
.TransitionTo(Coding));
During(Sleeping,
When(SleptWell).TransitionTo(Coding));
}
public Event<GotTiredDomainEvent> GotTired { get; private set; }
public Event<CoffeMadeDomainEvent> CoffeReady { get; private set; }
public Event<SleptWellDomainEvent> SleptWell { get; private set; }
public Event<CoffeMakeFailedDomainEvent> CoffeNotAvailable { get; private set; }
public State Coding { get; private set; }
public State MakingCoffee { get; private set; }
public State Sleeping { get; private set; }
}
###Saga instances To run sagas GridDomain use special abstraction - saga instance. It is pair of saga and saga data with some infrastructure, allowing saga to have a individual Id, produce commands for external world and receive events to transit itself. To simplify work with sagas, GridDomain uses terms:
-
**saga **- process that orchestrates several aggregates. Implemented as state machine. Change state by receiving external messages, in common - domain events from aggregates. Can produce commands in response.
-
saga state - state of saga internal state machine
-
saga data - poco class used to store saga progress and information, must implement ISagaState interface to store saga state. Does not have unique id.
-
saga instance - pair of saga and saga data with identity. Determines concrete process by storing it identity and binds saga with it data. Stores saga data as aggregate. It is common practice to mention saga instance as just "saga" in text or conversation. It may be confusing, but right meaning is usually quite clear based on context.
Saga instance is represented by two interfaces : ISagaInstance and ISagaInstance<TSaga,TData>. To create saga instance, use SagaInstance.New static method.
###Starting sagas Saga data contains all saga information, so saga start is a question of when we should create a new saga data. When means "in response to what message". Such messages are called saga start messages. We should define factory classes to produce a saga data by given message, and register it in GridDomain. Saga flow is independent of start messages, as is does not contain any state. From flow perspective, it is not important from what exact message saga should start. Saga data either does not hold information about start messages, as they are not a part of saga data. Saga start messages are stored only in saga descriptor, and factories should be created \ registered according to it.
###Saga Descriptor GridDomain needs to known a lot of information about saga in runtime to host and execute it. For example, start messages, accepted messages, commands that can be issued. Such knowledge is stored in ISagaDescriptor.
public interface ISagaDescriptor
{
//TODO: enforce check all messages are DomainEvents
IReadOnlyCollection<MessageBind> AcceptMessages { get; }
IReadOnlyCollection<Type> ProduceCommands { get; }
IReadOnlyCollection<Type> StartMessages { get; }
Type StateType { get; }
Type SagaType { get; }
}
It is mainly used in routing and saga registration in containers. For state sagas, it is recommended to implement ISagaDescriptor by saga itlesf and expose public static field. For instance sagas, descriptor can be obtained by SagaExtensions.CreateDescriptor extension methods. Please be aware that saga start messages should be passed to descriptor constructor manually. Saga descriptor provide information how a message should be bounded to a saga. MessageBind class contains message type and field name containing Guid of corresponding saga.
public class MessageBind
{
public Type MessageType { get; }
public string FieldName { get; }
}
By default field name is SagaId, and it can be customized by passing field name in Event method in saga.
class SoftwareProgrammingSaga: Saga<SoftwareProgrammingSagaData>
{
public SoftwareProgrammingSaga()
{
Event(() => MachineEvent, e => e.CustomId)
}
Event<ExternalMessage> MachineEvent { get; private set; }
}
In this case message ExternalMessage will be mapped to saga SoftwareProgrammingSaga using field CustomId, not SagaId.
Common way to create and expose saga descriptor:
class SoftwareProgrammingSaga: Saga<SoftwareProgrammingSagaData>
{
public static readonly ISagaDescriptor Descriptor
= SagaExtensions.CreateDescriptor<SoftwareProgrammingSaga,
SoftwareProgrammingSagaData,
GotTiredEvent,
SleptWellEvent>();
}
###Errors handling
If saga raises an exception during transition, it will be logged will Error level and publish IFault message, where T is type of message cased bad transition. All commands issued or events emitted before exception will be processed in regular manner.
###Saga conventions
- Map all external messages received by saga to machine events one-by-one. Do it by declaring Event Properties
- Register all commands saga will dispatch via Command() method. This information will be ised to deliver commands faults to saga.
- Register saga factories for all start messages and saga data types.
###Saga data management Inside event handlers in state machine we are free to modify saga data without any additional event raising or notifications. GridDomain stores all saga data inside an aggregate, using SagaDataAggregate class for it. It will store all incoming messages, and any saga transition. Saga data is saved as snapshot after any transition.
###Integrating saga in GridDomain To integrate new saga in GridDomain, we have to create saga factories, register saga in message routing and DI container. ####Saga factories To support sagas creation in runtime, GridDomain need to know how to create saga instance from saga start message and from existing saga data. To pass this information we have to provide several implementations of interfaces: ISagaFactory<TSaga,TData> and register it in GridDomain. For instance saga with start messages _StartA _and StartB we should create factories: ISagaFactory<TSagaInstance<TSaga,SagaDataAggregate>,StartA> ISagaFactory<TSagaInstance<TSaga,SagaDataAggregate>,StartB> ISagaFactory<TSagaInstance<TSaga,SagaDataAggregate>,SagaDataAggregate> It is handy to implement all interfaces in one class.
Example factory can looks like:
class SoftwareProgrammingSagaFactory:
ISagaFactory<ISagaInstance<SoftwareProgrammingSaga, SoftwareProgrammingSagaData>, SagaDataAggregate<SoftwareProgrammingSagaData>>,
ISagaFactory<ISagaInstance<SoftwareProgrammingSaga, SoftwareProgrammingSagaData>, GotTiredDomainEvent>
{
public ISagaInstance<SoftwareProgrammingSaga, SoftwareProgrammingSagaData> Create(SagaDataAggregate<SoftwareProgrammingSagaData> message)
{
return SagaInstance.New(new SoftwareProgrammingSaga(), message);
}
public ISagaInstance<SoftwareProgrammingSaga, SoftwareProgrammingSagaData> Create(GotTiredDomainEvent message)
{
var saga = new SoftwareProgrammingSaga();
var data = new SagaDataAggregate<SoftwareProgrammingSagaData>(message.SagaId,
new SoftwareProgrammingSagaData(saga.Coding));
return SagaInstance.New(saga, data);
}
}
Pay attention to way you create an aggregate inside factory method - any domain events produced during this process will be persisted. In most cases it is undesirable, as we need only an empty aggregate, "dummy" to apply events to.
####Saga routing Saga routing is registered using common IMessageRouter class in special configuration, called RouteMap implementing IMessageRouteMap. public class SoftwareProgrammingSagaRoutes : IMessageRouteMap { public void Register(IMessagesRouter router) { router.RegisterSaga<SoftwareProgrammingSaga,SoftwareProgrammingSagaData>(); } }
To deliver messages to and from saga, we should provide GridDomain information in what messages saga is interested and what it can produce. This information is stored in ISagaDescriptor instance. It can be obtained by SagaExtensions.GetDescriptor method. Having ISagaDescriptor we can register saga using IMessageRouter.RegisterSaga method.
Good thing is after saga is declared with all conventions, ISagaDescriptor can be obtained fully automatically. So to register saga you only have to call extensions method for IMessageRouter : MessageRouterExtensions.RegisterSaga<TSaga,TData>
####Saga container registrations & several start messages Saga execution in runtime relies on ISaga<TSaga, TData> implementation, so it should be registered in container. Use extension method for it from ContainerExtensions.RegisterSaga. Container registration requires saga factory existence for all start messages existing in saga descriptor. There are no automatic check for now, so you need to handle it manually. Example:
return new CustomContainerConfiguration(
c => c.RegisterSaga<SoftwareProgrammingSaga,
SoftwareProgrammingSagaData,
SoftwareProgrammingSagaFactory,
SleptWellEvent,
GotTiredEvent>());
If saga will be created without initial state by any reason, it will not change state by incoming events, but only log received messages with Warning level. Such situation can occur if we will try to handle a message by saga before start message.