-
Notifications
You must be signed in to change notification settings - Fork 7
Aggregate future events
###Overview
Business often needs to schedule an action or event in some future point of time. For example it can be contract period expiration, or hotel booking time elapsing. Since business behavior is encapsulated in aggregates, we can expect a possibility to schedule aggregate event raising in future. When event is raised it could mutate aggregate state and start sagas, as any other synchronous domain event raised as a result of command execution. In addition, future events should be persisted, as raise time can be far in future from now (month, year) and we cannot rely on storing in memory. So we can say that future event is ordinary domain event been scheduled for some time in future, e.g. it is a domain event with raise time. Acronim for future event is scheduled event.
###Show me the code
GridDomain provides base class Aggregate and Aggregate.RaiseEvent overload for raising events in some point if time, and it names this functionality as "Future events"
public class TestAggregate : Aggregate
{
private TestAggregate(Guid id):base(id)
{
}
public void ScheduleInFuture(DateTime raiseTime)
{
RaiseEvent(raiseTime, new TestDomainEvent("test value",Id));
}
}
Also we have to register future events in aggregate commands handler, because it will be invoked by external command. It can be done directly :
public class TestAggregatesCommandHandler : AggregateCommandsHandler<TestAggregate>,IAggregateCommandsHandlerDesriptor
{
public static readonly IAggregateCommandsHandlerDesriptor Descriptor = new TestAggregatesCommandHandler();
public TestAggregatesCommandHandler() : base(null)
{
Map<RaiseScheduledDomainEventCommand>(c => c.AggregateId,
(c,a) => a.RaiseScheduledEvent(c.AggregateId));
}
public Type AggregateType => typeof(TestAggregate);
}
Or using extension method to hide command type and raise event method. It is possible because they both belongs to base Aggregate class and will not change from aggregate to aggregate.
public class TestAggregatesCommandHandler : AggregateCommandsHandler<TestAggregate>,
IAggregateCommandsHandlerDesriptor
{
public static readonly IAggregateCommandsHandlerDesriptor Descriptor = new TestAggregatesCommandHandler();
public TestAggregatesCommandHandler() : base(null)
{
Map<TestCommand>(c => c.AggregateId,
(c, a) => a.ScheduleInFuture(c.RaiseTime));
this.MapFutureEvents();
}
public Type AggregateType => typeof(TestAggregate);
}
###How it works
At given time, aggregate will receive a RaiseScheduledDomainEventCommand and produce original event in response followed by one additional event FutureDomainEventOccuredEvent. Aggregate can produce only it own events. All future event data is stored inside the aggregate and is searched by id on raise. Id is unique per future event, so we cannot force aggregate to raise an event does not belonging to it.
###Terms There are several domain events involved into future events raising process. They all are produced by aggregate on different flows for future events processing.
- FutureEventScheduledEvent when future event is just created and scheduled. GridDomain has sheduling infrastructure out of aggregate, so aggregate is responsible only for future event creation and delegate scheduling on imfrastructure. To simplify things, GridDomain assumes future event creation and scheduling is one moment.
- FutureEventOccuredEvent future event was raised and applied by aggregate
- FutureEventCanceledEvent future event was canceled by aggregate. Physically there are two different stages - cancelling event inside the aggregate, and unscheduling it from infrastructure. GridDomain assumes it is one moment.
###Canceling future events
If aggregate is not longer interested in created future event it can be canceled by calling Aggregate.CancelScheduledEvents(Predicate). It will search for all events of type satisfying passed criteria. For all events found new domain event FutureEventCanceledEvent will be generated, and future event will be removed from aggregate internal state and from scheduling infrastructure. If no events of type T will be found, or all found events will not satisfy criteria, no exception will be raised.
###Under the hood
When an aggregate raise a future event, it is saved in aggregate internal collection and is published by AggregateActor within special FutureDomainEvent envelop. After it is passed to SchedulingActor and it creates a persisted Quartz job providing ExecutionOptions class containing information about command execution - what is the command, timeout, success event.
Quartz job at scheduled time will start ScheduledCommandExecutionSaga. Saga will issue command and wait for success event from transport after command execution or for a fault. After it job status is updated depending on a result. To complete the flow, original aggregate should be presented in system and it should contain scheduled event.
You can find more detail in future event life cycle diagramm: https://drive.google.com/file/d/0B9oAIj7WRXnjeVFjVXh2Q3M1NkE/view?usp=sharing