Skip to content

Instantly share code, notes, and snippets.

@akhansari
Last active December 16, 2022 00:09
Show Gist options
  • Select an option

  • Save akhansari/095414e79ad3b3e6a20f4047c651e08f to your computer and use it in GitHub Desktop.

Select an option

Save akhansari/095414e79ad3b3e6a20f4047c651e08f to your computer and use it in GitHub Desktop.

Revisions

  1. akhansari revised this gist Jul 22, 2021. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions event-sourced-user.fsx
    Original 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, same as projections
    // 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
    // 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)
  2. akhansari revised this gist Dec 6, 2020. 1 changed file with 22 additions and 34 deletions.
    56 changes: 22 additions & 34 deletions event-sourced-user.fsx
    Original 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 }
    | Registered _ -> { state with Registered = true }
    | Verified -> { state with Verified = true }
    | EmailModified _ -> { state with Verified = false }

    let decide command state =
    match command, state with
    match (command, state) with
    | Register userInfo, { Registered = false } -> [ Registered userInfo ]
    | WasVerified, { Verified = false } -> [ Verified ]
    | ModifyEmail email, { Registered = true } -> [ EmailModified email ]
    | 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
    let handleCommand
    (read: unit -> User.Event list)
    (write: User.Event list -> unit)
    (command: User.Command)
    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: User.Event list)
    (state: User.State) // can be used for more complex scenarios
    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
    | 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
    (saveUser: UserModel -> unit)
    (addUser: UserModel -> unit)
    (updateEmail: string -> unit)
    (updateStatus: string -> unit)
    (events: User.Event list)
    (state: User.State) // can be used for more complex scenarios
    events state
    =
    for event in events do
    match event with
    @@ -127,8 +122,8 @@ module Projector =
    Age = info.Age
    Email = info.Email
    Status = "pending" }
    |> saveUser
    | User.Verified ->
    |> 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
    open System.Collections.Generic

    let db = Dictionary ()

    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 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)
    (fun email -> printfn "verification email sent to %A" email)
    Handlers.handleEvents (project userId) Mailing.requestVerification

    let handle userId command =
    // must be transactional
  3. akhansari revised this gist Dec 6, 2020. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions event-sourced-user.fsx
    Original 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 if too large
    // 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 =
    // this function must be transactional
    // must be transactional
    handleCommand userId command
    ||> handleEvents userId

  4. akhansari revised this gist Dec 6, 2020. 1 changed file with 11 additions and 9 deletions.
    20 changes: 11 additions & 9 deletions event-sourced-user.fsx
    Original 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 =
    { Verified: bool }
    { Registered: bool
    Verified: bool }

    let initialState =
    { Verified = false }
    { Registered = false
    Verified = false }

    let private evolve state event =
    match event with
    | Registered _ -> state
    | Verified -> { Verified = true }
    | EmailModified _ -> { Verified = false }
    | 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 userInfo ]
    | Register userInfo, { Registered = false } -> [ Registered userInfo ]
    | WasVerified, { Verified = false } -> [ Verified ]
    | WasVerified, { Verified = true } -> [ ]
    | ModifyEmail email, _ -> [ EmailModified email ]
    | 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 of functions if too large
    // dependencies could be a record if too large
    Projector.project
    (ReadModel.addUser userId)
    (ReadModel.updateEmail userId)
  5. akhansari revised this gist Dec 6, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion event-sourced-user.fsx
    Original 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 if too large
    // dependencies could be a record of functions if too large
    Projector.project
    (ReadModel.addUser userId)
    (ReadModel.updateEmail userId)
  6. akhansari revised this gist Dec 6, 2020. 1 changed file with 10 additions and 4 deletions.
    14 changes: 10 additions & 4 deletions event-sourced-user.fsx
    Original 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 =
    // print functions could be database operations
    // dependencies could be a record if too large
    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)
    (ReadModel.addUser userId)
    (ReadModel.updateEmail userId)
    (ReadModel.updateStatus userId)

    let handleEvents userId =
    Handlers.handleEvents
  7. akhansari revised this gist Dec 6, 2020. No changes.
  8. akhansari revised this gist Dec 6, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion event-sourced-user.fsx
    Original 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 pretty generic
    // command handler is pretty generic
    let history = read ()
    let currentState = User.rebuild history
    let events = User.decide command currentState
  9. akhansari revised this gist Dec 6, 2020. 2 changed files with 210 additions and 182 deletions.
    182 changes: 0 additions & 182 deletions User.fsx
    Original file line number Diff line number Diff line change
    @@ -1,182 +0,0 @@
    // ========= 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"

    type Info =
    { Name: string
    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
    type State =
    { Name: string option
    Age: int option
    Email: string option
    IsActive: bool }

    let initialState =
    { Name = None
    Age = None
    Email = None
    IsActive = false }

    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, 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

    // 2 ========= application

    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)
    (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
    |> 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

    // 3 ========= infra

    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

    let handleCommand =
    App.handleCommand
    EventStore.read
    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"

    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 ()

    // to run the script: > dotnet fsi User.fsx
    210 changes: 210 additions & 0 deletions event-sourced-user.fsx
    Original 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
  10. akhansari revised this gist Jul 24, 2020. 1 changed file with 0 additions and 6 deletions.
    6 changes: 0 additions & 6 deletions User.fsx
    Original file line number Diff line number Diff line change
    @@ -20,10 +20,7 @@
    Rebuild the current state from the entire history.
    *)

    // hexa arch: the order is important

    // 1 ========= domain
    // domain logic, few references, no async

    module User =

    @@ -86,7 +83,6 @@ module User =
    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
    @@ -133,7 +129,6 @@ module App =
    |> write key

    // 3 ========= infra
    // infra logic, could only reference other infras

    module EventStore =
    // event store can be anything, depending on the context
    @@ -151,7 +146,6 @@ module EventStore =
    db.[key] <- history @ events

    // 4 ========= startup
    // satisfy app deps with infra through partial applications according to the envs

    let handleCommand =
    App.handleCommand
  11. akhansari revised this gist Jul 24, 2020. 1 changed file with 23 additions and 14 deletions.
    37 changes: 23 additions & 14 deletions User.fsx
    Original file line number Diff line number Diff line change
    @@ -20,7 +20,10 @@
    Rebuild the current state from the entire history.
    *)

    // ========= domain layer
    // 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 }

    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 ->
    @@ -79,7 +85,8 @@ module User =
    let build = List.fold evolve
    let rebuild = build initialState

    // ========= application layer
    // 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

    // ========= infra layer
    // 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

    // ========= startup layer
    // 4 ========= startup
    // satisfy app deps with infra through partial applications according to the envs

    let handleCommand =
    App.handleCommand
  12. akhansari renamed this gist Jul 24, 2020. 1 changed file with 16 additions and 17 deletions.
    33 changes: 16 additions & 17 deletions UserAggregate.fsx → User.fsx
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    // ========= event sourcing in a nutshell

    (*
    AggregateName: string
    FriendlyName: string
    Aggregate friendly name.
    Initial: 'State
    @@ -22,10 +22,9 @@

    // ========= domain layer

    [<RequireQualifiedAccess>]
    module User =

    let AggregateName = "User"
    let FriendlyName = "User"

    type Info =
    { Name: string
    @@ -105,26 +104,26 @@ module AppWithoutEventStore =
    |> User.build currentState
    |> save

    type AggregateKey =
    { Name: string
    Id: string }
    type StreamKey =
    { FriendlyName: string
    FriendlyId: 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
    (read: StreamKey -> User.Event list) // read history dep
    (write: StreamKey -> User.Event list -> unit) // write new events dep
    (userId: string)
    (command: User.Command)
    =
    let aggregateKey =
    { Name = User.AggregateName
    Id = userId }
    read aggregateKey
    let key =
    { FriendlyName = User.FriendlyName
    FriendlyId = userId }
    read key
    |> User.rebuild
    |> User.decide command
    |> write aggregateKey
    |> write key

    // ========= infra layer

    @@ -151,12 +150,12 @@ let handleCommand =
    EventStore.write

    let userId = "abc123"
    let aggregateKey = { Name = User.AggregateName; Id = userId }
    let userKey = { FriendlyName = User.FriendlyName; FriendlyId = userId }

    let printStep () =
    EventStore.read aggregateKey
    EventStore.read userKey
    |> printfn "database:\n%A"
    EventStore.read aggregateKey
    EventStore.read userKey
    |> User.rebuild
    |> printfn "state:\n%A"

    @@ -177,4 +176,4 @@ User.Activate
    |> handleCommand userId
    printStep ()

    // to run the script: > dotnet fsi UserAggregate.fsx
    // to run the script: > dotnet fsi User.fsx
  13. akhansari revised this gist Jul 23, 2020. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion UserAggregate.fsx
    Original 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
    // then only the state is loaded and saved but not really recommended

    let RegisterUser
    (save: User.State -> unit)
  14. akhansari revised this gist Jul 23, 2020. 1 changed file with 7 additions and 4 deletions.
    11 changes: 7 additions & 4 deletions UserAggregate.fsx
    Original 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 with
    | Register userInfo -> [ Registered userInfo ]
    | Activate -> [ Activated ]
    | ModifyEmail email -> [ EmailModified email ]
    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
  15. akhansari revised this gist Jul 23, 2020. 1 changed file with 15 additions and 11 deletions.
    26 changes: 15 additions & 11 deletions UserAggregate.fsx
    Original 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
    (load: AggregateKey -> User.Event list) // load history dep
    (save: AggregateKey -> User.Event list -> unit) // save new events dep
    (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 }
    load aggregateKey
    read aggregateKey
    |> User.rebuild
    |> User.decide command
    |> save aggregateKey
    |> write aggregateKey

    // ========= infra layer

    module EventStore =
    // event store can be anything, depending on the context
    open System.Collections.Generic

    let db = Dictionary ()

    let load key =
    let read key =
    match db.TryGetValue key with
    | true, events -> events
    | _ -> []

    let save key events =
    let history = load key
    let write key events =
    let history = read key
    db.[key] <- history @ events

    // ========= startup layer

    let handleCommand =
    App.handleCommand
    EventStore.load
    EventStore.save
    EventStore.read
    EventStore.write

    let userId = "abc123"
    let aggregateKey = { Name = User.AggregateName; Id = userId }

    let printStep () =
    EventStore.load aggregateKey
    EventStore.read aggregateKey
    |> printfn "database:\n%A"
    EventStore.load aggregateKey
    EventStore.read aggregateKey
    |> User.rebuild
    |> printfn "state:\n%A"

  16. akhansari revised this gist Jul 23, 2020. 1 changed file with 57 additions and 47 deletions.
    104 changes: 57 additions & 47 deletions UserAggregate.fsx
    Original 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 UserInfo =
    type Info =
    { Name: string
    Age: int
    Email: string }
    @@ -14,57 +36,64 @@ module User =
    { Name: string option
    Age: int option
    Email: string option
    Activated: bool }
    IsActive: bool }

    let initialState =
    { Name = None
    Age = None
    Email = None
    Activated = false }
    IsActive = false }

    type Event =
    | UserRegistered of UserInfo
    | AccountActivated
    | Registered of Info
    | Activated
    | EmailModified of string

    type Command =
    | RegisterUser of UserInfo
    | ActivateUser
    | Register of Info
    | Activate
    | ModifyEmail of string

    let private evolve (state: State) (event: Event) : State =
    match event with
    | UserRegistered userInfo ->
    | Registered userInfo ->
    { state with
    Name = Some userInfo.Name
    Age = Some userInfo.Age
    Email = Some userInfo.Email
    Activated = false }
    | AccountActivated ->
    { state with Activated = true }
    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
    | RegisterUser userInfo -> [ UserRegistered userInfo ]
    | ActivateUser -> [ AccountActivated ]
    | Register userInfo -> [ Registered userInfo ]
    | Activate -> [ Activated ]
    | ModifyEmail email -> [ EmailModified email ]

    let build = List.fold evolve
    let rebuild = build initialState

    // ========= app layer
    // ========= application layer

    module AppWithoutEventStore =

    let RegisterUser (save: User.State -> unit) (userInfo: User.UserInfo) =
    let RegisterUser
    (save: User.State -> unit)
    (userInfo: User.Info)
    =
    let currentState = User.initialState
    User.decide (User.RegisterUser userInfo) currentState
    User.decide (User.Register userInfo) currentState
    |> User.build User.initialState
    |> save

    let ModifyEmail (load: unit -> User.State) (save: User.State -> unit) email =
    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 userId = "abc123"
    let aggregateKey = { Name = User.AggregateName; Id = userId }

    let handleCommand =
    App.handleCommand
    EventStore.load
    EventStore.save
    userId

    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.RegisterUser { Name = "John Doe"; Age = 43; Email = "[email protected]" }
    |> handleCommand
    printStep ()

    printfn "\n==== Step 3: Activate Account"
    User.ActivateUser
    |> handleCommand
    User.Register
    { Name = "John Doe"; Age = 42; Email = "[email protected]" }
    |> handleCommand userId
    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.
    printfn "\n==== Step 3: Activate Account"

    Rebuild: 'Event list -> 'State
    Rebuild the current state from the entire history.
    *)
    User.Activate
    |> handleCommand userId
    printStep ()
  17. akhansari created this gist Jul 22, 2020.
    160 changes: 160 additions & 0 deletions UserAggregate.fsx
    Original 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.
    *)