Write a saga
A saga reacts to events and issues commands. With the FCQRS.FSharp facade you write two functions, a
StartOn predicate, and bundle them into a Saga record. This example mirrors the quota saga from the
tutorial: it starts from a Document request, asks a User
aggregate to consume a quota slot, then tells the document to approve or hold.
open FCQRS.Common
open FCQRS.FSharp
type State =
| CheckingQuota of Username * DocumentId
| Approving of DocumentId
| Holding of DocumentId
| Done
// A saga sees events as obj — they come from BOTH aggregates. Active patterns
// recover the typed payload so the handler matches event + state in one pass.
let private (|DocEvent|_|) (o: obj) =
match o with
| :? (Event<Document.Event>) as e -> Some e.EventDetails
| _ -> None
let private (|UserEvent|_|) (o: obj) =
match o with
| :? (Event<User.Event>) as e -> Some e.EventDetails
| _ -> None
// 1. react to events -> next state
let private handleEvent evt sagaState =
match evt, sagaState.State with
| DocEvent(Document.CreateOrUpdateRequested(doc, owner)), None ->
CheckingQuota(owner, doc.Id) |> StateChangedEvent
| UserEvent(User.QuotaApproved _), Some(CheckingQuota(_, docId)) ->
Approving docId |> StateChangedEvent
| UserEvent User.QuotaRejected, Some(CheckingQuota(_, docId)) ->
Holding docId |> StateChangedEvent
| DocEvent(Document.ApprovedEvt _), Some(Approving _) -> Done |> StateChangedEvent
| DocEvent(Document.HeldForApproval _), Some(Holding _) -> Done |> StateChangedEvent
| _ -> UnhandledEvent
// 2. on entering a state, choose a transition and issue commands
let private applySideEffects documentFactory userFactory sagaState _recovering =
match sagaState.State with
// cross-aggregate command: a specific User instance, by id
| CheckingQuota(owner, docId) ->
Stay, [ toAggregate userFactory owner.Value (User.ConsumeQuota docId) ]
// command back to the originating Document
| Approving _ -> Stay, [ toOriginator documentFactory Document.Approve ]
| Holding _ -> Stay, [ toOriginator documentFactory Document.Hold ]
| Done -> StopSaga, []
// Which originator events spawn an instance. Typed to the originator's event, so
// the originator-event type is inferred — there is no type argument to get wrong.
let startsOn (e: Event<Document.Event>) =
match e.EventDetails with
| Document.CreateOrUpdateRequested _ -> true
| _ -> false
let definition documentFactory userFactory =
{ Name = "QuotaSaga"
InitialData = () // no cross-step data: progress lives in State
Originator = documentFactory
HandleEvent = handleEvent
ApplySideEffects = applySideEffects documentFactory userFactory
StartOn = startsOn }
|
Register it from your composition root, after the aggregates it references — this is what fires the safe start-up handshake:
let documents = Fcqrs.aggregate api { Name = "Document"; Initial = Document.initial; Decide = Document.decide; Fold = Document.fold }
let users = Fcqrs.aggregate api { Name = "User"; Initial = User.initial; Decide = User.decide; Fold = User.fold }
let quota = Fcqrs.saga api (definition documents.Factory users.Factory)
Fcqrs.wireSagaStarters api [ quota ]
|
Command builders. The facade turns side-effect commands into one-liners instead of hand-rolled
TargetActor records:
toOriginator factory cmd— to the aggregate that started the saga.toAggregate factory id cmd— to a specific aggregate instance, by id (cross-aggregate).toActor actorRef cmd— to an arbitrary actor ref.-
toOriginatorAfter factory delayMs taskName cmd— a delayed command, which is how you build retry-with-backoff.
Recovery: use the recovering flag (the last argument to applySideEffects) to avoid re-issuing
real-world effects when the saga replays after a restart. A complete runnable example is
focument_fsharp. Concept: Sagas.
module Event from Microsoft.FSharp.Control
--------------------
type Event<'T> = new: unit -> Event<'T> member Trigger: arg: 'T -> unit member Publish: IEvent<'T>
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate and reference type)> = new: unit -> Event<'Delegate,'Args> member Trigger: sender: obj * args: 'Args -> unit member Publish: IEvent<'Delegate,'Args>
--------------------
new: unit -> Event<'T>
--------------------
new: unit -> Event<'Delegate,'Args>
FCQRS