Query a registered user
The Query: Alice line comes from a dictionary. This handler fills it from saved registrations:
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
open System.Collections.Concurrent
open System.Threading.Tasks
let accountId = "alice"
let id = Fcqrs.aggregateId accountId
Fcqrs_250-tutorial_004-running-it.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_004-running-it.md_page.Account.RegisterUserRegisterUsername: stringstringAn abbreviation for the CLI type . Basic Types
Fcqrs_250-tutorial_004-running-it.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.DataCollectionsConcurrentThreadingTasksaccountId: stringid: AggregateIdFCQRS.FSharp.FcqrsaggregateId: string -> AggregateIdAn 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.
let users = ConcurrentDictionary<string, string>()
let ready = TaskCompletionSource<unit>(TaskCreationOptions.RunContinuationsAsynchronously)
let project (_offset: int64) (message: obj) =
match message with
| :? Event<UserRegistered> as event when event.Sender = Some id ->
let (UserRegistered name) = event.EventDetails
users[accountId] <- name
ready.TrySetResult() |> ignore
| _ -> ()
users: ConcurrentDictionary<string,string>``.ctor``: unit -> unitInitializes a new instance of the class that is empty, has the default concurrency level, has the default initial capacity, and uses the default comparer for the key type.
stringAn abbreviation for the CLI type . Basic Types
ready: TaskCompletionSource<unit>``.ctor``: TaskCreationOptions -> unitCreates a with the specified options. The options to use when creating the underlying . The represent options invalid for use with a .
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
System.Threading.Tasks.TaskCreationOptionsSpecifies flags that control optional behavior for the creation and execution of tasks.
RunContinuationsAsynchronously: TaskCreationOptionsForces continuations added to the current task to be executed asynchronously. Note that the member is available in the enumeration starting with the .NET Framework 4.6.
project: int64 -> obj -> unit_offset: int64int64An abbreviation for the CLI type . Basic Types
message: objobjAn abbreviation for the CLI type . Basic Types
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>
Fcqrs_250-tutorial_004-running-it.md_page.Account.UserRegisteredevent: Event<UserRegistered>Sender: AggregateId optionAn optional identifier for the actor that generated the event.
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
id: AggregateIdUserRegisteredname: stringEventDetails: 'EventDetailsThe specific details or payload of the event.
Item: stringGets or sets the value associated with the specified key. The key of the value to get or set. is . The property is retrieved and does not exist in the collection. The value of the key/value pair at the specified index.
TrySetResult: unit -> boolAttempts to transition the underlying into the state. The result value to bind to this . The was disposed. if the operation was successful; otherwise, .
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
var users = new ConcurrentDictionary<string, string>();
var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void Project(long offset, object message)
{
if (message is Event<UserRegistered> stored
&& stored.Sender?.Value.Equals(id) == true)
{
users[accountId] = stored.EventDetails.Name;
ready.TrySetResult();
}
}
A projection turns events into query data. Sender is the ID of the aggregate that emitted the
event; the check selects the account being queried.
The program waits for ready.Task before reading users[accountId]. Waiting only for the command reply
could read the dictionary too early. This observer is enough for the example's one immutable registration;
updates need coordination for the particular write.
The dictionary disappears when the program stops. FCQRS reads the saved event from SQLite and calls this handler to rebuild the dictionary on the next run. A 30-second wait limits this example's query wait; a timeout does not mean registration failed.
Connect it to FCQRS
Program connects the account rules and the query handler to the SQLite runtime. In F#, api is
created by Fcqrs.actor; in C#, AddFcqrs configures the application's host:
Shared setup
let register (api: IActor) =
register: IActor -> AggregateHandle<RegisterUser,UserRegistered>api: IActorFCQRS.Common.IActorDefines 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.
let accounts = Fcqrs.aggregate api
{ Name = "RegistrationFSharpAccount"; Initial = None
Decide = decide; Fold = fold; Snapshots = Default
Passivation = PassivationPolicy.Default }
Fcqrs.wireSagaStarters api []
// Register the observer before sending. Offset 0 also reads earlier registrations.
Fcqrs.projection api (Projection.single 0 project) |> ignore
accounts: AggregateHandle<RegisterUser,UserRegistered>FCQRS.FSharp.Fcqrsaggregate: IActor -> Aggregate<'State,'Command,'Event> -> AggregateHandle<'Command,'Event>Register an aggregate and return its typed handle. Calling this IS the registration (it initializes the sharding region).
api: IActorName: stringInitial: 'StateNoneThe representation of "No value"
Decide: Command<'Command> -> 'State -> EventAction<'Event>handleCommand (decide): command + current state -> what to do.
decide: Command<RegisterUser> -> string option -> EventAction<UserRegistered>Fold: Event<'Event> -> 'State -> 'StateapplyEvent (fold): event + current state -> next state (pure).
fold: Event<UserRegistered> -> string option -> string optionSnapshots: SnapshotPolicySnapshot cadence: Default (config / 30), NoSnapshots, or Every n.
DefaultUse the global config (config:akka:persistence:snapshot-version-count), or 30.
Passivation: PassivationPolicyIdle passivation: PassivationPolicy.Default (configuration, then Akka's 120s), After an idle period, or Never.
FCQRS.Common.PassivationPolicyIdle passivation for an aggregate type, set per entity at registration. Passivation stops an idle actor and releases its in-memory state; the next command recovers it from the journal. Only messages routed through cluster sharding count as activity. Sagas ignore this: their shard regions remember entities, which disables idle passivation in Akka.NET. A saga stops at StopSaga or abort instead.
DefaultUse configuration: `akka.cluster.sharding.<EntityName>.passivate-idle-entity-after`, then `akka.cluster.sharding.passivate-idle-entity-after`, then Akka.NET's 120s.
wireSagaStarters: IActor -> SagaHandle list -> unitWire every registered saga into one saga-starter (or the empty starter if none). Call after the aggregates + sagas are registered.
projection: IActor -> Projection -> FCQRS.Query.ISubscribeRegister the read-model projection and return the subscription stream.
FCQRS.FSharp.ProjectionModuleConstructors for the two projection-handler shapes.
single: int64 -> (int64 -> obj -> unit) -> ProjectionSingle-event handler: just update the read model (returns unit); each aggregate event is then published to subscribers as-is. The common case when every event is worth notifying.
project: int64 -> obj -> unit(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
Shared setup
accounts
accounts: AggregateHandle<RegisterUser,UserRegistered>var builder = Host.CreateApplicationBuilder();
builder.Logging.ClearProviders();
// Register the observer before sending. Offset 0 also reads earlier registrations.
builder.Services.AddFcqrs($"Data Source={database};", "registration-csharp")
.AddAggregate<Account>()
.AddProjection(Project, lastOffset: 0);
using var host = builder.Build();
await host.StartAsync();
Register the projection before sending so it is ready to observe events. Offset 0 starts reading
at the beginning of the journal, including registrations from earlier runs. F# also requires
wireSagaStarters api [] to complete runtime initialization even though this example has no sagas;
the C# host performs that step during startup.
Fcqrs.aggregate returns accounts in F#. In C#, AddAggregate<Account>() registers the
Handler<RegisterUser, UserRegistered> that Program obtains from the host.
For mutable data, send an edit at the version the caller observed to reject stale commands before they reach the domain decision function.
For a durable query database, save the data and its offset together. To check the registration rule without starting FCQRS, test your domain. To call it from an HTTP client, try the optional registration API.