Skip to content

Instantly share code, notes, and snippets.

@DamianReeves
Last active December 20, 2016 15:36
Show Gist options
  • Save DamianReeves/9e785e1e6c9f5023ea568630c8454182 to your computer and use it in GitHub Desktop.
Save DamianReeves/9e785e1e6c9f5023ea568630c8454182 to your computer and use it in GitHub Desktop.
namespace FSharp.EventSourcing
open System
open System.Threading.Tasks
open Orleankka
open Orleankka.FSharp
open Strategies.Contracts
open System.Collections
[<AbstractClass>]
type CqsActor<'Command,'Query> =
inherit Actor
interface IActor
new () = { inherit Orleankka.Actor(); }
new (id:string, runtime:IActorRuntime) = {
inherit Orleankka.Actor(id, runtime, null);
}
override this.OnReceive message = task {
match message with
| :? 'Command as cmd -> return! this.OnCommand(cmd)
| :? 'Query as query -> return! this.OnQuery(query)
| _ -> sprintf "Received unexpected message of type %s" (message.GetType().ToString()) |> failwith
return nothing
}
abstract member OnCommand: command:'Command -> Task<obj>
abstract member OnQuery: query: 'Query -> Task<obj>
abstract member Activate: unit -> Task<unit>
default this.Activate() = Task.completedTask
override this.OnActivate() = this.Activate() :> Task
[<AbstractClass>]
type EventSourcedActor<'Command,'Query,'Event>() =
inherit CqsActor<'Command,'Query>()
override this.OnCommand command = task {
let! events = this.Handle(command)
this.ApplyEvents(events)
return nothing
}
/// Handle the command
abstract member Handle: command:'Command -> Task<'Event seq>
/// Handle/apply the event
abstract member On: event:'Event -> unit
abstract member ApplyEvents: events:'Event seq -> unit
default this.ApplyEvents events =
for event in events do
this.On(event)
module StreamStoneSample
open System
open System.Threading.Tasks
open Orleankka
open Orleankka.FSharp
open FSharp.EventSourcing
type InventoryItemDetails = {
Name:string
Total:int
Active:bool
}
type Command =
| Create of Name:string
| CheckIn of Quantity:int
| CheckOut of Quantity:int
| Rename of NewName:string
| Deactivate
type Query =
| GetDetails
type Event =
| InventoryItemCreated of Name:string
| InventoryItemCheckedIn of Quantity:int
| InventoryItemCheckedOut of Quantity:int
| InventoryItemRenamed of OldName:string * NewName:string
| InventoryItemDeactivated
[<Reentrant(typeof<Query>)>]
type InventoryItem() as this =
inherit EventSourcedActor<Command,Query,Event>()
let mutable name = String.Empty;
let mutable total = 0;
let mutable active = false;
let checkIfActive() =
if not active then
sprintf "%s item is deactivated" this.Id
|> invalidOp
|> raise
let onCreate name = seq {
if String.IsNullOrWhiteSpace(name) then
"Inventory item name cannot be null or empty" |> invalidArg "name" |> raise
match name with
| null ->
sprintf "Inventory item with id %s has been already created" this.Id
|> invalidOp
|> raise
| _ -> ()
yield InventoryItemCreated name
}
let onRename newName = seq {
checkIfActive()
if String.IsNullOrEmpty(newName) then
"Inventory item name cannot be null or empty"
|> invalidArg "newName"
|> raise
yield InventoryItemRenamed (name, newName)
}
let onCheckIn quantity = seq {
checkIfActive()
match quantity with
| qty when qty <= 0 ->
"must have a qty greater than 0 to add to inventory"
|> invalidOp
|> raise
| qty ->
yield InventoryItemCheckedIn(qty)
}
let onCheckOut quantity = seq {
checkIfActive()
match quantity with
| qty when qty <= 0 ->
"can't remove negative qty from inventory"
|> invalidOp
|> raise
| qty ->
yield InventoryItemCheckedOut(qty)
}
let onDeactivate () = seq {
checkIfActive()
yield InventoryItemDeactivated
}
/// <summary>
/// Handles events.
/// </summary>
/// <remarks>
/// The event handler is where the actually changes to state occur.
/// </remarks>
/// <param name="event"></param>
override x.On event =
match event with
| InventoryItemCreated itemName ->
name <- itemName
active <- true
| InventoryItemRenamed (_, newName) -> name <- newName
| InventoryItemCheckedIn newTotal -> total <- newTotal
| InventoryItemCheckedOut newTotal -> total <- newTotal
| InventoryItemDeactivated -> active <- false
override x.OnQuery query = task {
match query with
| GetDetails -> return {Name=name;Total=total;Active=active} :> obj
}
/// Handles commands
override x.Handle command = task{
match command with
| Create name -> return onCreate name
| Rename newName -> return onRename newName
| CheckIn qty -> return onCheckIn qty
| CheckOut qty -> return onCheckOut qty
| Deactivate -> return onDeactivate()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment