Try another registration
Run the registration sample once to save Alice. Then change one input.
Ask to register Bob
In the sample's Program.fs / Program.cs, change the command to:
Shared setup
module Account =
open FCQRS.Common
open FCQRS.FSharp
type RegisterUser = RegisterUser of name: string
type UserRegistered = UserRegistered of name: string
let decide (command: Command<RegisterUser>) (state: string option) =
let (RegisterUser name) = command.CommandDetails
persistIf state.IsNone (UserRegistered(defaultArg state name))
let fold (event: Event<UserRegistered>) (_state: string option) =
let (UserRegistered name) = event.EventDetails
Some name
open System
open FCQRS.Model.Data
open FCQRS.Common
open FCQRS.FSharp
open Account
let send (accounts: AggregateHandle<RegisterUser, UserRegistered>) (id: AggregateId) = async {
Fcqrs_250-tutorial_003-the-aggregate.md_page.AccountFCQRSFCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
FCQRS.FSharpIdiomatic-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 -> ...)
Fcqrs_250-tutorial_003-the-aggregate.md_page.Account.RegisterUserRegisterUsername: stringstringAn abbreviation for the CLI type . Basic Types
Fcqrs_250-tutorial_003-the-aggregate.md_page.Account.UserRegisteredUserRegistereddecide: Command<RegisterUser> -> string option -> EventAction<UserRegistered>command: Command<RegisterUser>FCQRS.Common.Command`1Represents a command to be processed by an aggregate actor. <typeparam name="'CommandDetails">The specific type of the command payload.</typeparam>
state: string optionoptionThe type of optional values. When used from other CLI languages the empty option is the null value. Use the constructors Some and None to create values of this type. Use the values in the Option module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value null to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of null as a value representation. Options
CommandDetails: 'CommandDetailsThe specific details or payload of the command.
persistIf: bool -> 'e -> EventAction<'e>Persist the event when `shouldPersist`, else defer it (published and folded but not journalled). The deferred fold should preserve state, because it cannot be replayed. This is the idempotent "emit this verdict, write it only once" shape.
IsNone: boolReturn 'true' if the option is a 'None' value.
defaultArg: 'T option -> 'T -> 'TUsed to specify a default value for an optional argument in the implementation of a function An option representing the argument. The default value of the argument. The argument value. If it is None, the defaultValue is returned. type Vector(x: double, y: double, ?z: double) = let z = defaultArg z 0.0 member this.X = x member this.Y = y member this.Z = z let v1 = Vector(1.0, 2.0) v1.Z // Evaluates to 0. let v2 = Vector(1.0, 2.0, 3.0) v2.Z // Evaluates to 3.0
fold: Event<UserRegistered> -> string option -> string optionevent: Event<UserRegistered>FCQRS.Common.Event`1Represents 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>
_state: string optionEventDetails: 'EventDetailsThe specific details or payload of the event.
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
SystemModelFCQRS.Model.Datasend: AggregateHandle<RegisterUser,UserRegistered> -> AggregateId -> Async<Event<UserRegistered>>accounts: AggregateHandle<RegisterUser,UserRegistered>FCQRS.FSharp.AggregateHandle`2What you get back after registering an aggregate.
id: AggregateIdFCQRS.Model.Data.AggregateIdasync: AsyncBuilderBuilds an asynchronous workflow using computation expression syntax. let sleepExample() = async { printfn "sleeping" do! Async.Sleep 10 printfn "waking up" return 6 } sleepExample() |> Async.RunSynchronously
let! reply = accounts.Send (Fcqrs.newCid ()) id (RegisterUser "Bob") (fun _ -> true)
reply: Event<UserRegistered>accounts: AggregateHandle<RegisterUser,UserRegistered>Send: CID -> AggregateId -> 'Command -> ('Event -> bool) -> Async<Event<'Event>>Send a command and await the first matching aggregate event.
FCQRS.FSharp.FcqrsnewCid: unit -> CIDA fresh correlation id (UUID v7).
id: AggregateIdRegisterUservar reply = await accounts(_ => true, Values.NewCID(), id, new RegisterUser("Bob"));
Keep accountId set to "alice" and run the same project again:
Already registered: Alice (version 1)
Query: Alice
The handler uses the existing name when the account is already registered. Its conditional-persist
call returns a deferred UserRegistered("Alice") reply. Applying that reply leaves state unchanged,
and no second event is saved.
If the reply used "Bob" instead, applying it would change the in-memory name without recording the
change. Restarting would recover "Alice" again. That is why the
handler chooses the name before deciding whether to persist.
Create a different account
Keep the command's name as "Bob" and change the account ID near the top of Program:
Shared setup
return reply
}
reply: Event<UserRegistered>let accountId = "bob"
accountId: stringvar accountId = "bob";
Run again:
Registered: Bob (version 1)
Query: Bob
alice and bob identify separate aggregate instances, each with its own state and event history.
The account ID selects whose rules run; the name is the data that account stores.
Set the ID back to "alice" and run again: Alice is still registered at version 1.
Restore the command's name to "Alice" afterward.