Add a projection
A projection is a function called once per event, in order. Fold each event into your read model and
record the offset in the same transaction, so processing is exactly-once across a crash. With the
FCQRS.FSharp facade the common shape is int64 -> obj -> unit (Projection.single): you just update
the read model, and FCQRS publishes each aggregate event to subscribers for you — which is what wakes a
read-your-writes wait.
open FCQRS.Common
open FCQRS.FSharp
let handle (connString: string) (offset: int64) (event: obj) : unit =
use conn = new SqliteConnection(connString)
conn.Open()
use tx = conn.BeginTransaction()
match event with
| :? Event<Document.Event> as e ->
match e.EventDetails with
| Document.Updated doc ->
conn.Execute(
"insert or replace into Documents (Id, Title, Body) values (@Id, @Title, @Body)",
{| Id = doc.Id.ToString(); Title = doc.Title.Value; Body = doc.Content.Value |}, tx)
|> ignore
| _ -> ()
| _ -> ()
// advance the offset in the SAME transaction as the writes
conn.Execute(
"update Offsets set OffsetCount = @n where OffsetName = 'DocumentProjection'",
{| n = offset |}, tx)
|> ignore
tx.Commit()
|
Register it from your composition root, resuming from the stored offset:
let subscriptions =
Fcqrs.projection api
(Projection.single (int (Db.getLastOffset connString)) (handle connString))
|
Fcqrs.projection returns an ISubscribe — the same subscription stream .Send and the
read-your-writes wait use. (subscriptions.Subscribe(cid, 1) waits for one event with a given
correlation id.)
Filtering which events wake subscribers.
Projection.singlepublishes every aggregate event. To wake read-your-writes on only some events — e.g. suppress an intermediate event so the caller unblocks on the final one — useProjection.filtered(returnPublish/Suppressper event) orProjection.multi(return the exactIMessageWithCID listto publish). In C#, theNotify-returning andIList<IMessageWithCID>-returning.AddProjectionoverloads do the same.
Two things matter most. Track the offset in the same transaction as your writes — that's what makes processing exactly-once across a crash. And rebuild freely: to fix a projection bug, correct this function, delete the read model, reset the offset to 0, and replay — the journal is untouched. Background: The read side.
val string: value: 'T -> string
--------------------
type string = System.String
val int64: value: 'T -> int64 (requires member op_Explicit)
--------------------
type int64 = System.Int64
--------------------
type int64<'Measure> = int64
module Event from Microsoft.FSharp.Control
--------------------
type Event<'T> = new: unit -> Event<'T> member Trigger: arg: 'T -> unit member Publish: IEvent<'T>
--------------------
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<'T>
--------------------
new: unit -> Event<'Delegate,'Args>
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
FCQRS