3. Adding a saga
We'll reuse the validated values from chapter 1 — Title/Content wrap
FCQRS's ShortString/LongString, plus a Username (which doubles as the User aggregate's id):
module Values =
type DocumentId =
| DocumentId of Guid
static member OfGuid g = DocumentId g
member this.Value = let (DocumentId g) = this in g
override this.ToString() = let (DocumentId g) = this in g.ToString()
type Title =
| Title of ShortString
static member TryCreate s =
match ValueLens.TryCreate s with
| Ok ss -> Ok(Title ss)
| Error _ -> Error "Invalid title"
member this.Value = let (Title s) = this in ValueLens.Value s
type Content =
| Content of LongString
static member TryCreate s =
match ValueLens.TryCreate s with
| Ok ss -> Ok(Content ss)
| Error _ -> Error "Invalid content"
member this.Value = let (Content s) = this in ValueLens.Value s
type Username =
| Username of ShortString
static member TryCreate s =
match ValueLens.TryCreate s with
| Ok ss -> Ok(Username ss)
| Error _ -> Error "a username is required"
member this.Value = let (Username s) = this in ValueLens.Value s
Here's a rule that sounds trivial and turns out to be impossible with what we've built: each user may
create at most three documents per minute. Go ahead and try to put it in Document.decide. You can't —
a document only knows about itself. It has no idea how many other documents the same person created in
the last sixty seconds, and it would be a layering disaster if it did. The limit isn't a fact about one
document; it's a fact about a user, spanning many documents.
That's the shape of problem a saga exists for. This chapter introduces a second aggregate (a User
that owns the quota) and the saga that coordinates the two. It's the most involved chapter — and the one
where the framework finally does something you'd genuinely dread writing by hand.
Why an aggregate can't reach across the boundary
Recall from chapter 1 that an aggregate is a consistency boundary: one entity, deciding alone, one command at a time. That isolation is precisely what gives you no-locks, no-races correctness. The price is that an aggregate cannot reach into another aggregate to check or change it — if it could, you'd be right back to shared mutable state and the races we escaped.
🎯 Key principle. Aggregates turn commands into events. A saga is the mirror image: it turns events into commands. When something true happens in one aggregate (a document was requested) and it should cause something in another (consume a quota slot), the saga is the only thing allowed to carry that intent across the boundary. It is a process manager — a small, durable state machine that listens for events and issues commands. 💡 Mental model. Think of a travel booking. The "flight" service and the "hotel" service each guard their own data; neither reaches into the other. A booking coordinator watches for "flight reserved," then tells the hotel "reserve a room," and if that fails, tells the flight "cancel." The coordinator holds no flight or hotel data of its own — it only watches and issues orders. That coordinator is a saga.
Here's the flow we're about to build, end to end:
create request
|
v
Document -- CreateOrUpdateRequested --> (this starts the saga)
|
v
saga -- ConsumeQuota --> User[owner]
|
+--------------+--------------+
| |
QuotaApproved QuotaRejected
(within quota) (over quota)
| |
v v
saga: Approve saga: Hold
| |
v v
Document: ApprovedEvt Document: HeldForApproval
⚠️ Before we write it: a saga is real added complexity — a second persistent actor, an extra hop, more states to reason about. Don't reach for one to send a welcome email you could fire inline. Reach for one when the work genuinely spans aggregates or must be reliably retried/compensated. Our quota is the first kind: two aggregates, so a saga isn't a style choice, it's structurally required.
Step 1 — teach the Document about approval
The first write can no longer just succeed, because whether it's allowed now depends on the user's
quota — which the document can't see. So a creation becomes a pending request (CreateOrUpdateRequested)
that records the document and starts the saga; the saga later tells the document Approve or Hold.
An edit of a document we already hold still skips all of it — editing isn't gated.
This is where the command/event split from chapter 1 pays off: CreateOrUpdate now fans out into
several possible events, and because we kept them separate types from the start, nothing about the
caller's command has to change.
module Document =
open Values
type Root =
{ Id: DocumentId; Title: Title; Content: Content }
static member TryCreate(guid, title, content) =
match Title.TryCreate title, Content.TryCreate content with
| Ok t, Ok c -> Ok { Id = DocumentId.OfGuid guid; Title = t; Content = c }
| Error e, _ -> Error e
| _, Error e -> Error e
type Approval = Pending | AwaitingApproval | Approved | Rejected
type DocumentError = DocumentNotFound
type Command =
| CreateOrUpdate of Root * Username // who's asking
| Approve
| Reject
| Hold
type Event =
| CreateOrUpdateRequested of Root * Username
| Updated of Root
| Errored of DocumentError
| ApprovedEvt of DocumentId
| RejectedEvt of DocumentId
| HeldForApproval of DocumentId
type State = { Document: Root option; Version: int64; Approval: Approval }
let initial = { Document = None; Version = 0L; Approval = Pending }
let decide (cmd: Command<_>) state =
match cmd.CommandDetails, state.Document with
// First write — no document yet. A pending request that starts the quota saga.
| CreateOrUpdate(doc, owner), None -> CreateOrUpdateRequested(doc, owner) |> PersistEvent
// Edit of the document we already hold — no saga, no quota.
| CreateOrUpdate(doc, _), Some existing when existing.Id = doc.Id -> Updated doc |> PersistEvent
| CreateOrUpdate _, _ -> Errored DocumentNotFound |> DeferEvent
// Verdicts — idempotent: if already in the target state, defer (still
// published so a re-issuing saga sees it) rather than persist a duplicate.
| Approve, Some doc ->
let e = ApprovedEvt doc.Id
if state.Approval = Approved then DeferEvent e else PersistEvent e
| Reject, Some doc ->
let e = RejectedEvt doc.Id
if state.Approval = Rejected then DeferEvent e else PersistEvent e
| Hold, Some doc ->
let e = HeldForApproval doc.Id
if state.Approval = AwaitingApproval then DeferEvent e else PersistEvent e
| _ -> UnhandledEvent
let fold evt state =
match evt.EventDetails with
| CreateOrUpdateRequested(doc, _) -> { state with Document = Some doc; Version = state.Version + 1L; Approval = Pending }
| Updated doc -> { state with Document = Some doc; Version = state.Version + 1L }
| ApprovedEvt _ -> { state with Approval = Approved }
| RejectedEvt _ -> { state with Approval = Rejected }
| HeldForApproval _ -> { state with Approval = AwaitingApproval }
| Errored _ -> state
|
Look closely at the verdict cases — they're a small but important lesson:
⚠️ Common mistake. Assuming each command is delivered exactly once. Across restarts and retries a saga may re-issue
Approvefor a document that's already approved. If you blindlyPersistEventevery time, you log duplicate approvals and inflate the version on no-ops. The guard here checks "am I already in that state?" and downgrades the repeat to aDeferEvent— published so the saga still hears "yes," but not re-recorded. Designing for at-least-once delivery, not exactly-once, is the rule, not the exception, in any distributed system.
Notice UnhandledEvent finally appears: it's how decide says "that command makes no sense in this
state" — distinct from IgnoreEvent ("valid, but do nothing").
Step 2 — the User aggregate owns the quota
The second aggregate is keyed by username and remembers the slots a user consumed in the last minute. Two design choices make it safe under a saga's retries, and both are worth pausing on.
module User =
open Values
type Command = ConsumeQuota of DocumentId
type Event =
| QuotaApproved of DocumentId * DateTime
| QuotaRejected
type Consumption = { DocId: DocumentId; At: DateTime }
type State = { Consumed: Consumption list }
let initial = { Consumed = [] }
[<Literal>]
let Limit = 3
let Window = TimeSpan.FromMinutes 1.0
let private prune (reference: DateTime) slots =
let cutoff = reference - Window
slots |> List.filter (fun c -> c.At > cutoff)
let decide (cmd: Command<_>) state =
match cmd.CommandDetails with
| ConsumeQuota docId ->
match state.Consumed |> List.tryFind (fun c -> c.DocId = docId) with
// Re-delivery: this document already holds a slot — re-grant the SAME slot.
| Some existing -> QuotaApproved(docId, existing.At) |> PersistEvent
| None ->
if prune cmd.CreationDate state.Consumed |> List.length < Limit then
QuotaApproved(docId, cmd.CreationDate) |> PersistEvent
else
QuotaRejected |> DeferEvent
let fold evt state =
match evt.EventDetails with
| QuotaApproved(docId, _) when state.Consumed |> List.exists (fun c -> c.DocId = docId) -> state
| QuotaApproved(docId, at) -> { state with Consumed = prune at ({ DocId = docId; At = at } :: state.Consumed) }
| QuotaRejected -> state
|
First, idempotency by document id: if a slot was already granted for this document, decide
re-grants the same slot instead of spending a new one, so a retried ConsumeQuota can't double-charge.
Second — and this is the one people get wrong — look at where time comes from:
🎯 Key principle.
decidereadscmd.CreationDate(the moment captured when the command was issued), andfoldonly ever uses the timestamp the event carries (QuotaApproved(_, at)). Neither callsDateTime.UtcNow. This is the chapter-1 purity rule with teeth: iffoldread the wall clock, replaying the log a minute later would prune different slots and reconstruct a different quota state. Capture time in the command, carry it in the event, fold only what the event holds — and replay stays deterministic forever.
The saga
A saga is a tiny state machine. It begins when a document is requested, asks the user to consume a
slot, then routes the document to approval or hold. The framework supplies the "not started yet"
preamble (the None you'll see below); these four states are ours, and the rest is two functions.
module QuotaSaga =
open Values
type State =
| CheckingQuota of Username * DocumentId
| Approving of DocumentId
| Holding of DocumentId
| Done
A saga sees its events as obj, because they arrive from both aggregates — Document events and
User events flow into the same handler. Two small active patterns recover the typed payload so the
handler can match on event-and-state together in one flat, readable pass. Each arm returns the next
state with StateChangedEvent.
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
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
handleEvent decides what state we're in; applySideEffects decides what to do on entering it — the
transition (Stay, NextState, StopSaga) and the commands to send. This is where the saga turns
events back into commands, and the facade keeps it to one line each with two helpers: toAggregate
addresses a specific aggregate instance by id (the User keyed by owner name), toOriginator replies
to whichever document started this saga.
let private applySideEffects documentFactory userFactory sagaState _recovering =
match sagaState.State with
| CheckingQuota(owner, docId) ->
Stay, [ toAggregate userFactory owner.Value (User.ConsumeQuota docId) ]
| Approving _ -> Stay, [ toOriginator documentFactory Document.Approve ]
| Holding _ -> Stay, [ toOriginator documentFactory Document.Hold ]
| Done -> StopSaga, []
⚠️ Common mistake. Performing the side effect inside the saga — sending the email, calling the API — right here. Don't. A saga's job is to issue a command; the receiving actor performs the effect through the same persist-and-recover machinery as everything else, so it can be retried and audited. That's also what the
_recoveringflag is for: when a saga rebuilds itself after a restart it replays its states, and you use that flag to avoid re-firing real-world effects you already fired. (Our quota only issues commands to other aggregates, which are themselves idempotent, so we can ignore it here.)
Finally, bundle it into a Saga record. StartOn is typed to the originator's event, so the
framework infers the originator-event type — there's no type argument to remember or 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 }
|
Wiring it all up
The composition root registers both aggregates, builds the saga from the factories they return, and wires the saga-starter — the declaration that fires the safe start-up handshake.
let wire (api: IActor) =
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 (QuotaSaga.definition documents.Factory users.Factory)
Fcqrs.wireSagaStarters api [ quota ]
documents
|
🤔 Did you know? There's a quiet race lurking in "an event starts a saga": if the document published
CreateOrUpdateRequestedbefore the saga was listening, the saga would miss the very event meant to create it.wireSagaStartersis what lets FCQRS run a brief handshake — hold the event until the saga is subscribed, then publish — so the start is safe by construction. You declare the rule; the framework guarantees the ordering. (The handshake, in detail.)
Run it — watch the quota bite
Send four creates for the same user (each a different document id) and the fourth crosses the line:
create #1 (alice) -> ApprovedEvt "saved" create #2 (alice) -> ApprovedEvt "saved" create #3 (alice) -> ApprovedEvt "saved" create #4 (alice) -> HeldForApproval "over quota - awaiting approval"
The first three each ride the path CreateOrUpdateRequested -> ConsumeQuota -> QuotaApproved -> Approve
-> ApprovedEvt. On the fourth, the User aggregate's sliding window is full, so it answers
QuotaRejected, the saga swings to Holding, and the document ends up AwaitingApproval instead of
approved. Wait a minute for the window to slide and the next create is approved again — because prune
drops the now-expired slots. You implemented a cross-entity, time-windowed rule without a single lock or
shared variable.
What you now understand
An aggregate is sealed inside its own boundary on purpose, so when a rule spans entities you need a
different tool: a saga, which listens to events from several aggregates and issues commands back. You
saw the three pieces — handleEvent (event → next state), applySideEffects (state → commands), and
StartOn (which event spawns it) — and the two distributed-systems reflexes that keep it honest:
idempotent commands and time captured in events, never read in folds.
Common mistakes
- Trying to enforce a cross-entity rule inside one aggregate. It can't see the others; that's the whole reason sagas exist.
-
Assuming exactly-once delivery. Make commands idempotent — re-grant the same slot, downgrade a
repeat verdict to
DeferEvent— so retries are harmless. -
Reading the clock in
decide/foldinstead of carrying time in the event. Replay then drifts and reconstructs the wrong state. -
Doing the side effect inside the saga. Issue a command and let the target actor perform it, so the
effect is retryable and recoverable; use
_recoveringto avoid re-firing on replay. - Reaching for a saga when a plain function call would do. Sagas pay off for cross-aggregate or must-be-reliable work — not for everything that happens "after" an event.
Further study
- Sagas — process managers, the safe start-up handshake, and how they make side effects reliable.
- Consistency and recovery — version checks, restarts, and avoiding duplicate effects.
- Write a saga — the same pattern as a focused recipe, with all the command builders.
You've built the whole loop
Across three chapters you defined an aggregate from two pure functions, gave it a running home and
watched event sourcing reconstruct state across a restart, then added a second aggregate and a saga that
coordinates both to enforce a rule neither could alone — the entire FCQRS model. The complete, runnable
version of exactly this app is focument_fsharp. From
here, the How-to guides are task-focused recipes, and
Concepts goes deeper on anything you want to understand more fully.
<summary> Contains common types like Events and Commands </summary>
<namespacedoc><summary>Functionality for Write Side.</summary></namespacedoc>
<summary> Idiomatic-F# functional facade for FCQRS. Gives F# consumers the same one-call ergonomics the C# host-builder (HostExtensions.fs) gives C#, but with F# idioms: records-of-functions for the definitions, typed handles for the results, an explicit wiring pipeline, and plain helpers for saga side effects. It is a *pure addition* — it wraps only the existing primitives (IActor.InitializeActor / SagaBuilder.initSimple / Query.init / InitializeSagaStarter / CreateCommandSubscription / Actor.api) and changes nothing in the C# interop layer or the core. open FCQRS.FSharp let api = Fcqrs.actor config loggerFactory (Some (Fcqrs.connect DBType.Sqlite conn)) "Cluster" let documents = Fcqrs.aggregate api { Name="Document"; Initial=...; Decide=...; Fold=... } let users = Fcqrs.aggregate api { Name="User"; ... } let quota = Fcqrs.saga api (quotaDef documents.Factory users.Factory) Fcqrs.wireSagaStarters api [ quota ] let subs = Fcqrs.projection api { LastOffset = 0; Handle = handle } // send a command and await the resulting event (read-your-writes): let! ev = documents.Send (Fcqrs.newCid()) (Fcqrs.aggregateId id) cmd (fun e -> ...) </summary>
[<Struct>] type Guid = new: b: byte array -> unit + 6 overloads member CompareTo: value: Guid -> int + 1 overload member Equals: g: Guid -> bool + 1 overload member GetHashCode: unit -> int member ToByteArray: unit -> byte array + 1 overload member ToString: unit -> string + 2 overloads member TryFormat: utf8Destination: Span<byte> * bytesWritten: byref<int> * ?format: ReadOnlySpan<char> -> bool + 1 overload member TryWriteBytes: destination: Span<byte> -> bool + 1 overload static member (<) : left: Guid * right: Guid -> bool static member (<=) : left: Guid * right: Guid -> bool ...
<summary>Represents a globally unique identifier (GUID).</summary>
--------------------
Guid ()
Guid(b: byte array) : Guid
Guid(b: ReadOnlySpan<byte>) : Guid
Guid(g: string) : Guid
Guid(b: ReadOnlySpan<byte>, bigEndian: bool) : Guid
Guid(a: int, b: int16, c: int16, d: byte array) : Guid
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
union case DocumentId.DocumentId: Guid -> DocumentId
--------------------
type DocumentId = | DocumentId of Guid override ToString: unit -> string static member OfGuid: g: Guid -> DocumentId member Value: Guid
Guid.ToString(format: string) : string
Guid.ToString(format: string, provider: IFormatProvider) : string
<summary> Validated non-blank string up to 255 chars inclusive. </summary>
union case Title.Title: ShortString -> Title
--------------------
type Title = | Title of ShortString static member TryCreate: s: string -> Result<Title,string> member Value: string
static member ValueLens.Value: this: 'Wrapped -> 'Inner (requires member Value_)
<summary> Represents any string at least 1 chars </summary>
union case Content.Content: LongString -> Content
--------------------
type Content = | Content of LongString static member TryCreate: s: string -> Result<Content,string> member Value: string
union case Username.Username: ShortString -> Username
--------------------
type Username = | Username of ShortString static member TryCreate: s: string -> Result<Username,string> member Value: string
type Command = | CreateOrUpdate of Root * Username | Approve | Reject | Hold
--------------------
type Command<'CommandDetails> = { CommandDetails: 'CommandDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Metadata: Map<string,string> } interface IMessage interface ISerializable member Equals: Command<'CommandDetails> * IEqualityComparer -> bool override ToString: unit -> string
<summary> Represents a command to be processed by an aggregate actor. <typeparam name="'CommandDetails">The specific type of the command payload.</typeparam> </summary>
--------------------
type Command<'Command,'Event> = | Execute of CommandDetails<'Command,'Event>
<summary> Represents the message sent to the internal subscription mechanism. <typeparam name="'Command">The type of the command payload.</typeparam> <typeparam name="'Event">The type of the expected event payload.</typeparam> </summary>
module Event from Microsoft.FSharp.Control
--------------------
type Event = | CreateOrUpdateRequested of Root * Username | Updated of Root | Errored of DocumentError | ApprovedEvt of DocumentId | RejectedEvt of DocumentId | HeldForApproval of DocumentId
--------------------
type Event<'EventDetails> = { EventDetails: 'EventDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Version: Version Metadata: Map<string,string> } interface IMessage interface ISerializable member Equals: Event<'EventDetails> * IEqualityComparer -> bool override ToString: unit -> string
<summary> Represents an event generated by an aggregate actor as a result of processing a command. <typeparam name="'EventDetails">The specific type of the event payload.</typeparam> </summary>
--------------------
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<'Delegate,'Args>
type State = { Document: Root option Version: int64 Approval: Approval }
--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
<summary> Aggregate Version </summary>
val int64: value: 'T -> int64 (requires member op_Explicit)
--------------------
type int64 = Int64
--------------------
type int64<'Measure> = int64
<summary> The specific details or payload of the command. </summary>
<summary> Persist the event to the journal. The actor's state will be updated using the event handler *after* persistence succeeds. </summary>
<summary> Defer the event. It will be stashed and processed later, potentially after other events. </summary>
<summary> Indicate that the command or event could not be handled in the current state. </summary>
<summary> The specific details or payload of the event. </summary>
type Command = | ConsumeQuota of DocumentId
--------------------
type Command<'CommandDetails> = { CommandDetails: 'CommandDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Metadata: Map<string,string> } interface IMessage interface ISerializable member Equals: Command<'CommandDetails> * IEqualityComparer -> bool override ToString: unit -> string
<summary> Represents a command to be processed by an aggregate actor. <typeparam name="'CommandDetails">The specific type of the command payload.</typeparam> </summary>
--------------------
type Command<'Command,'Event> = | Execute of CommandDetails<'Command,'Event>
<summary> Represents the message sent to the internal subscription mechanism. <typeparam name="'Command">The type of the command payload.</typeparam> <typeparam name="'Event">The type of the expected event payload.</typeparam> </summary>
module Event from Microsoft.FSharp.Control
--------------------
type Event = | QuotaApproved of DocumentId * DateTime | QuotaRejected
--------------------
type Event<'EventDetails> = { EventDetails: 'EventDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Version: Version Metadata: Map<string,string> } interface IMessage interface ISerializable member Equals: Event<'EventDetails> * IEqualityComparer -> bool override ToString: unit -> string
<summary> Represents an event generated by an aggregate actor as a result of processing a command. <typeparam name="'EventDetails">The specific type of the event payload.</typeparam> </summary>
--------------------
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<'Delegate,'Args>
[<Struct>] type DateTime = new: date: DateOnly * time: TimeOnly -> unit + 16 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMicroseconds: value: float -> DateTime member AddMilliseconds: value: float -> DateTime member AddMinutes: value: float -> DateTime member AddMonths: months: int -> DateTime member AddSeconds: value: float -> DateTime member AddTicks: value: int64 -> DateTime ...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>
--------------------
DateTime ()
(+0 other overloads)
DateTime(ticks: int64) : DateTime
(+0 other overloads)
DateTime(date: DateOnly, time: TimeOnly) : DateTime
(+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : DateTime
(+0 other overloads)
DateTime(date: DateOnly, time: TimeOnly, kind: DateTimeKind) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : DateTime
(+0 other overloads)
type State = { Consumed: Consumption list }
--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
type LiteralAttribute = inherit Attribute new: unit -> LiteralAttribute
--------------------
new: unit -> LiteralAttribute
[<Struct>] type TimeSpan = new: hours: int * minutes: int * seconds: int -> unit + 4 overloads member Add: ts: TimeSpan -> TimeSpan member CompareTo: value: obj -> int + 1 overload member Divide: divisor: float -> TimeSpan + 1 overload member Duration: unit -> TimeSpan member Equals: value: obj -> bool + 2 overloads member GetHashCode: unit -> int member Multiply: factor: float -> TimeSpan member Negate: unit -> TimeSpan member Subtract: ts: TimeSpan -> TimeSpan ...
<summary>Represents a time interval.</summary>
--------------------
TimeSpan ()
TimeSpan(ticks: int64) : TimeSpan
TimeSpan(hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int, microseconds: int) : TimeSpan
TimeSpan.FromMinutes(value: float) : TimeSpan
TimeSpan.FromMinutes(minutes: int64, ?seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
<summary> The timestamp when the command was created. </summary>
type State = | CheckingQuota of Username * DocumentId | Approving of DocumentId | Holding of DocumentId | Done
--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
module Event from Microsoft.FSharp.Control
--------------------
type Event<'EventDetails> = { EventDetails: 'EventDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Version: Version Metadata: Map<string,string> } interface IMessage interface ISerializable member Equals: Event<'EventDetails> * IEqualityComparer -> bool override ToString: unit -> string
<summary> Represents an event generated by an aggregate actor as a result of processing a command. <typeparam name="'EventDetails">The specific type of the event payload.</typeparam> </summary>
--------------------
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<'Delegate,'Args>
<summary> The specific details or payload of the event. </summary>
<summary> The specific details or payload of the event. </summary>
<summary> The current state machine state of the saga. </summary>
<summary> Indicate that the state of a saga has changed (used internally by sagas for persistence). </summary>
<summary> The current state machine state of the saga. </summary>
<summary> The saga should stay in current state without changes </summary>
<summary> Send a command to a specific aggregate instance by id (cross-aggregate). </summary>
<summary> Send a command back to the saga's originator aggregate. </summary>
<summary> The saga should stop and terminate </summary>
<summary> Identify the target by its string name (entity ID). </summary>
<summary> Identify the target as the originator actor of the current saga process. </summary>
<summary> Defines the core functionalities and context provided by the FCQRS environment to actors. This interface provides access to essential Akka.NET services and FCQRS initialization methods. </summary>
<summary> Register an aggregate and return its typed handle. Calling this IS the registration (it initializes the sharding region). </summary>
<summary> Register a saga and return its handle. The originator-event type is inferred from `def.StartOn`, so there are no type arguments to supply: `Fcqrs.saga api def`. </summary>
<summary> Entity-ref factory (DEFAULT_SHARD applied) — hand this to a saga to target it. </summary>
<summary> Wire every registered saga into one saga-starter (or the empty starter if none). Call after the aggregates + sagas are registered. </summary>
FCQRS