1. The aggregate
Most applications store the current document. Saving a new body replaces the old one. An event-sourced
document stores facts such as DocumentCreated and ContentEdited, then derives the current document
by applying those facts in order.
This chapter models that write side. It uses no actor system or database yet. You will define the domain values, separate requests from recorded outcomes, and write the two pure functions FCQRS runs inside an aggregate.
Course position: the quickstart showed the whole request path. This chapter isolates its first part, the aggregate decision. By the end you will be able to explain and test command, event, state,
decide, andfoldwithout running Akka.NET.
This wireframe is the chapter's result: validated values, message types, and three functions with their bodies left unimplemented. The rest of the chapter fills them in.
// Values: Title and Content, validated string types that reject illegal values.
// Document.Root, State, Command, and Event: the document and its message types.
module Document =
let decide (cmd: Command<Command>) (state: State) : EventAction<Event> =
failwith "not written yet"
let fold (event: Event<Event>) (state: State) : State =
failwith "not written yet"
let register (api: IActor) : AggregateHandle<Command, Event> =
failwith "not written yet"
|
Commands and events are not the same thing
A command asks the system to do something, such as CreateOrUpdate. The aggregate may accept or
reject it. An event records an outcome, such as Updated or Rejected. A stored event is already a
fact and is not edited when a later command arrives.
Commands and events are different types because one request can have several outcomes. Keeping them separate also lets the publication workflow in chapter 3 add a new outcome without pretending that every command succeeds.
Motivation: A command records intent; an event records what the domain decided. Keeping those moments separate makes rejection explicit and prevents an unaccepted request from entering history as though it were a fact.
An aggregate owns the state and rules needed to make decisions about one entity. FCQRS runs each aggregate as an actor that handles one command at a time. Sequential handling eliminates races within that aggregate. Rules spanning several aggregates require coordination, which chapter 3 introduces.
Open Program.fs in the project from the tutorial intro and follow along. The opens
first:
open System
open FCQRS.Common
open FCQRS.Model.Data
open FCQRS.FSharp
|
Make the illegal values impossible to type
A title and document body have different meaning even though both arrive as strings. Separate domain
types prevent them from being swapped and provide one place to reject invalid input. FCQRS provides
validated ShortString and LongString values through ValueLens; the document wraps them as Title
and Content.
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
|
TryCreate returns a Result, so invalid input is handled where raw strings enter the application.
After construction, Title and Content carry validated values and the aggregate does not repeat the
same validation.
State, command, event
Root.TryCreate validates the title and content together and returns either a complete document or one
error. Commands therefore carry a complete Root, not a mixture of raw and validated fields.
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
|
State is the value FCQRS keeps in the actor and rebuilds during recovery. Before any event has been
stored, the document is absent:
type State = { Document: Root option }
let initial = { Document = None }
|
The first model has one command and one event. Chapter 3 adds a separate Publish request with several
possible outcomes. Defining commands and events separately now leaves room for that growth.
type Command = CreateOrUpdate of Root
type Event = Updated of Root
|
Decide what the command means
decide receives a command envelope and the current state. The envelope carries the command payload,
creation time, correlation id, and metadata. The function returns an EventAction describing what
FCQRS should do. It performs no mutation or I/O itself.
let decide (cmd: Command<Command>) state =
match cmd.CommandDetails with
| CreateOrUpdate doc -> Updated doc |> PersistEvent
|
The common actions are:
-
PersistEvent event: append the event, increment the aggregate version, fold it into state, and publish it. -
DeferEvent reply: publish and fold a reply without storing it or incrementing the persisted version. Use this for a rejection or idempotent response whose fold leaves state unchanged. Any state change made only by a deferred event disappears on recovery. IgnoreEvent: produce no reply or state change.UnhandledEvent: report that the command is not valid for this handler or state.
Persist only facts needed to reconstruct the aggregate. Operational auditing of rejected attempts belongs in logs or a separate audit model unless the rejection itself changes the domain.
fold: rebuild the present from the past
FCQRS calls fold after persisting a new event and again when replaying stored events during recovery.
The same event sequence must produce the same state in both cases.
let fold (event: Event<Event>) state =
match event.EventDetails with
| Updated doc -> { Document = Some doc }
|
fold must not read the clock, generate random values, or perform I/O. If a decision needs the current
time, read the creation time from the command and include the relevant value in the event. Recovery then
uses the value that was recorded when the decision was made.
Bind the functions to an actor
Fcqrs.aggregate registers the functions with the actor system and returns a typed handle used to send
commands. The actor lifecycle, sharding, persistence, and recovery stay outside the domain functions.
let register (api: IActor) =
Fcqrs.aggregate api
{ Name = "Document"
Initial = initial
Decide = decide
Fold = fold
Snapshots = Default } // snapshot cadence: Default | NoSnapshots | Every n
|
Name, Initial, Decide, Fold, and Snapshots form the aggregate definition. The domain functions
do not depend on the actor implementation.
What you now understand
The write model now has four distinct parts: a command requests a change, decide chooses an action, a
persisted event records the result, and fold derives state from stored events. You can test each
decision with ordinary function calls:
let doc = Document.Root.TryCreate(System.Guid.NewGuid(), "Spec", "draft") |> Result.value
// decide returns an action, so the test asserts on that value.
let action = Document.decide (cmd (Document.CreateOrUpdate doc)) Document.initial
// => PersistEvent (Updated doc)
|
Common mistakes
-
Reading changing values in
fold. Capture time and generated ids before persistence and carry them in the event. - Persisting rejections. Use
DeferEventwhen the reply does not represent a state change. - Passing raw strings through the domain. Parse them into domain values at the application edge.
- Treating aggregate state as stored data. It is derived from the event history during recovery.
Continue the learning path
Next, run the aggregate and project its events. Chapter 2 uses the domain model you just built and introduces the runtime, journal, projection, and query path.
After completing chapter 2, use Aggregates and the write side for a deeper boundary-design discussion or Define an aggregate as the short implementation recipe. Neither is required before continuing.
<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: 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
type State = { Document: Root option }
--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
type Command = | CreateOrUpdate of Root
--------------------
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
--------------------
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> The specific details or payload of the event. </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> Identify the target by its string name (entity ID). </summary>
<summary> Use the global config (config:akka:persistence:snapshot-version-count), or 30. </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>
--------------------
System.Guid ()
System.Guid(b: byte array) : System.Guid
System.Guid(b: System.ReadOnlySpan<byte>) : System.Guid
System.Guid(g: string) : System.Guid
System.Guid(b: System.ReadOnlySpan<byte>, bigEndian: bool) : System.Guid
System.Guid(a: int, b: int16, c: int16, d: byte array) : System.Guid
System.Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : System.Guid
System.Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : System.Guid
module Result from Microsoft.FSharp.Core
--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
FCQRS