Header menu logo FCQRS

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
// The same aggregate in C#: commands/events are C# 15 unions, state is a record,
// and the two functions are switch expressions on an Aggregate<> subclass.
using static FCQRS.Common;   // Command<>, Event<>, EventAction<>
using static FCQRS.CSharp;    // Aggregate<>, EventActions

public union DocumentCommand(DocumentCommand.Create, DocumentCommand.Edit)
{
    public record Create(string Title, string Content);
    public record Edit(string Content);
}

public union DocumentEvent(
    DocumentEvent.Created, DocumentEvent.Edited,
    DocumentEvent.AlreadyExists, DocumentEvent.NoSuchDocument)
{
    public record Created(string Title, string Content);
    public record Edited(string Content);
    public record AlreadyExists;
    public record NoSuchDocument;
}

public record DocumentState(string? Title = null, string? Content = null, long Version = 0)
{
    public static readonly DocumentState Initial = new();
}

public sealed class DocumentAggregate : Aggregate<DocumentState, DocumentCommand, DocumentEvent>
{
    public override DocumentState InitialState => DocumentState.Initial;
    public override string EntityName => "Document";

    // decide
    public override EventAction<DocumentEvent> HandleCommand(
        Command<DocumentCommand> cmd, DocumentState state) =>
        (cmd.CommandDetails, state) switch
        {
            (DocumentCommand.Create c, { Title: null }) =>
                EventActions.Persist<DocumentEvent>(new DocumentEvent.Created(c.Title, c.Content)),
            (DocumentCommand.Create, _) =>
                EventActions.Defer<DocumentEvent>(new DocumentEvent.AlreadyExists()),
            (DocumentCommand.Edit e, { Title: not null }) =>
                EventActions.Persist<DocumentEvent>(new DocumentEvent.Edited(e.Content)),
            _ => EventActions.Defer<DocumentEvent>(new DocumentEvent.NoSuchDocument())
        };

    // fold
    public override DocumentState ApplyEvent(Event<DocumentEvent> evt, DocumentState state) =>
        evt.EventDetails switch
        {
            DocumentEvent.Created e => state with { Title = e.Title, Content = e.Content, Version = state.Version + 1 },
            DocumentEvent.Edited e => state with { Content = e.Content, Version = state.Version + 1 },
            _ => 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"
// C# declares the system and aggregate through the DI host-builder; FCQRS owns
// the startup ordering. (Aggregates are registered as the classes above.)
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddFcqrs("Data Source=getstarted.db;", "getstarted")
    .AddAggregate<DocumentAggregate, DocumentState, DocumentCommand, DocumentEvent>();

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 ]
    | _ -> []
// C#: the same projection function, registered with .AddProjection(...).
public static IList<IMessageWithCID> Handle(long offset, object ev) =>
    ev is Event<DocumentEvent> e
        ? new List<IMessageWithCID> { e }
        : new List<IMessageWithCID>();

builder.Services.AddProjection(handler: _ => Handle, lastOffset: _ => 0);

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
    }
// C#: resolve the command handler + subscription from DI, then the same
// subscribe-before-send, read-your-writes flow.
var app = builder.Build();
var documents = app.Services.GetRequiredService<Handler<DocumentCommand, DocumentEvent>>();
var subs = app.Services.GetRequiredService<ISubscribe>();

var cid = Values.NewCID();
var id = Values.CreateAggregateId("readme");

using var awaiter = subs.SubscribeForFirst(cid);   // subscribe BEFORE sending
var ev = await documents(
    e => e is DocumentEvent.Created or DocumentEvent.AlreadyExists,
    cid, id, new DocumentCommand.Create("README", "hello"));
await awaiter.Task;                                // read side is now up to date
Console.WriteLine($"saved {ev.EventDetails} at version {ev.Version}");

Call it from your program's entry point:

[<EntryPoint>]
let main _ =
    run () |> Async.RunSynchronously
    0

Next steps

namespace System
namespace Microsoft
namespace Microsoft.Extensions
namespace Microsoft.Extensions.Configuration
namespace Microsoft.Extensions.Logging
namespace FCQRS
module Common from FCQRS
<summary> Contains common types like Events and Commands </summary>
<namespacedoc><summary>Functionality for Write Side.</summary></namespacedoc>
namespace FCQRS.Model
module Data from FCQRS.Model
module FSharp from FCQRS
<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 -&gt; ...) </summary>
Multiple items
type State = { Title: string option Content: string option Version: int64 }

--------------------
type State<'Command,'Event> = { CommandDetails: CommandDetails<'Command,'Event> Sender: IActorRef }
Multiple items
val string: value: 'T -> string

--------------------
type string = String
type 'T option = Option<'T>
type Version = private | Version of int64 member Equals: Version * IEqualityComparer -> bool override ToString: unit -> string static member Value_: (Version -> int64) * (int64 -> Version -> Result<Version,ModelError>) static member Zero: Version
<summary> Aggregate Version </summary>
Multiple items
val int64: value: 'T -> int64 (requires member op_Explicit)

--------------------
type int64 = Int64

--------------------
type int64<'Measure> = int64
Multiple items
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. &lt;typeparam name="'CommandDetails"&gt;The specific type of the command payload.&lt;/typeparam&gt; </summary>

--------------------
type Command<'Command,'Event> = | Execute of CommandDetails<'Command,'Event>
<summary> Represents the message sent to the internal subscription mechanism. &lt;typeparam name="'Command"&gt;The type of the command payload.&lt;/typeparam&gt; &lt;typeparam name="'Event"&gt;The type of the expected event payload.&lt;/typeparam&gt; </summary>
Multiple items
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. &lt;typeparam name="'EventDetails"&gt;The specific type of the event payload.&lt;/typeparam&gt; </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>
val initial: State
union case Option.None: Option<'T>
val decide: cmd: Command<Command> -> state: State -> EventAction<Event>
 decide: command + current state -> what happened
val cmd: Command<Command>
val state: State
Command.CommandDetails: Command
<summary> The specific details or payload of the command. </summary>
union case Command.Create: title: string * content: string -> Command
val t: string
val c: string
union case Event.Created: string * string -> Event
union case EventAction.PersistEvent: 'T -> EventAction<'T>
<summary> Persist the event to the journal. The actor's state will be updated using the event handler *after* persistence succeeds. </summary>
union case Option.Some: Value: 'T -> Option<'T>
union case Event.AlreadyExists: Event
union case EventAction.DeferEvent: 'T -> EventAction<'T>
<summary> Defer the event. It will be stashed and processed later, potentially after other events. </summary>
union case Command.Edit: content: string -> Command
union case Event.Edited: string -> Event
union case Event.NoSuchDocument: Event
val fold: event: Event<Event> -> state: State -> State
 fold: apply one event to the state
val event: Event<Event>
Event.EventDetails: Event
<summary> The specific details or payload of the event. </summary>
State.Version: int64
val buildApi: unit -> IActor
type IActor = abstract CreateCommandSubscription: (string -> IEntityRef<obj>) -> CID -> AggregateId -> 'b -> ('c -> bool) -> Map<string,string> option -> Async<Event<'c>> abstract InitializeActor: 'a -> string -> (Command<'c> -> 'a -> EventAction<'b>) -> (Event<'b> -> 'a -> 'a) -> EntityFac<obj> abstract InitializeSaga: SagaState<'SagaState,'State> -> (obj -> SagaState<'SagaState,'State> -> EventAction<'State>) -> (SagaState<'SagaState,'State> -> SagaStartingEvent<Event<'c>> option -> bool -> SagaTransition<'State> * ExecuteCommand list) -> (SagaState<'SagaState,'State> -> SagaState<'SagaState,'State>) -> string -> EntityFac<obj> abstract InitializeSagaStarter: (obj -> ((string -> IEntityRef<obj>) * PrefixConversion * obj) list) -> unit + 1 overload abstract Stop: unit -> Task abstract SubscribeForCommand: Command<'a,'b> -> Async<Event<'b>> abstract Configuration: IConfiguration abstract LoggerFactory: ILoggerFactory abstract Materializer: ActorMaterializer abstract Mediator: IActorRef ...
<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>
val config: IConfigurationRoot
Multiple items
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
val loggerFactory: ILoggerFactory
Multiple items
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
LoggerFactory.Create(configure: Action<ILoggingBuilder>) : ILoggerFactory
val connection: FCQRS.Actor.Connection
module Fcqrs from FCQRS.FSharp
val connect: dbType: FCQRS.Actor.DBType -> connectionString: string -> FCQRS.Actor.Connection
<summary> Build a SQLite/etc. Connection from a raw connection string (ShortString hidden). </summary>
module Actor from FCQRS
type DBType = | Sqlite | SqlServer2012 | SqlServer2014 | SqlServer2016 | SqlServer2017 | SqlServer2019 | SqlServer2022 | PostgreSQL | PostgreSQL15 | MySql ... member Equals: DBType * IEqualityComparer -> bool member IsDB2: bool member IsFirebird: bool member IsMySql: bool member IsOracle: bool member IsPostgreSQL: bool member IsPostgreSQL15: bool member IsSqlServer2012: bool member IsSqlServer2014: bool member IsSqlServer2016: bool ...
<summary> Represents the type of database connection </summary>
union case FCQRS.Actor.DBType.Sqlite: FCQRS.Actor.DBType
<summary> SQLite using Microsoft.Data.Sqlite provider </summary>
val actor: config: IConfiguration -> loggerFactory: ILoggerFactory -> connection: FCQRS.Actor.Connection option -> clusterName: string -> IActor
<summary> Create the actor system from plain values (cluster name as a string). </summary>
val handle: _offset: int64 -> event: obj -> IMessageWithCID list
val _offset: int64
val event: obj
type obj = Object
type IMessageWithCID = abstract CID: CID
<summary> Interface for messages that carry a Correlation ID (CID). </summary>
type 'T list = List<'T>
Multiple items
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. &lt;typeparam name="'EventDetails"&gt;The specific type of the event payload.&lt;/typeparam&gt; </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>
module Document from Get-started
type Event = | Created of string * string | Edited of string | AlreadyExists | NoSuchDocument
val e: Event<Document.Event>
val run: unit -> Async<unit>
val async: AsyncBuilder
val api: IActor
val documents: AggregateHandle<Document.Command,Document.Event>
val aggregate: api: IActor -> def: Aggregate<'State,'Command,'Event> -> AggregateHandle<'Command,'Event>
<summary> Register an aggregate and return its typed handle. Calling this IS the registration (it initializes the sharding region). </summary>
union case TargetName.Name: string -> TargetName
<summary> Identify the target by its string name (entity ID). </summary>
val initial: Document.State
val decide: cmd: Command<Document.Command> -> state: Document.State -> EventAction<Document.Event>
 decide: command + current state -> what happened
val fold: event: Event<Document.Event> -> state: Document.State -> Document.State
 fold: apply one event to the state
val wireSagaStarters: api: IActor -> sagas: SagaHandle list -> unit
<summary> Wire every registered saga into one saga-starter (or the empty starter if none). Call after the aggregates + sagas are registered. </summary>
val subs: FCQRS.Query.ISubscribe
val projection: api: IActor -> p: Projection -> FCQRS.Query.ISubscribe
<summary> Register the read-model projection and return the subscription stream. </summary>
val cid: CID
val newCid: unit -> CID
<summary> A fresh correlation id (UUID v7). </summary>
val id: AggregateId
val aggregateId: s: string -> AggregateId
<summary> An aggregate id from a string (e.g. a document/user key). </summary>
val awaiter: FCQRS.Query.IAwaitableDisposable
abstract FCQRS.Query.ISubscribe.Subscribe: callback: ('TDataEvent -> unit) * ?cancellationToken: Threading.CancellationToken -> IDisposable
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
val event: Event<Document.Event>
AggregateHandle.Send: CID -> AggregateId -> Document.Command -> (Document.Event -> bool) -> Async<Event<Document.Event>>
<summary> Send a command and await the first matching event (read-your-writes). </summary>
union case Document.Command.Create: title: string * content: string -> Document.Command
val e: Document.Event
union case Document.Event.Created: string * string -> Document.Event
union case Document.Event.AlreadyExists: Document.Event
property FCQRS.Query.IAwaitable.Task: Threading.Tasks.Task with get
Multiple items
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 -> Async<unit>
static member Async.AwaitTask: task: Threading.Tasks.Task<'T> -> Async<'T>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
Multiple items
type EntryPointAttribute = inherit Attribute new: unit -> EntryPointAttribute

--------------------
new: unit -> EntryPointAttribute
static member Async.RunSynchronously: computation: Async<'T> * ?timeout: int * ?cancellationToken: System.Threading.CancellationToken -> 'T

Type something to start searching.