Get started
This page builds a complete FCQRS write-and-read loop — a Document you can create and edit — in a
single file, using only the FCQRS package and its idiomatic-F# facade, FCQRS.FSharp. No HOCON
file, no configuration ceremony: the framework ships with sensible Akka.NET defaults, and you tell it
just one thing — which database to use.
Want the why behind each piece first? Read Concepts. Want to build it up
gradually, all the way to a quota saga, with explanation at each step? Follow the
Tutorial. This page is the five-minute version; the full worked application is
focument_fsharp.
Every code block below is compiled against the pinned FCQRS package as part of building this site,
so it cannot quietly drift out of date.
Install
dotnet new console -lang F# -n MyApp
cd MyApp
dotnet add package FCQRS --prerelease
1. The aggregate
An aggregate takes commands and emits events; its state is folded from those events and is
never stored directly. The whole Document is two types, a piece of state, and two pure functions —
decide (the handleCommand) and fold (the applyEvent):
module Document =
type State = { Title: string option; Content: string option; Version: int64 }
type Command =
| Create of title: string * content: string
| Edit of content: string
type Event =
| Created of string * string
| Edited of string
| AlreadyExists
| NoSuchDocument
let initial = { Title = None; Content = None; Version = 0L }
/// decide: command + current state -> what happened
let decide (cmd: Command<Command>) state =
match cmd.CommandDetails, state with
| Create(t, c), { Title = None } -> Created(t, c) |> PersistEvent
| Create _, { Title = Some _ } -> AlreadyExists |> DeferEvent
| Edit c, { Title = Some _ } -> Edited c |> PersistEvent
| Edit _, { Title = None } -> NoSuchDocument |> DeferEvent
/// fold: apply one event to the state
let fold (event: Event<Event>) state =
match event.EventDetails with
| Created(t, c) -> { state with Title = Some t; Content = Some c; Version = state.Version + 1L }
| Edited c -> { state with Content = Some c; Version = state.Version + 1L }
| AlreadyExists
| NoSuchDocument -> state
|
PersistEvent stores the event, applies it, and publishes it. DeferEvent publishes a rejection
without storing it. Both functions are pure and Akka-free, so they are trivially testable. (Concept:
Aggregates and the write side.)
2. Wiring — no HOCON required
Fcqrs.actor builds the actor system. You only supply a database Connection (here via
Fcqrs.connect); the rest of the Akka configuration comes from built-in defaults, and an empty
IConfiguration is fine. A .hocon file is optional — see Configuration.
let buildApi () : IActor =
let config = ConfigurationBuilder().Build()
// No logging providers, to keep this to the FCQRS package alone. For
// console logs add the Microsoft.Extensions.Logging.Console package.
let loggerFactory = LoggerFactory.Create(fun _ -> ())
let connection =
Fcqrs.connect FCQRS.Actor.DBType.Sqlite "Data Source=getstarted.db;"
Fcqrs.actor config loggerFactory (Some connection) "getstarted"
|
3. A minimal read side
A projection is called once per event, in order. This one just forwards each Document event to
subscribers so a caller can be told when the read side has caught up. (Concept:
The read side.)
let handle (_offset: int64) (event: obj) : IMessageWithCID list =
match event with
| :? Event<Document.Event> as e -> [ e :> IMessageWithCID ]
| _ -> []
|
4. Send a command and read your write
Registering an aggregate with Fcqrs.aggregate returns a typed handle with a .Send that mints
nothing for you to remember: pass a correlation id, the aggregate id, the command, and a predicate
that says which event you are waiting for. Subscribe to the correlation id before sending, and by
the time the wait returns the read side has processed the event. (Concept:
Consistency and recovery.)
let run () =
async {
let api = buildApi ()
// Registering the aggregate IS the registration — it returns the handle.
let documents =
Fcqrs.aggregate api
{ Name = "Document"
Initial = Document.initial
Decide = Document.decide
Fold = Document.fold }
// No sagas yet, but the saga-starter still has to be wired (with none).
Fcqrs.wireSagaStarters api []
let subs = Fcqrs.projection api { LastOffset = 0; Handle = handle }
let cid = Fcqrs.newCid ()
let id = Fcqrs.aggregateId "readme"
// Subscribe to this CID *before* sending, so it can't be missed.
use awaiter = subs.Subscribe(cid, 1)
let! event =
documents.Send cid id (Document.Create("README", "hello"))
(fun e ->
match e with
| Document.Created _
| Document.AlreadyExists -> true
| _ -> false)
do! awaiter.Task |> Async.AwaitTask // read side is now up to date
printfn "saved %A at version %A" event.EventDetails event.Version
}
|
Call it from your program's entry point:
[<EntryPoint>]
let main _ =
run () |> Async.RunSynchronously
0
Next steps
-
Build it up with explanation — the Tutorial takes this
Documentall the way to a cross-aggregate quota saga. - Understand the model — Concepts.
- Do specific tasks — the How-to guides.
- From C# — Use FCQRS from C#.
<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>
type State = { Title: string option Content: string option Version: int64 }
--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
val string: value: 'T -> string
--------------------
type string = String
<summary> Aggregate Version </summary>
val int64: value: 'T -> int64 (requires member op_Explicit)
--------------------
type int64 = Int64
--------------------
type int64<'Measure> = int64
type Command = | Create of title: string * content: string | Edit of content: string
--------------------
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 = | Created of string * string | Edited of string | AlreadyExists | NoSuchDocument
--------------------
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>
decide: command + current state -> what happened
<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>
fold: apply one event to the state
<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>
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>
<summary> Interface for messages that carry a Correlation ID (CID). </summary>
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> 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>
decide: command + current state -> what happened
fold: apply one event to the state
<summary> Wire every registered saga into one saga-starter (or the empty starter if none). Call after the aggregates + sagas are registered. </summary>
<summary> Register the read-model projection and return the subscription stream. </summary>
<summary> A fresh correlation id (UUID v7). </summary>
<summary> An aggregate id from a string (e.g. a document/user key). </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
<summary> Send a command and await the first matching event (read-your-writes). </summary>
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>
type EntryPointAttribute = inherit Attribute new: unit -> EntryPointAttribute
--------------------
new: unit -> EntryPointAttribute
FCQRS