Read your writes
Save a document and immediately open the page that shows it. With an asynchronous projection the page can still show the old content: the aggregate has stored the event, but the read model has not applied it yet. Nothing is broken, and waiting or refreshing would eventually show the save. A response that must include the caller's own change cannot ship "refresh in a moment", so read-your-writes closes the gap by waiting for the required projection before the query runs.
Motivation: Keep projections asynchronous for throughput and independence, then pay the waiting cost only for a request whose response must include its own change.
Read Correlation IDs and read-your-writes first if you need the mental model behind the sequence, projection boundary, and ephemeral notification.
To wait for every event already committed across the journal, use a transactional projection's
CatchUpAsync. Catch up projections shows the registration and the
snapshot boundary. A matching correlation notification alone does not establish that boundary.
Use the combined F# helper
Fcqrs.sendAwaiting subscribes before sending, sends the command, and waits for one projection
notification when the aggregate reply was journaled:
Shared setup
open System
open FCQRS.Model.Data
open FCQRS.Common
open FCQRS.FSharp
open FCQRS.Model.Data
module Document =
type Document = { Id: Guid; Title: ShortString; Content: LongString }
type Command = CreateOrUpdate of Document
type Event = Updated of Document | Rejected of string
open Document
open System.Threading
open FCQRS.Query
let save (api: IActor) (handle: int64 -> obj -> unit)
(documents: AggregateHandle<Document.Command, Document.Event>) cid id doc = async {
SystemFCQRSModelFCQRS.Model.DataFCQRS.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_450-how-to_005-read-your-writes.md_page.DocumentFcqrs_450-how-to_005-read-your-writes.md_page.Document.DocumentId: GuidSystem.GuidRepresents a globally unique identifier (GUID).
Title: ShortStringFCQRS.Model.Data.ShortStringValidated non-blank string up to 255 chars inclusive.
Content: LongStringFCQRS.Model.Data.LongStringRepresents any string at least 1 chars
Fcqrs_450-how-to_005-read-your-writes.md_page.Document.CommandCreateOrUpdateFcqrs_450-how-to_005-read-your-writes.md_page.Document.EventUpdatedRejectedstringAn abbreviation for the CLI type . Basic Types
ThreadingFCQRS.Querysave: IActor -> (int64 -> obj -> unit) -> AggregateHandle<Command,Event> -> CID -> AggregateId -> Document -> Async<Event<Event>>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.
handle: int64 -> obj -> unitint64An abbreviation for the CLI type . Basic Types
objAn abbreviation for the CLI type . Basic Types
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
documents: AggregateHandle<Command,Event>FCQRS.FSharp.AggregateHandle`2What you get back after registering an aggregate.
cid: CIDid: AggregateIddoc: Documentasync: 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 subs = Fcqrs.projection api (Projection.single 0 handle) // the ISubscribe stream
let! ack =
Fcqrs.sendAwaiting subs documents cid id (CreateOrUpdate doc) (function
| Document.Updated _ -> true
| _ -> false)
// On return, this projection has published the matching event. Query its model now.
subs: ISubscribeFCQRS.FSharp.Fcqrsprojection: IActor -> Projection -> ISubscribeRegister the read-model projection and return the subscription stream.
api: IActorFCQRS.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.
handle: int64 -> obj -> unitack: Event<Event>sendAwaiting: ISubscribe<IMessageWithCID> -> AggregateHandle<'Command,'Event> -> CID -> AggregateId -> 'Command -> ('Event -> bool) -> Async<Event<'Event>>Read-your-writes in one call: subscribe on the CID BEFORE sending, send, then await the projection ONLY if the delivered ack was journaled. A deferred (rejection-style) ack never reaches the journal, so a naive await would hang until timeout. The subscribe-before-send ordering is what makes the wait race-free; owning it here means callers cannot get it backwards. Awaits exactly ONE projected event: a batch persist (PersistAllEvents) caller should Subscribe with an explicit take instead. Envelopes without the delivery stamp (pre-stamp FCQRS) are treated as journaled. The projection wait is bounded by `akka.fcqrs.command-timeout` (default 30s, bare number = seconds): a projection that suppresses or filters out the matching notification raises TimeoutException instead of hanging the caller forever.
documents: AggregateHandle<Command,Event>cid: CIDid: AggregateIdCreateOrUpdatedoc: DocumentFcqrs_450-how-to_005-read-your-writes.md_page.DocumentUpdated// C# composes the same subscribe-before-send sequence explicitly.
using var projected = subscriptions.SubscribeForFirst(cid);
var reply = await documents(
isExpectedReply,
cid,
documentId,
new DocumentCommand.CreateOrUpdate(document));
if (reply.Journaled is not { Value: false })
await projected.Task;
// This projection has now published the matching event. Query its model.
The helper waits for one notification. If a command persists a batch and the projection publishes
several events for the same CID, either filter notifications so only the final required update is
published or compose a subscription with the correct take count.
The wait is bounded: if no matching notification arrives within akka.fcqrs.command-timeout (default
30s — a bare number means seconds, HOCON durations like 500ms also work), sendAwaiting raises
TimeoutException. A projection that suppresses or filters out the matching event therefore surfaces
as a timeout instead of hanging the request. See
Configuration.
Why "only if journaled"
An aggregate can persist an event or defer a reply. A deferred rejection or idempotent response is returned to the caller but never enters the journal, so no projection will receive it.
FCQRS stamps the delivered envelope with Event.Journaled : bool option:
Some true: the event was stored and can reach a projection;Some false: the reply was deferred or publish-only and will not reach a projection;None: the envelope predates or bypassed the delivery stamp.
sendAwaiting skips the projection wait for Some false. The C# sequence performs the equivalent
Journaled check explicitly.
Compose the sequence manually
Use the explicit form when waiting for several notifications, adding cancellation, or applying a notification filter:
Shared setup
return ack
}
let saveManually (subscriptions: ISubscribe)
(documents: AggregateHandle<Document.Command, Document.Event>)
(cid: CID) documentId command isExpectedReply (cancellationToken: CancellationToken) = async {
ack: Event<Event>saveManually: ISubscribe -> AggregateHandle<Command,Event> -> CID -> AggregateId -> Command -> (Event -> bool) -> CancellationToken -> Async<unit>subscriptions: ISubscribeFCQRS.Query.ISubscribeThe canonical subscription stream: a non-generic shorthand for ISubscribe<IMessageWithCID> — the type every FCQRS projection / read-your-writes subscription actually uses (cf. IEnumerable vs IEnumerable<T>). Lets consumers write ISubscribe instead of the closed generic, and inject it by that name.
documents: AggregateHandle<Command,Event>FCQRS.FSharp.AggregateHandle`2What you get back after registering an aggregate.
Fcqrs_450-how-to_005-read-your-writes.md_page.DocumentFcqrs_450-how-to_005-read-your-writes.md_page.Document.CommandFcqrs_450-how-to_005-read-your-writes.md_page.Document.Eventcid: CIDFCQRS.Model.Data.CIDCorrelationID for commands and Sagas
documentId: AggregateIdcommand: CommandisExpectedReply: Event -> boolcancellationToken: CancellationTokenSystem.Threading.CancellationTokenPropagates notification that operations should be canceled.
async: 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
use awaiter = subscriptions.Subscribe(cid, 1, cancellationToken = cancellationToken)
let! reply = documents.Send cid documentId command isExpectedReply
if reply.Journaled <> Some false then
do! awaiter.Task |> Async.AwaitTask
// Query the model maintained by subscriptions.
awaiter: IAwaitableDisposablesubscriptions: ISubscribeSubscribe: CID * int * (IMessageWithCID -> unit) option * CancellationToken option -> IAwaitableDisposableSubscribes to events matching a specific correlation ID. The correlation ID to match. Maximum number of events to process. Optional callback function to handle the event. An optional cancellation token to cancel the subscription.
cid: CIDcancellationTokencancellationToken: CancellationTokenreply: Event<Event>documents: AggregateHandle<Command,Event>Send: CID -> AggregateId -> 'Command -> ('Event -> bool) -> Async<Event<'Event>>Send a command and await the first matching aggregate event.
documentId: AggregateIdcommand: CommandisExpectedReply: Event -> boolJournaled: bool optionWhether this envelope's event was journaled, read from the delivery stamp: Some true (a projection event will follow), Some false (a deferred/publish-only reply — nothing to await), or None (an envelope that never passed through aggregate delivery, e.g. read back from the journal, or produced by a pre-stamp FCQRS).
(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
Task: Tasks.Task(|>): '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
Microsoft.FSharp.Control.FSharpAsyncHolds static members for creating and manipulating asynchronous computations. See also F# Language Guide - Async Workflows. Async Programming
AwaitTask: Tasks.Task -> Async<unit>Return an asynchronous computation that will wait for the given task to complete and return its result. The task to await. If an exception occurs in the asynchronous computation then an exception is re-raised by this function. If the task is cancelled then is raised. Note that the task may be governed by a different cancellation token to the overall async computation where the AwaitTask occurs. In practice you should normally start the task with the cancellation token returned by let! ct = Async.CancellationToken, and catch any at the point where the overall async is started. Awaiting Results
Shared setup
}
The ordering is part of correctness. Subscribing after .Send creates a race in which the projection
can publish before the subscription exists.
Subscribe registers the listener before returning. Disposing or cancelling an awaitable subscription
before it receives the requested number of notifications cancels its task; disposal does not report
that the projection has caught up.
using var awaiter = subscriptions.SubscribeForFirst(cid);
var reply = await documents(
isExpectedReply,
cid,
documentId,
command);
if (reply.Journaled is not { Value: false })
await awaiter.Task;
// Query the model maintained by subscriptions.
Wait for the right projection
A notification means that the projection publishing it has completed its handler. It says nothing about another projection with a different offset or deployment. If a response depends on several read models, wait for a completion signal representing all of them.
Subscriptions are in-memory rendezvous points, not durable messages for disconnected clients. Create the subscription as part of the active request, and decide how the API reports a projection that does not catch up in time.
The timeout story differs by API. The F# sendAwaiting helper is bounded by
akka.fcqrs.command-timeout (default 30s) and raises TimeoutException. Raw Subscribe awaiters and
the C# SubscribeForFirst awaiter are not bounded by that key — compose them with
WaitAsync(cancellationToken) in C# or a cancellation token in F#, as the C# examples in
Use FCQRS from C# show.