Last active
December 16, 2022 00:09
-
-
Save akhansari/095414e79ad3b3e6a20f4047c651e08f to your computer and use it in GitHub Desktop.
Revisions
-
akhansari revised this gist
Jul 22, 2021 . 1 changed file with 4 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -47,7 +47,7 @@ module User = // state is internal to the domain // depending on requirements it could be anything // as it's not stored, it could be fixed and replayed type State = { Registered: bool Verified: bool } @@ -81,14 +81,16 @@ module Handlers = (write: User.Event list -> unit) command = // command handler is pretty generic and could be shared let history = read () let currentState = User.rebuild history let events = User.decide command currentState let state = User.build currentState events write events (events, state) // IRL, everything here must be idempotent // caution, avoid distributed transaction and instead prefer queueing by case let handleEvents (project: User.Event list -> User.State -> unit) (requestVerification: string -> unit) -
akhansari revised this gist
Dec 6, 2020 . 1 changed file with 22 additions and 34 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -58,15 +58,15 @@ module User = let private evolve state event = match event with | Registered _ -> { state with Registered = true } | Verified -> { state with Verified = true } | EmailModified _ -> { state with Verified = false } let decide command state = match (command, state) with | Register userInfo, { Registered = false } -> [ Registered userInfo ] | WasVerified, { Verified = false } -> [ Verified ] | ModifyEmail email, { Registered = true } -> [ EmailModified email ] | _ -> [ ] // could be some Result.Error instead let build = List.fold evolve @@ -76,10 +76,10 @@ module User = module Handlers = let handleCommand (read: unit -> User.Event list) (write: User.Event list -> unit) command = // command handler is pretty generic let history = read () @@ -92,17 +92,13 @@ module Handlers = let handleEvents (project: User.Event list -> User.State -> unit) (requestVerification: string -> unit) events state // state can be used for more complex scenarios = for event in events do match event with | User.Registered info -> requestVerification info.Email | User.Verified -> () | User.EmailModified email -> requestVerification email project events state module Projector = @@ -114,11 +110,10 @@ module Projector = Status: string } let project (addUser: UserModel -> unit) (updateEmail: string -> unit) (updateStatus: string -> unit) events state = for event in events do match event with @@ -127,8 +122,8 @@ module Projector = Age = info.Age Email = info.Email Status = "pending" } |> addUser | User.Verified -> updateStatus "ok" | User.EmailModified email -> updateEmail email @@ -142,15 +137,11 @@ type StreamKey = module EventStore = // event store can be anything, depending on the context let db = System.Collections.Generic.Dictionary () let read key = match db.TryGetValue key with | true, events -> events | _ -> [] let write key events = let history = read key db.[key] <- history @ events @@ -161,17 +152,16 @@ module ReadModel = let updateEmail userId = printfn "email updated to %A" let updateStatus userId = printfn "status changed to %A" module Mailing = let requestVerification = printfn "verification email sent to %A" // 4 ========= startup module Startup = let handleCommand userId = let key = { FriendlyName = User.FriendlyName; FriendlyId = userId } Handlers.handleCommand (fun () -> EventStore.read key) (EventStore.write key) let project userId = // dependencies could be a record of functions, if too large @@ -181,9 +171,7 @@ module Startup = (ReadModel.updateStatus userId) let handleEvents userId = Handlers.handleEvents (project userId) Mailing.requestVerification let handle userId command = // must be transactional -
akhansari revised this gist
Dec 6, 2020 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -174,7 +174,7 @@ module Startup = (EventStore.write key) let project userId = // dependencies could be a record of functions, if too large Projector.project (ReadModel.addUser userId) (ReadModel.updateEmail userId) @@ -186,7 +186,7 @@ module Startup = (fun email -> printfn "verification email sent to %A" email) let handle userId command = // must be transactional handleCommand userId command ||> handleEvents userId -
akhansari revised this gist
Dec 6, 2020 . 1 changed file with 11 additions and 9 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -49,23 +49,25 @@ module User = // depending on requirements it could be anything // as it's not stored, it could be fixed and replayed, same as projections type State = { Registered: bool Verified: bool } let initialState = { Registered = false Verified = false } let private evolve state event = match event with | Registered _ -> { state with Registered = true } | Verified -> { state with Verified = true } | EmailModified _ -> { state with Verified = false } let decide command state = match command, state with | Register userInfo, { Registered = false } -> [ Registered userInfo ] | WasVerified, { Verified = false } -> [ Verified ] | ModifyEmail email, { Registered = true } -> [ EmailModified email ] | _ -> [ ] // could be some Result.Error instead let build = List.fold evolve let rebuild = build initialState @@ -172,7 +174,7 @@ module Startup = (EventStore.write key) let project userId = // dependencies could be a record if too large Projector.project (ReadModel.addUser userId) (ReadModel.updateEmail userId) -
akhansari revised this gist
Dec 6, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -172,7 +172,7 @@ module Startup = (EventStore.write key) let project userId = // dependencies could be a record of functions if too large Projector.project (ReadModel.addUser userId) (ReadModel.updateEmail userId) -
akhansari revised this gist
Dec 6, 2020 . 1 changed file with 10 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -153,6 +153,12 @@ module EventStore = let history = read key db.[key] <- history @ events module ReadModel = // print functions could be database operations let addUser userId = printfn "user saved:\n%A" let updateEmail userId = printfn "email updated to %A" let updateStatus userId = printfn "status changed to %A" // 4 ========= startup module Startup = @@ -166,11 +172,11 @@ module Startup = (EventStore.write key) let project userId = // dependencies could be a record if too large Projector.project (ReadModel.addUser userId) (ReadModel.updateEmail userId) (ReadModel.updateStatus userId) let handleEvents userId = Handlers.handleEvents -
akhansari revised this gist
Dec 6, 2020 . No changes.There are no files selected for viewing
-
akhansari revised this gist
Dec 6, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -79,7 +79,7 @@ module Handlers = (write: User.Event list -> unit) (command: User.Command) = // command handler is pretty generic let history = read () let currentState = User.rebuild history let events = User.decide command currentState -
akhansari revised this gist
Dec 6, 2020 . 2 changed files with 210 additions and 182 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,182 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,210 @@ // ========= Event Sourcing in a nutshell (* FriendlyName: string Aggregate friendly name. Initial: 'State Initial (empty) state we will start with. Decide: 'Command -> 'State -> 'Event list Given the current state and what has been requested, decide what should happen. Evolve: 'State -> 'Event -> 'State Given the current state and what happened, evolve to a new state. Build: 'State -> 'Event list -> 'State Given the current state and the history, build the state. Rebuild: 'Event list -> 'State Rebuild the current state from the entire history. *) // 1 ========= domain module User = let FriendlyName = "User" // domain is identification agnostic, userId must never be present type Info = { Name: string Age: int Email: string } // events and commands should never leak out of the domain // they should be mapped to a dto if needed type Event = | Registered of Info | Verified | EmailModified of string type Command = | Register of Info | WasVerified | ModifyEmail of string // state is internal to the domain // depending on requirements it could be anything // as it's not stored, it could be fixed and replayed, same as projections type State = { Verified: bool } let initialState = { Verified = false } let private evolve state event = match event with | Registered _ -> state | Verified -> { Verified = true } | EmailModified _ -> { Verified = false } let decide command state = match command, state with | Register userInfo, _ -> [ Registered userInfo ] | WasVerified, { Verified = false } -> [ Verified ] | WasVerified, { Verified = true } -> [ ] | ModifyEmail email, _ -> [ EmailModified email ] let build = List.fold evolve let rebuild = build initialState // 2 ========= application module Handlers = let handleCommand (read: unit -> User.Event list) (write: User.Event list -> unit) (command: User.Command) = // command handler pretty generic let history = read () let currentState = User.rebuild history let events = User.decide command currentState let state = User.build currentState events write events (events, state) let handleEvents (project: User.Event list -> User.State -> unit) (requestVerification: string -> unit) (events: User.Event list) (state: User.State) // can be used for more complex scenarios = for event in events do match event with | User.Registered info -> requestVerification info.Email | User.Verified -> () | User.EmailModified email -> requestVerification email project events state module Projector = type UserModel = { Name: string Age: int Email: string Status: string } let project (saveUser: UserModel -> unit) (updateEmail: string -> unit) (updateStatus: string -> unit) (events: User.Event list) (state: User.State) // can be used for more complex scenarios = for event in events do match event with | User.Registered info -> { Name = info.Name Age = info.Age Email = info.Email Status = "pending" } |> saveUser | User.Verified -> updateStatus "ok" | User.EmailModified email -> updateEmail email updateStatus "pending" // 3 ========= infra type StreamKey = { FriendlyName: string FriendlyId: string } module EventStore = // event store can be anything, depending on the context open System.Collections.Generic let db = Dictionary () let read key = match db.TryGetValue key with | true, events -> events | _ -> [] let write key events = let history = read key db.[key] <- history @ events // 4 ========= startup module Startup = let handleCommand userId = let key = { FriendlyName = User.FriendlyName FriendlyId = userId } Handlers.handleCommand (fun () -> EventStore.read key) (EventStore.write key) let project userId = // print functions could be database operations Projector.project (fun userModel -> printfn "user saved:\n%A" userModel) (fun email -> printfn "email updated to %A" email) (fun status -> printfn "status changed to %A" status) let handleEvents userId = Handlers.handleEvents (project userId) (fun email -> printfn "verification email sent to %A" email) let handle userId command = // this function must be transactional handleCommand userId command ||> handleEvents userId // demo let userId = "abc123" printfn "\n==== Register User" User.Register { Name = "John Doe"; Age = 42; Email = "[email protected]" } |> Startup.handle userId printfn "\n==== Was Verified" User.WasVerified |> Startup.handle userId printfn "\n==== Email Changed" User.ModifyEmail "[email protected]" |> Startup.handle userId printfn "\n==== Was Verified" User.WasVerified |> Startup.handle userId printfn "\n==== Event Store State" EventStore.db |> Seq.collect (fun kv -> kv.Value) |> Seq.iteri (fun i v -> printfn "%i- %A" (i+1) v) //> dotnet fsi event-sourced-user.fsx -
akhansari revised this gist
Jul 24, 2020 . 1 changed file with 0 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -20,10 +20,7 @@ Rebuild the current state from the entire history. *) // 1 ========= domain module User = @@ -86,7 +83,6 @@ module User = let rebuild = build initialState // 2 ========= application module AppWithoutEventStore = // event sourcing could be used only on the domain layer without an event store @@ -133,7 +129,6 @@ module App = |> write key // 3 ========= infra module EventStore = // event store can be anything, depending on the context @@ -151,7 +146,6 @@ module EventStore = db.[key] <- history @ events // 4 ========= startup let handleCommand = App.handleCommand -
akhansari revised this gist
Jul 24, 2020 . 1 changed file with 23 additions and 14 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -20,7 +20,10 @@ Rebuild the current state from the entire history. *) // hexa arch: the order is important // 1 ========= domain // domain logic, few references, no async module User = @@ -31,6 +34,19 @@ module User = Age: int Email: string } // events and commands should never leak out of the domain // they should be mapped to dto if needed type Event = | Registered of Info | Activated | EmailModified of string type Command = | Register of Info | Activate | ModifyEmail of string // state is internal to the domain // depending on requirements it could be anything // as it's not stored, it could be fixed and replayed, same as projections @@ -46,16 +62,6 @@ module User = Email = None IsActive = false } let private evolve (state: State) (event: Event) : State = match event with | Registered userInfo -> @@ -79,7 +85,8 @@ module User = let build = List.fold evolve let rebuild = build initialState // 2 ========= application // infra related logic, infra is mocked as dependencies module AppWithoutEventStore = // event sourcing could be used only on the domain layer without an event store @@ -125,7 +132,8 @@ module App = |> User.decide command |> write key // 3 ========= infra // infra logic, could only reference other infras module EventStore = // event store can be anything, depending on the context @@ -142,7 +150,8 @@ module EventStore = let history = read key db.[key] <- history @ events // 4 ========= startup // satisfy app deps with infra through partial applications according to the envs let handleCommand = App.handleCommand -
akhansari renamed this gist
Jul 24, 2020 . 1 changed file with 16 additions and 17 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,7 @@ // ========= event sourcing in a nutshell (* FriendlyName: string Aggregate friendly name. Initial: 'State @@ -22,10 +22,9 @@ // ========= domain layer module User = let FriendlyName = "User" type Info = { Name: string @@ -105,26 +104,26 @@ module AppWithoutEventStore = |> User.build currentState |> save type StreamKey = { FriendlyName: string FriendlyId: string } module App = // with an event store, everything becomes a Royce let handleCommand (read: StreamKey -> User.Event list) // read history dep (write: StreamKey -> User.Event list -> unit) // write new events dep (userId: string) (command: User.Command) = let key = { FriendlyName = User.FriendlyName FriendlyId = userId } read key |> User.rebuild |> User.decide command |> write key // ========= infra layer @@ -151,12 +150,12 @@ let handleCommand = EventStore.write let userId = "abc123" let userKey = { FriendlyName = User.FriendlyName; FriendlyId = userId } let printStep () = EventStore.read userKey |> printfn "database:\n%A" EventStore.read userKey |> User.rebuild |> printfn "state:\n%A" @@ -177,4 +176,4 @@ User.Activate |> handleCommand userId printStep () // to run the script: > dotnet fsi User.fsx -
akhansari revised this gist
Jul 23, 2020 . 1 changed file with 4 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -32,6 +32,9 @@ module User = Age: int Email: string } // state is internal to the domain // depending on requirements it could be anything // as it's not stored, it could be fixed and replayed, same as projections type State = { Name: string option Age: int option @@ -81,7 +84,7 @@ module User = module AppWithoutEventStore = // event sourcing could be used only on the domain layer without an event store // then only the state is loaded and saved but not really recommended let RegisterUser (save: User.State -> unit) -
akhansari revised this gist
Jul 23, 2020 . 1 changed file with 7 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -68,10 +68,11 @@ module User = { state with Email = Some email } let decide (command: Command) (state: State) : Event list = match command, state with | Register userInfo, _ -> [ Registered userInfo ] | Activate, { IsActive = false } -> [ Activated ] | Activate, { IsActive = true } -> [ ] | ModifyEmail email, _ -> [ EmailModified email ] let build = List.fold evolve let rebuild = build initialState @@ -172,3 +173,5 @@ printfn "\n==== Step 3: Activate Account" User.Activate |> handleCommand userId printStep () // to run the script: > dotnet fsi UserAggregate.fsx -
akhansari revised this gist
Jul 23, 2020 . 1 changed file with 15 additions and 11 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -79,6 +79,8 @@ module User = // ========= application layer module AppWithoutEventStore = // event sourcing could be used only on the domain layer without an event store // then only the state is loaded and saved let RegisterUser (save: User.State -> unit) @@ -104,51 +106,53 @@ type AggregateKey = Id: string } module App = // with an event store, everything becomes a Royce let handleCommand (read: AggregateKey -> User.Event list) // read history dep (write: AggregateKey -> User.Event list -> unit) // write new events dep (userId: string) (command: User.Command) = let aggregateKey = { Name = User.AggregateName Id = userId } read aggregateKey |> User.rebuild |> User.decide command |> write aggregateKey // ========= infra layer module EventStore = // event store can be anything, depending on the context open System.Collections.Generic let db = Dictionary () let read key = match db.TryGetValue key with | true, events -> events | _ -> [] let write key events = let history = read key db.[key] <- history @ events // ========= startup layer let handleCommand = App.handleCommand EventStore.read EventStore.write let userId = "abc123" let aggregateKey = { Name = User.AggregateName; Id = userId } let printStep () = EventStore.read aggregateKey |> printfn "database:\n%A" EventStore.read aggregateKey |> User.rebuild |> printfn "state:\n%A" -
akhansari revised this gist
Jul 23, 2020 . 1 changed file with 57 additions and 47 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,11 +1,33 @@ // ========= event sourcing in a nutshell (* AggregateName: string Aggregate friendly name. Initial: 'State Initial (empty) state we will start with. Decide: 'Command -> 'State -> 'Event list Given the current state and what has been requested, decide what should happen. Evolve: 'State -> 'Event -> 'State Given the current state and what happened, evolve to a new state. Build: 'State -> 'Event list -> 'State Given the current state and the history, build the state. Rebuild: 'Event list -> 'State Rebuild the current state from the entire history. *) // ========= domain layer [<RequireQualifiedAccess>] module User = let AggregateName = "User" type Info = { Name: string Age: int Email: string } @@ -14,57 +36,64 @@ module User = { Name: string option Age: int option Email: string option IsActive: bool } let initialState = { Name = None Age = None Email = None IsActive = false } type Event = | Registered of Info | Activated | EmailModified of string type Command = | Register of Info | Activate | ModifyEmail of string let private evolve (state: State) (event: Event) : State = match event with | Registered userInfo -> { state with Name = Some userInfo.Name Age = Some userInfo.Age Email = Some userInfo.Email IsActive = false } | Activated -> { state with IsActive = true } | EmailModified email -> { state with Email = Some email } let decide (command: Command) (state: State) : Event list = match command with | Register userInfo -> [ Registered userInfo ] | Activate -> [ Activated ] | ModifyEmail email -> [ EmailModified email ] let build = List.fold evolve let rebuild = build initialState // ========= application layer module AppWithoutEventStore = let RegisterUser (save: User.State -> unit) (userInfo: User.Info) = let currentState = User.initialState User.decide (User.Register userInfo) currentState |> User.build User.initialState |> save let ModifyEmail (load: unit -> User.State) (save: User.State -> unit) email = let currentState = load () User.decide (User.ModifyEmail email) currentState |> User.build currentState @@ -108,14 +137,13 @@ module EventStore = // ========= startup layer let handleCommand = App.handleCommand EventStore.load EventStore.save let userId = "abc123" let aggregateKey = { Name = User.AggregateName; Id = userId } let printStep () = EventStore.load aggregateKey @@ -125,36 +153,18 @@ let printStep () = |> printfn "state:\n%A" printfn "==== Step 1 : Empty EventStore" printStep () printfn "\n==== Step 2 : Register User" User.Register { Name = "John Doe"; Age = 42; Email = "[email protected]" } |> handleCommand userId printStep () printfn "\n==== Step 3: Activate Account" User.Activate |> handleCommand userId printStep () -
akhansari created this gist
Jul 22, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,160 @@ // ========= domain layer module User = let AggregateName = "User" type UserInfo = { Name: string Age: int Email: string } type State = { Name: string option Age: int option Email: string option Activated: bool } let initialState = { Name = None Age = None Email = None Activated = false } type Event = | UserRegistered of UserInfo | AccountActivated | EmailModified of string type Command = | RegisterUser of UserInfo | ActivateUser | ModifyEmail of string let private evolve (state: State) (event: Event) : State = match event with | UserRegistered userInfo -> { state with Name = Some userInfo.Name Age = Some userInfo.Age Email = Some userInfo.Email Activated = false } | AccountActivated -> { state with Activated = true } | EmailModified email -> { state with Email = Some email } let decide (command: Command) (state: State) : Event list = match command with | RegisterUser userInfo -> [ UserRegistered userInfo ] | ActivateUser -> [ AccountActivated ] | ModifyEmail email -> [ EmailModified email ] let build = List.fold evolve let rebuild = build initialState // ========= app layer module AppWithoutEventStore = let RegisterUser (save: User.State -> unit) (userInfo: User.UserInfo) = let currentState = User.initialState User.decide (User.RegisterUser userInfo) currentState |> User.build User.initialState |> save let ModifyEmail (load: unit -> User.State) (save: User.State -> unit) email = let currentState = load () User.decide (User.ModifyEmail email) currentState |> User.build currentState |> save type AggregateKey = { Name: string Id: string } module App = let handleCommand (load: AggregateKey -> User.Event list) // load history dep (save: AggregateKey -> User.Event list -> unit) // save new events dep (userId: string) (command: User.Command) = let aggregateKey = { Name = User.AggregateName Id = userId } load aggregateKey |> User.rebuild |> User.decide command |> save aggregateKey // ========= infra layer module EventStore = open System.Collections.Generic let db = Dictionary () let load key = match db.TryGetValue key with | true, events -> events | _ -> [] let save key events = let history = load key db.[key] <- history @ events // ========= startup layer let userId = "abc123" let aggregateKey = { Name = User.AggregateName; Id = userId } let handleCommand = App.handleCommand EventStore.load EventStore.save userId let printStep () = EventStore.load aggregateKey |> printfn "database:\n%A" EventStore.load aggregateKey |> User.rebuild |> printfn "state:\n%A" printfn "==== Step 1 : Empty EventStore" printStep () printfn "\n==== Step 2 : Register User" User.RegisterUser { Name = "John Doe"; Age = 43; Email = "[email protected]" } |> handleCommand printStep () printfn "\n==== Step 3: Activate Account" User.ActivateUser |> handleCommand printStep () // ========= documentation (* Name: string Aggregate friendly name. Initial: 'State Initial (empty) state we will start with. Decide: 'Command -> 'State -> 'Event list Given the current state and what has been requested, decide what should happen. Evolve: 'State -> 'Event -> 'State Given the current state and what happened, evolve to a new state. Build: 'State -> 'Event list -> 'State Given the current state and the history, build the state. Rebuild: 'Event list -> 'State Rebuild the current state from the entire history. *)