3. Adding a saga
DocStore can now store and look up documents. This chapter adds one more rule: a document can be published under a URL slug, and each slug may belong to only one document.
Every rule so far involved a single document, so one aggregate could enforce it alone. This rule is different, and it runs into a restriction that has been true since chapter 1: an aggregate can read and change only its own state. A document aggregate cannot look inside a slug aggregate, cannot lock it, and cannot update it in the same transaction. Aggregates never call each other. FCQRS keeps them isolated on purpose, because that isolation is what lets each one process commands, recover, and move between nodes independently.
In a single-database application the new rule would be one transaction touching a documents table and a slugs table. Across aggregates there is no shared transaction, so something else must coordinate, and that coordinator is the saga. A saga is a durable workflow that does the job a transaction coordinator does in a database: it waits for an event from one aggregate, sends a command to the next, waits for the reply, and reports the outcome back to where the work started. It cannot lock both aggregates or roll them back together. Instead it stores its own progress after every step, so a crash in the middle resumes the conversation instead of losing it.
This example is intentionally small. The only new problem is durable coordination, so the saga mechanics remain visible.
Course position: chapter 2 completed work owned by one aggregate and projected its event. This chapter handles one rule with two independent owners. By the end you will be able to derive saga state from events, issue commands only after progress is stored, and explain safe startup and resumption.
The rule belongs to two owners
The document aggregate decides whether its document may be published. The slug aggregate decides whether its slug is still free. Neither one can see the other's state, so neither one can enforce the whole rule alone. The saga carries the conversation between them:
Document Publication saga Slug[guides/fcqrs] | | | |-- PublicationRequested ------------->| | | |-- Reserve(documentId) --------------->| | |<-- SlugReserved / Unavailable --------| |<-- FinishPublication(result) --------| | | | | |-- PublicationFinished -------------->| StopSaga |
The aggregates still make every business decision. The saga only remembers how far the conversation has gone and sends the next message.
Motivation: Keeping each rule with its owner prevents the saga from becoming a second, stale copy of document and slug state. The saga coordinates answers; it does not invent them.
The chapter's new pieces as a wireframe; each unimplemented body names the step that fills it in.
// Step 1 extends Document with Publish / FinishPublication commands and
// PublicationRequested / PublicationFinished events.
module Slug =
// One aggregate instance per slug; the first Reserve wins.
let decide (cmd: Command<Command>) (state: State) : EventAction<Event> =
failwith "step 2"
let fold (event: Event<Event>) (state: State) : State =
failwith "step 2"
module PublicationSaga =
// Store intended progress first, then send the next safe command.
let handleEvent (message: obj) (sagaState: SagaState<unit, State option>)
: EventAction<State> =
failwith "step 3"
let applySideEffects documentFactory slugFactory sagaState recovering
: SagaTransition<State> * ExecuteCommand list =
failwith "step 3"
let startsOn (event: Event<Document.Event>) : bool =
failwith "step 4"
|
Extend the document with publication
Chapters 1 and 2 gave the document its content model: Root, CreateOrUpdate, and Updated. Those
stay exactly as they were. Publication is added to that same aggregate — the state gains a publication
track beside the document. The document id is the aggregate's own identity, so the new Publish
command carries only the slug. Validate the slug at the application boundary before sending it.
The validated values are unchanged from chapter 1:
module Values =
type DocumentId =
| DocumentId of Guid
static member OfGuid value = DocumentId value
member this.Value = let (DocumentId value) = this in value
override this.ToString() = let (DocumentId value) = this in value.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
|
Step 1: let the document request publication
The document stores PublicationRequested before any reservation begins. Its state records the slug
being reserved, so the eventual result remains valid after recovery. A document must exist (chapter
1's CreateOrUpdate) before it can be published.
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 PublicationResult =
| Published
| Rejected
type PublicationStatus =
| NotRequested
| WaitingForSlug of slug: string
| Finished of slug: string * PublicationResult
type State =
{ Document: Root option
Publication: PublicationStatus }
let initial = { Document = None; Publication = NotRequested }
type Command =
| CreateOrUpdate of Root
| Publish of slug: string
| FinishPublication of PublicationResult
type Event =
| Updated of Root
| PublicationRequested of DocumentId * slug: string
| PublicationFinished of DocumentId * slug: string * PublicationResult
let decide (cmd: Command<Command>) state =
match cmd.CommandDetails, state with
// Content changes work exactly as in chapter 1.
| CreateOrUpdate doc, _ ->
Updated doc |> PersistEvent
// First Publish: store the request; state now remembers the slug being reserved.
| Publish slug, { Document = Some doc; Publication = NotRequested } ->
PublicationRequested(doc.Id, slug) |> PersistEvent
// The same Publish again (a retry): same reply, nothing stored twice.
| Publish slug, { Document = Some doc; Publication = WaitingForSlug currentSlug }
when slug = currentSlug ->
PublicationRequested(doc.Id, slug) |> DeferEvent
// The saga reports the outcome: store it once.
| FinishPublication result, { Document = Some doc; Publication = WaitingForSlug slug } ->
PublicationFinished(doc.Id, slug, result) |> PersistEvent
// The same outcome again (saga recovery): same reply, nothing stored twice.
| FinishPublication result, { Document = Some doc; Publication = Finished(slug, current) }
when result = current ->
PublicationFinished(doc.Id, slug, result) |> DeferEvent
// Everything else: no document yet, a different slug, a contradicting result.
| _ -> UnhandledEvent
let fold (event: Event<Event>) state =
match event.EventDetails with
| Updated doc -> { state with Document = Some doc }
| PublicationRequested(_, slug) -> { state with Publication = WaitingForSlug slug }
| PublicationFinished(_, slug, result) -> { state with Publication = Finished(slug, result) }
FinishPublication carries either Published or Rejected. Repeating the same result returns the
same PublicationFinished outcome with DeferEvent, so recovery can reissue one saga command without
storing another domain event. Keeping the result as data removes duplicate success and failure
branches from the aggregate.
The publication cases form two pairs: the first request is stored and its retry is deferred; the
first result is stored and its retry is deferred. The final wildcard rejects every command and state
combination that does not belong to this workflow — including Publish before the document exists.
|
Step 2: let each slug protect its own uniqueness
The slug aggregate is addressed by the slug text. Its entire state is the document that owns the reservation, if any.
module Slug =
open Values
type State = { ReservedFor: DocumentId option }
let initial = { ReservedFor = None }
type Command = Reserve of DocumentId
type Event =
| SlugReserved of DocumentId
| SlugUnavailable of DocumentId
let decide (cmd: Command<Command>) state =
match cmd.CommandDetails, state.ReservedFor with
// First reservation wins and becomes durable.
| Reserve documentId, None ->
SlugReserved documentId |> PersistEvent
// The same document asking again gets the same answer, nothing stored.
| Reserve documentId, Some current when current = documentId ->
SlugReserved documentId |> DeferEvent
// Any other document: taken; the owner does not change.
| Reserve documentId, Some _ ->
SlugUnavailable documentId |> DeferEvent
let fold (event: Event<Event>) state =
match event.EventDetails with
| SlugReserved documentId -> { ReservedFor = Some documentId }
| SlugUnavailable _ -> state
The first reservation becomes durable. Repeating it for the same document returns the existing result.
A different document receives SlugUnavailable without changing the owner. Both repeated paths are
safe when a saga resumes after uncertain delivery.
|
Step 3: write the saga as a table
Before writing the saga functions, write every accepted event and resulting command:
Current state |
Incoming event |
Stored next state |
Command after storage |
|---|---|---|---|
not started |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stop |
The fourth row is the timeout. ExpectationExhausted is not a domain event: FCQRS delivers it when
the wait declared in Function 2 below runs out of time without any answer from the slug.
This table separates two questions:
- Given an event and current saga state, what state should be stored?
- Once that state is durable, which command should be sent?
Motivation: The split ensures FCQRS can store what the saga intends to do before it sends a command that may be delivered just as the process fails.
module PublicationSaga =
open Values
type State =
| ReservingSlug of DocumentId * string
| ReportingResult of Document.PublicationResult
| Done
|
Function 1: event plus state becomes persisted progress
Saga events arrive as obj because several aggregate event types share the same workflow. Active
patterns recover their typed envelopes. The first domain event reaches user code with
sagaState.State = None; no user-defined state exists until that event is accepted.
let private (|DocumentEvent|_|) (message: obj) =
match message with
| :? Event<Document.Event> as event -> Some event.EventDetails
| _ -> None
let private (|SlugEvent|_|) (message: obj) =
match message with
| :? Event<Slug.Event> as event -> Some event.EventDetails
| _ -> None
let handleEvent (message: obj) sagaState =
match message, sagaState.State with
// Table row 1: the starting event opens the workflow.
| DocumentEvent(Document.PublicationRequested(documentId, slug)), None ->
ReservingSlug(documentId, slug) |> StateChangedEvent
// Table rows 2 and 3: the slug's answer decides which result to report.
| SlugEvent(Slug.SlugReserved documentId), Some(ReservingSlug(expected, _))
when documentId = expected ->
ReportingResult Document.Published |> StateChangedEvent
| SlugEvent(Slug.SlugUnavailable documentId), Some(ReservingSlug(expected, _))
when documentId = expected ->
ReportingResult Document.Rejected |> StateChangedEvent
// Timeout row: the declared wait ran out without an answer. Unknown is
// treated as Rejected; the prose after Function 2 discusses the cost.
| :? ExpectationExhausted, Some(ReservingSlug _) ->
ReportingResult Document.Rejected |> StateChangedEvent
// Last row: the document confirmed the result; the workflow is complete.
| DocumentEvent(Document.PublicationFinished(_, _, result)), Some(ReportingResult expected)
when result = expected ->
Done |> StateChangedEvent
// Anything else is out of order for the current state.
| _ -> UnhandledEvent
|
StateChangedEvent next is the saga counterpart to a persisted aggregate outcome: FCQRS stores the
new workflow state. UnhandledEvent means the incoming event is not valid for the current state.
Function 2: persisted state becomes commands
FCQRS calls applySideEffects only after a state change is stored. It calls the same function after
recovery, with recovering = true.
let applySideEffects documentFactory slugFactory sagaState _recovering =
match sagaState.State with
// Ask the slug to reserve, and declare the wait: re-send Reserve every
// five seconds (it is retry-safe, the same property recovery relies on)
// and deliver ExpectationExhausted after thirty. The deadline measures
// from the journaled entry into this state, so restarts cannot reset it.
| ReservingSlug(documentId, slug) ->
expecting (TimeSpan.FromSeconds 30.0) (FixedInterval(TimeSpan.FromSeconds 5.0))
[ toAggregate slugFactory slug (Slug.Reserve documentId) ],
[]
// Report the outcome back to the document that started the workflow.
| ReportingResult result ->
Stay, [ toOriginator documentFactory (Document.FinishPublication result) ]
// Nothing left to send; the saga completes and passivates.
| Done ->
StopSaga, []
|
The C# constructor in Step 4 supplies _documents and _slugs. As in F#, the method receives the
recovery flag even though this retry-safe workflow sends the same command during live processing and
recovery.
StayExpecting (C#: Stay plus the Expect property) keeps the stored state like Stay while
declaring what the wait is for: FCQRS sends Reserve on entry into ReservingSlug, re-sends exactly
that command on the schedule while no transition is stored, and past the deadline delivers the
ExpectationExhausted handled in Function 1. ReportingResult uses plain Stay; a fuller design
would bound that wait the same way. StopSaga completes and passivates the saga. NextState next is
the fourth available transition; it persists another state immediately when a step should advance
without waiting for an event. This workflow does not need it.
The timeout row buys a bounded wait at a price the domain must acknowledge: exhaustion reports
Rejected while the true outcome is unknown. The slug may have reserved successfully with only its
answer lost, and a SlugReserved arriving after the escalation is out of order for
ReportingResult, so handleEvent ignores it: the reservation stays held by a document that was
told its publication failed. A production workflow gives the failure path a compensating command
(release the slug) or lets the late answer un-fail the workflow.
Write a saga states the general rules.
The state is always stored before its commands are issued:
incoming event
-> handleEvent
-> persist StateChangedEvent
-> applySideEffects
-> send commands
That ordering is what makes the next action recoverable.
Step 4: declare how the saga starts
StartOn selects the originator event that creates one workflow instance. Originator identifies the
aggregate family that produced it and lets toOriginator route back to the exact document.
Motivation: A saga cannot subscribe before it exists. Declaring the exact start event gives FCQRS a safe point to create and subscribe the saga before that event is released.
let startsOn (event: Event<Document.Event>) =
match event.EventDetails with
| Document.PublicationRequested _ -> true
| _ -> false
let definition documentFactory slugFactory =
{ Name = "PublicationSaga"
InitialData = ()
Originator = documentFactory
HandleEvent = handleEvent
ApplySideEffects = applySideEffects documentFactory slugFactory
StartOn = startsOn
Snapshots = Default }
|
The domain publishes PublicationRequested; application code does not create SagaStartingEvent.
FCQRS wraps the matched event internally so the new saga remembers its originator, starting version,
correlation context, and the event that created it.
The safe-start sequence is:
- the document chooses
PublicationRequested; StartOnsays the event requiresPublicationSaga;- FCQRS creates and subscribes the saga;
- the saga stores its starting envelope;
- the document event is persisted and published;
handleEventstoresReservingSlug;- only then does
applySideEffectssendReserve.
Without this handshake, the first event could be published before the new saga was listening.
The C# API uses the same two-function split shown beside the F# code. HandleEvent stores progress;
ApplySideEffects returns commands and Stay, NextState, or StopSaga. The document and slug
aggregates use the HandleCommand and ApplyEvent pattern taught in chapter 1.
Wire the participants and starter
Register both aggregates first, then construct the saga from their factories. The final call installs the start predicate and handshake.
let wire (api: IActor) =
let documents =
Fcqrs.aggregate api
{ Name = "Document"
Initial = Document.initial
Decide = Document.decide
Fold = Document.fold
Snapshots = Default }
let slugs =
Fcqrs.aggregate api
{ Name = "Slug"
Initial = Slug.initial
Decide = Slug.decide
Fold = Slug.fold
Snapshots = Default }
let publication =
Fcqrs.saga api (PublicationSaga.definition documents.Factory slugs.Factory)
Fcqrs.wireSagaStarters api [ publication ]
documents
|
How resumption works
Assume the saga has stored ReportingResult Published and sent FinishPublication Published, then the process
stops. On restart FCQRS:
- loads the saga snapshot when one exists;
- replays later state changes;
- restores the starting-event context;
- subscribes the saga again;
- calls
applySideEffectsforReportingResult Publishedwithrecovering = true.
The saga cannot know whether the earlier result reached the document. It sends the command again. The
document's idempotent decision returns the same PublicationFinished event without storing a
duplicate if the first command already succeeded.
This is resumability: recover durable progress and re-drive the next safe action. It is not rewinding the other aggregate, and it is not exactly-once delivery.
Motivation: Restarting the workflow from its first command could repeat already completed work. Re-driving only the action implied by the last stored state limits repetition to one retry-safe step.
Run both outcomes
Now run the workflow. The helper below publishes one document and prints the result the saga drives
back. Two details from chapter 2 return here: the document is created with CreateOrUpdate before
Publish, and the subscription is made before sending so the result cannot slip past. The saga's
commands keep your correlation id, so one subscription sees the whole workflow — the final
PublicationFinished included.
let buildApi () : IActor =
let config = ConfigurationBuilder().Build()
let loggerFactory = LoggerFactory.Create(fun _ -> ())
let connection =
Fcqrs.connect FCQRS.Actor.DBType.Sqlite "Data Source=tutorial.db;"
Fcqrs.actor config loggerFactory (Some connection) "tutorial"
let readModel = ConcurrentDictionary<string, Document.Root>()
let handleProjection (_offset: int64) (message: obj) =
match message with
| :? Event<Document.Event> as event ->
match event.EventDetails with
| Document.Updated document -> readModel[document.Id.ToString()] <- document
| _ -> ()
| _ -> ()
let private isPublicationFinished (message: IMessageWithCID) =
match message with
| :? Event<Document.Event> as event ->
match event.EventDetails with
| Document.PublicationFinished _ -> true
| _ -> false
| _ -> false
let publishAndPrintOutcome
(documents: AggregateHandle<Document.Command, Document.Event>)
(subscriptions: FCQRS.Query.ISubscribe)
(doc: Document.Root)
slug
=
async {
let id = Fcqrs.aggregateId (doc.Id.ToString())
// The document must exist before it can be published (chapter 1's command).
let! _created =
documents.Send (Fcqrs.newCid ()) id (Document.CreateOrUpdate doc)
(fun e ->
match e with
| Document.Updated _ -> true
| _ -> false)
let cid = Fcqrs.newCid ()
let mutable outcome = None
// Subscribe BEFORE publishing: the saga's commands keep this CID, so the
// final result arrives on this same subscription.
use finished =
subscriptions.Subscribe(cid, isPublicationFinished, 1, fun message ->
outcome <- Some message)
let! _requested =
documents.Send cid id (Document.Publish slug)
(fun e ->
match e with
| Document.PublicationRequested _ -> true
| _ -> false)
do! finished.Task |> Async.AwaitTask
match outcome with
| Some(:? Event<Document.Event> as event) ->
(match event.EventDetails with
| Document.PublicationFinished(_, slug, result) ->
printfn "%s -> %A (%s)" slug result doc.Title.Value
| _ -> ())
|> ignore
| _ -> ()
}
|
let run () =
async {
let api = buildApi ()
let documents = wire api
let subscriptions = Fcqrs.projection api (Projection.single 0 handleProjection)
let docA =
Document.Root.TryCreate(Guid.NewGuid(), "FCQRS guide", "draft A") |> Result.value
let docB =
Document.Root.TryCreate(Guid.NewGuid(), "Competing guide", "draft B") |> Result.value
do! publishAndPrintOutcome documents subscriptions docA "guides/fcqrs"
do! publishAndPrintOutcome documents subscriptions docB "guides/fcqrs"
}
[<EntryPoint>]
let main _ =
run () |> Async.RunSynchronously
0
|
Publish two documents under the same slug:
|
The slug aggregate serializes both reservations and accepts only the first owner. Each publication
saga stores its own progress and safely completes the matching document. Run again without deleting
tutorial.db and both documents are rejected — the reservation from the first run is durable.
Common mistakes
-
Changing state inside
applySideEffectswithout returningNextState. Persist progress throughStateChangedEventorNextState; mutable local state disappears on recovery. - Sending commands from
handleEvent. Let FCQRS store the next state before side effects run. -
Matching too many events in
StartOn. Only the domain event that begins a new workflow should create a saga instance. -
Constructing
SagaStartingEventin application code. DeclareStartOn; FCQRS owns the runtime envelope and handshake. -
Suppressing commands whenever
recovering = true. Re-drive an idempotent command or reconcile uncertain external work, otherwise the saga can remain stuck. -
Waiting without a deadline. A slug that answers with an ignored command, or a lost message
between nodes, parks the saga forever.
ReservingSlugdeclares its wait withStayExpecting; Write a saga states the general rules and the hand-rolledtoSelfAfterform. -
Assuming
StopSagadeletes history. It completes and passivates the actor; persisted progress remains available for diagnostics and storage policy.
Continue the learning path
Next, test the state machine and evolve its events. Chapter 4 turns the decisions, folds, retries, and stored contracts from the first three chapters into executable checks.
After chapter 4, use Sagas for deeper treatment of uncertain delivery and compensation, or Write a saga as the compact implementation recipe.
<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* that 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 slugs = Fcqrs.aggregate api { Name="Slug"; Initial=...; Decide=...; Fold=... } let publication = Fcqrs.saga api (publicationDef documents.Factory slugs.Factory) Fcqrs.wireSagaStarters api [ publication ] let subs = Fcqrs.projection api (Projection.single 0 updateReadModel) // (Projection.multi when you must control which notifications publish) // send a command and await the matching aggregate reply: let! ev = documents.Send (Fcqrs.newCid()) (Fcqrs.aggregateId id) cmd (fun e -> ...) </summary>
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>
[<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: value: 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
val string: value: 'T -> string
--------------------
type string = String
type State = { Document: Root option Publication: PublicationStatus }
--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
type Command = | CreateOrUpdate of Root | Publish of slug: string | FinishPublication of PublicationResult
--------------------
type Command<'CommandDetails> = { CommandDetails: 'CommandDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Metadata: Map<string,string> } interface IEnvelope 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 = | Updated of Root | PublicationRequested of DocumentId * slug: string | PublicationFinished of DocumentId * slug: string * PublicationResult
--------------------
type Event<'EventDetails> = { EventDetails: 'EventDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Version: Version Metadata: Map<string,string> } interface IEnvelope interface IMessage interface ISerializable member Equals: Event<'EventDetails> * IEqualityComparer -> bool override ToString: unit -> string member Journaled: bool option
<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 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> Publish and fold the event in the live actor without storing it or incrementing the persisted version. </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 State = { ReservedFor: DocumentId option }
--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
type Command = | Reserve of DocumentId
--------------------
type Command<'CommandDetails> = { CommandDetails: 'CommandDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Metadata: Map<string,string> } interface IEnvelope 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 = | SlugReserved of DocumentId | SlugUnavailable of DocumentId
--------------------
type Event<'EventDetails> = { EventDetails: 'EventDetails CreationDate: DateTime Id: MessageId Sender: AggregateId option CorrelationId: CID Version: Version Metadata: Map<string,string> } interface IEnvelope interface IMessage interface ISerializable member Equals: Event<'EventDetails> * IEqualityComparer -> bool override ToString: unit -> string member Journaled: bool option
<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 = | ReservingSlug of DocumentId * string | ReportingResult of PublicationResult | 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 IEnvelope interface IMessage interface ISerializable member Equals: Event<'EventDetails> * IEqualityComparer -> bool override ToString: unit -> string member Journaled: bool option
<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> Delivered to the saga's event handler when an expectation's deadline has passed without a state transition. The handler must match this type and answer with a state change (typically to a domain failure or compensation state). An unhandled exhaustion is logged as an error and re-delivered one deadline period later; the framework never invents a terminal state. The original reply may still arrive after exhaustion — the escalated state's handler should decide what a late success means. </summary>
<summary> The current state machine state of the saga. </summary>
<summary> Declare a saga expectation: stay in this state, send `resend` now, re-send exactly those commands on `retryEvery`, and once `deadline` (measured from the persisted state-entry time, so restarts cannot postpone it) has passed without a state transition, deliver an ExpectationExhausted message to HandleEvent. The handler must answer it with a transition, typically to a failure or compensation state. Resend commands must be retry-safe and must not carry their own DelayInMs. </summary>
[<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.FromSeconds(value: float) : TimeSpan
TimeSpan.FromSeconds(seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
<summary> Re-send at a fixed interval. </summary>
<summary> Send a command to a specific aggregate instance by id (cross-aggregate). </summary>
<summary> The saga should stay in current state without changes </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> Use the global config (config:akka:persistence:snapshot-version-count), or 30. </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>
type ConfigurationBuilder = interface IConfigurationBuilder new: unit -> unit member Add: source: IConfigurationSource -> IConfigurationBuilder member Build: unit -> IConfigurationRoot member Properties: IDictionary<string,obj> member Sources: IList<IConfigurationSource>
<summary> Builds key/value-based configuration settings for use in an application. </summary>
--------------------
ConfigurationBuilder() : ConfigurationBuilder
type LoggerFactory = interface ILoggerFactory interface IDisposable new: unit -> unit + 4 overloads member AddProvider: provider: ILoggerProvider -> unit member CreateLogger: categoryName: string -> ILogger member Dispose: unit -> unit static member Create: configure: Action<ILoggingBuilder> -> ILoggerFactory
<summary>Produces instances of <see cref="T:Microsoft.Extensions.Logging.ILogger" /> classes based on the given providers.</summary>
--------------------
LoggerFactory() : LoggerFactory
LoggerFactory(providers: Collections.Generic.IEnumerable<ILoggerProvider>) : LoggerFactory
LoggerFactory(providers: Collections.Generic.IEnumerable<ILoggerProvider>, filterOptions: LoggerFilterOptions) : LoggerFactory
LoggerFactory(providers: Collections.Generic.IEnumerable<ILoggerProvider>, filterOption: Extensions.Options.IOptionsMonitor<LoggerFilterOptions>) : LoggerFactory
LoggerFactory(providers: Collections.Generic.IEnumerable<ILoggerProvider>, filterOption: Extensions.Options.IOptionsMonitor<LoggerFilterOptions>, ?options: Extensions.Options.IOptions<LoggerFactoryOptions>) : LoggerFactory
<summary> Build a SQLite/etc. Connection from a raw connection string (ShortString hidden). </summary>
<summary> Represents the type of database connection </summary>
<summary> SQLite using Microsoft.Data.Sqlite provider </summary>
<summary> Create the actor system from plain values (cluster name as a string). </summary>
type ConcurrentDictionary<'TKey,'TValue> = interface ICollection<KeyValuePair<'TKey,'TValue>> interface IEnumerable<KeyValuePair<'TKey,'TValue>> interface IEnumerable interface IDictionary<'TKey,'TValue> interface IReadOnlyCollection<KeyValuePair<'TKey,'TValue>> interface IReadOnlyDictionary<'TKey,'TValue> interface ICollection interface IDictionary new: unit -> unit + 6 overloads member AddOrUpdate: key: 'TKey * addValueFactory: Func<'TKey,'TValue> * updateValueFactory: Func<'TKey,'TValue,'TValue> -> 'TValue + 2 overloads ...
<summary>Represents a thread-safe collection of key/value pairs that can be accessed by multiple threads concurrently.</summary>
<typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
<typeparam name="TValue">The type of the values in the dictionary.</typeparam>
--------------------
ConcurrentDictionary() : ConcurrentDictionary<'TKey,'TValue>
ConcurrentDictionary(collection: Collections.Generic.IEnumerable<Collections.Generic.KeyValuePair<'TKey,'TValue>>) : ConcurrentDictionary<'TKey,'TValue>
ConcurrentDictionary(comparer: Collections.Generic.IEqualityComparer<'TKey>) : ConcurrentDictionary<'TKey,'TValue>
ConcurrentDictionary(collection: Collections.Generic.IEnumerable<Collections.Generic.KeyValuePair<'TKey,'TValue>>, comparer: Collections.Generic.IEqualityComparer<'TKey>) : ConcurrentDictionary<'TKey,'TValue>
ConcurrentDictionary(concurrencyLevel: int, capacity: int) : ConcurrentDictionary<'TKey,'TValue>
ConcurrentDictionary(concurrencyLevel: int, collection: Collections.Generic.IEnumerable<Collections.Generic.KeyValuePair<'TKey,'TValue>>, comparer: Collections.Generic.IEqualityComparer<'TKey>) : ConcurrentDictionary<'TKey,'TValue>
ConcurrentDictionary(concurrencyLevel: int, capacity: int, comparer: Collections.Generic.IEqualityComparer<'TKey>) : ConcurrentDictionary<'TKey,'TValue>
val int64: value: 'T -> int64 (requires member op_Explicit)
--------------------
type int64 = Int64
--------------------
type int64<'Measure> = int64
<summary> Interface for messages that carry a Correlation ID (CID). </summary>
<summary> What you get back after registering an aggregate. </summary>
type ISubscribe<'TDataEvent (requires 'TDataEvent :> IMessageWithCID)> = abstract Subscribe: callback: ('TDataEvent -> unit) * ?cancellationToken: CancellationToken -> IDisposable + 3 overloads
--------------------
type ISubscribe = inherit ISubscribe<IMessageWithCID>
<summary> The canonical subscription stream: a non-generic shorthand for ISubscribe&lt;IMessageWithCID&gt; — the type every FCQRS projection / read-your-writes subscription actually uses (cf. IEnumerable vs IEnumerable&lt;T&gt;). Lets consumers write ISubscribe instead of the closed generic, and inject it by that name. </summary>
<summary> An aggregate id from a string (e.g. a document/user key). Any non-blank id works: the shard names entity actors Uri.EscapeDataString(entityId), so characters Akka actor names would reject directly (spaces, %) are escaped before they reach an actor path. </summary>
<summary> Send a command and await the first matching aggregate event. </summary>
<summary> A fresh correlation id (UUID v7). </summary>
abstract FCQRS.Query.ISubscribe.Subscribe: cid: CID * take: int * ?callback: ('TDataEvent -> unit) * ?cancellationToken: Threading.CancellationToken -> FCQRS.Query.IAwaitableDisposable
abstract FCQRS.Query.ISubscribe.Subscribe: filter: ('TDataEvent -> bool) * take: int * ?callback: ('TDataEvent -> unit) * ?cancellationToken: Threading.CancellationToken -> FCQRS.Query.IAwaitableDisposable
abstract FCQRS.Query.ISubscribe.Subscribe: cid: CID * filter: ('TDataEvent -> bool) * take: int * ?callback: ('TDataEvent -> unit) * ?cancellationToken: Threading.CancellationToken -> FCQRS.Query.IAwaitableDisposable
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...
--------------------
type Async<'T>
static member Async.AwaitTask: task: Threading.Tasks.Task<'T> -> Async<'T>
module Result from Microsoft.FSharp.Core
--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
type EntryPointAttribute = inherit Attribute new: unit -> EntryPointAttribute
--------------------
new: unit -> EntryPointAttribute
FCQRS