Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eqxFc: Allocation Example #41

Draft
wants to merge 45 commits into
base: add-fc
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8013724
Add Location initial impl
bartelink Nov 29, 2019
1d85820
Add Fc template wrapping
bartelink Nov 29, 2019
5977207
Correct AccessStrategy
bartelink Nov 29, 2019
55d192b
Fix streamId ordering
bartelink Nov 29, 2019
bff353f
Tidy log output
bartelink Nov 29, 2019
7423302
Tidy connection logic
bartelink Nov 29, 2019
94164b2
Add README
bartelink Nov 29, 2019
cb323fd
Move AggregateId to Events
bartelink Dec 7, 2019
03445be
Aggregate layout/naming consistency
bartelink Dec 7, 2019
72b4159
Sync layout
bartelink Jan 17, 2020
c9c56b6
wip
bartelink Feb 12, 2020
4b06d77
wip
bartelink Feb 12, 2020
98e524c
Complete Location, Inventory and LocationTests
bartelink Feb 13, 2020
d5844dd
Yes, comments
bartelink Feb 13, 2020
6ccceb8
tmp
bartelink Feb 13, 2020
479378a
WIP code snipped
bartelink Feb 13, 2020
03bab4f
Add missing README.md
bartelink Feb 13, 2020
bf6813b
Tidy accessStrategy
bartelink Feb 14, 2020
6b8d0cf
Formatting
bartelink Feb 14, 2020
fc139b5
InventoryTransaction wip
bartelink Feb 14, 2020
49f726c
Handle FsCodec _ restriction
bartelink Feb 14, 2020
b251291
Remove inventoryId from Trans SN
bartelink Feb 15, 2020
4a56430
Remove checkpoints from InventorySeries
bartelink Feb 17, 2020
4e53aa1
Tidying / formatting
bartelink Feb 17, 2020
95afe89
Fix batch vs epoch naming in inventory
bartelink Feb 17, 2020
790a4e8
Complete Process Manager Apply
bartelink Feb 18, 2020
ed33c94
Target V2s
bartelink Feb 19, 2020
18a8059
wip
bartelink Feb 19, 2020
a8ecd75
Process Manager wip
bartelink Feb 21, 2020
dbc1f44
Minor renames; tests not yet compiling
bartelink Feb 21, 2020
9d1a9c1
WIP
bartelink Feb 23, 2020
b473d2b
Remove sln mess
bartelink Feb 25, 2020
edb99a7
Cover denial of Remove action
bartelink Feb 25, 2020
29077e1
Add explicit Service action methods on Process Manager
bartelink Feb 26, 2020
c69b8e4
Add Watchdog
bartelink Feb 26, 2020
16bd139
Add Watchdog to Fc sln
bartelink Feb 26, 2020
5ce4774
Tidy wiring
bartelink Feb 27, 2020
ba7c315
Apply style changes from 4.2
bartelink Mar 10, 2020
e73d6f3
Stragglers
bartelink Mar 10, 2020
e236bcd
Allocation example from Equinox 174
bartelink Nov 30, 2019
8bb5e6c
Aggregate layout/naming consistency
bartelink Dec 7, 2019
d68c643
Formatting consistency
bartelink Dec 7, 2019
aee5367
Formatting consistency
bartelink Dec 7, 2019
f950013
Update to FsCodec 2.0.0-rc3
bartelink Feb 13, 2020
d13f4d7
Tidy resolvers
bartelink Mar 11, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ The `Unreleased` section name is replaced by the expected version of next releas
### Added

- Add handling for `-T` TCP/IP switch on EventStore args [#46](https://github.com/jet/dotnet-templates/pulls/46)
- `equinox-fc`: Fulfilment-Center inspired example utilizing Process Manager patterns with basic `Equinox.MemoryStore` and `Equinox.Cosmos` tests [#40](https://github.com/jet/dotnet-templates/pulls/40)

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This repo hosts the source for Jet's [`dotnet new`](https://docs.microsoft.com/e
- [`eqxweb`](equinox-web/README.md) - Boilerplate for an ASP .NET Core 2 Web App, with an associated storage-independent Domain project using [Equinox](https://github.com/jet/equinox).
- [`eqxwebcs`](equinox-web-csharp/README.md) - Boilerplate for an ASP .NET Core 2 Web App, with an associated storage-independent Domain project using [Equinox](https://github.com/jet/equinox), _ported to C#_.
- [`eqxtestbed`](equinox-testbed/README.md) - Host that allows running back-to-back benchmarks when prototyping models using [Equinox](https://github.com/jet/equinox), using different stores and/or store configuration parameters.
- [`eqxfc`](equinox-fc/README.md) - Samples showcasing various modeling and testing techniques such as (FsCheck-based) unit tests and use of `MemoryStore` for integration tests.

## [Propulsion](https://github.com/jet/propulsion) related

Expand Down
31 changes: 27 additions & 4 deletions dotnet-templates.sln
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ EndProjectSection
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.Templates", "src\Equinox.Templates\Equinox.Templates.fsproj", "{8C92B728-85A5-4231-863A-E4236E46CC36}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fcInventory", "fcInventory", "{4946576F-1558-49ED-A272-6F4D92FB0031}"
ProjectSection(SolutionItems) = preProject
equinox-fc\.template.config\template.json = equinox-fc\.template.config\template.json
equinox-fc\README.md = equinox-fc\README.md
EndProjectSection
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain", "equinox-fc\Domain\Domain.fsproj", "{B3CFC965-6AB9-47E8-AA47-548A8D8A2E2C}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain.Tests", "equinox-fc\Domain.Tests\Domain.Tests.fsproj", "{49890A45-D6C2-4EF6-87AD-39960E03E254}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Watchdog", "equinox-fc\Watchdog\Watchdog.fsproj", "{46B8B7C9-3334-4C13-A339-57571C14F2B9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -128,10 +140,18 @@ Global
{F66A5BFE-7C81-44DC-97DE-3FD8C83B8F06}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F66A5BFE-7C81-44DC-97DE-3FD8C83B8F06}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F66A5BFE-7C81-44DC-97DE-3FD8C83B8F06}.Release|Any CPU.Build.0 = Release|Any CPU
{8C92B728-85A5-4231-863A-E4236E46CC36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C92B728-85A5-4231-863A-E4236E46CC36}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C92B728-85A5-4231-863A-E4236E46CC36}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C92B728-85A5-4231-863A-E4236E46CC36}.Release|Any CPU.Build.0 = Release|Any CPU
{B3CFC965-6AB9-47E8-AA47-548A8D8A2E2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3CFC965-6AB9-47E8-AA47-548A8D8A2E2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3CFC965-6AB9-47E8-AA47-548A8D8A2E2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3CFC965-6AB9-47E8-AA47-548A8D8A2E2C}.Release|Any CPU.Build.0 = Release|Any CPU
{49890A45-D6C2-4EF6-87AD-39960E03E254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49890A45-D6C2-4EF6-87AD-39960E03E254}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49890A45-D6C2-4EF6-87AD-39960E03E254}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49890A45-D6C2-4EF6-87AD-39960E03E254}.Release|Any CPU.Build.0 = Release|Any CPU
{46B8B7C9-3334-4C13-A339-57571C14F2B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46B8B7C9-3334-4C13-A339-57571C14F2B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46B8B7C9-3334-4C13-A339-57571C14F2B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46B8B7C9-3334-4C13-A339-57571C14F2B9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F66A5BFE-7C81-44DC-97DE-3FD8C83B8F06} = {B72FFAAE-7801-41B2-86F5-FD90E97A30F7}
Expand All @@ -145,5 +165,8 @@ Global
{D7ACBDF8-5F24-420F-9657-20096CE08B49} = {818D28A6-E6AB-4416-BDA6-1577C5D54447}
{B6389F9E-A8E4-4BD7-B4C0-703B1A69BEA1} = {E7434881-8655-4C22-82CD-91ADB5123A73}
{36C2D70A-F292-4481-8ADA-5066A80F92B2} = {1F3C9245-F973-43A3-97C9-5E527B93060C}
{B3CFC965-6AB9-47E8-AA47-548A8D8A2E2C} = {4946576F-1558-49ED-A272-6F4D92FB0031}
{49890A45-D6C2-4EF6-87AD-39960E03E254} = {4946576F-1558-49ED-A272-6F4D92FB0031}
{46B8B7C9-3334-4C13-A339-57571C14F2B9} = {4946576F-1558-49ED-A272-6F4D92FB0031}
EndGlobalSection
EndGlobal
18 changes: 18 additions & 0 deletions equinox-fc/.template.config/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "@jet @bartelink",
"classifications": [
"Equinox",
"Event Sourcing",
"Fc",
"Propulsion"
],
"tags": {
"language": "F#"
},
"identity": "Equinox.Fc",
"name": "Equinox Fc Example",
"shortName": "fcInventory",
"sourceName": "Fc",
"preferNameDirectory": true
}
10 changes: 10 additions & 0 deletions equinox-fc/Domain.Tests/AllocationTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module AllocationTests

open Allocation
open FsCheck.Xunit
open Swensen.Unquote

let [<Property>] ``codec can roundtrip`` event =
let ee = Events.codec.Encode(None, event)
let ie = FsCodec.Core.TimelineEvent.Create(0L, ee.EventType, ee.Data)
test <@ Some event = Events.codec.TryDecode ie @>
59 changes: 59 additions & 0 deletions equinox-fc/Domain.Tests/AllocatorTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module AllocatorTests

open Allocator
open FsCheck.Xunit
open Swensen.Unquote
open System

type Command =
| Commence of AllocationId * DateTimeOffset
| Complete of AllocationId * Events.Reason

type Result =
| Accepted
| Conflict of AllocationId

let execute cmd state =
match cmd with
| Commence (a, c) ->
match decideCommence a c state with
| CommenceResult.Accepted, es -> Accepted, es
| CommenceResult.Conflict a, es -> Conflict a, es
| Complete (a, r) -> let es = decideComplete a r state in Accepted, es

let [<Property>] properties c1 c2 =
let res, events = execute c1 Fold.initial
let state1 = Fold.fold Fold.initial events
match c1, res, events, state1 with
| Commence (a, c), Accepted, [Events.Commenced ({ allocationId = ea; cutoff = ec } as e)], state ->
test <@ a = ea && c = ec && state = Some e @>
| Complete _, Accepted, [], None ->
() // Non-applicable Complete requests are simply ignored
| _, res, l, _ ->
test <@ List.isEmpty l && res = Accepted @>

let res, events = execute c2 state1
let state2 = Fold.fold state1 events
match state1, c2, res, events, state2 with
// As per above, normal commence
| None, Commence (a, c), Accepted, [Events.Commenced ({ allocationId = ea; cutoff = ec } as e)], state ->
test <@ a = ea && c = ec && state = Some e @>
// Idempotent accept if same allocationId
| Some active as s1, Commence (a, _), Accepted, [], s2 ->
test <@ s1 = s2 && active.allocationId = a @>
// Conflict reports owner allocator
| Some active as s1, Commence (a2, _), Conflict a1, [], s2 ->
test <@ s1 = s2 && a2 <> a1 && a1 = active.allocationId @>
// Correct complete for same allocator is accepted
| Some active, Complete (a, r), Accepted, [Events.Completed { allocationId = ea; reason = er }], None ->
test <@ er = r && ea = a && active.allocationId = a @>
// Completes not for the same allocator are ignored
| Some active as s1, Complete (a, _), Accepted, [], s2 ->
test <@ active.allocationId <> a && s2 = s1 @>
| _, _, res, l, _ ->
test <@ List.isEmpty l && res = Accepted @>

let [<Property>] ``codec can roundtrip`` event =
let ee = Events.codec.Encode(None, event)
let ie = FsCodec.Core.TimelineEvent.Create(0L, ee.EventType, ee.Data)
test <@ Some event = Events.codec.TryDecode ie @>
35 changes: 35 additions & 0 deletions equinox-fc/Domain.Tests/Domain.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<WarningLevel>5</WarningLevel>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<Compile Include="Infrastructure.fs" />
<Compile Include="LocationSeriesTests.fs" />
<Compile Include="LocationEpochTests.fs" />
<Compile Include="LocationTests.fs" />
<Compile Include="TicketTests.fs" />
<Compile Include="TicketListTests.fs" />
<Compile Include="AllocatorTests.fs" />
<Compile Include="AllocationTests.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Destructurama.FSharp" Version="1.1.1-dev-00033" />
<PackageReference Include="Equinox.MemoryStore" Version="2.0.0" />
<PackageReference Include="FsCheck.xUnit" Version="2.14.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="unquote" Version="4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Domain\Domain.fsproj" />
</ItemGroup>

</Project>
51 changes: 51 additions & 0 deletions equinox-fc/Domain.Tests/Infrastructure.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[<AutoOpen>]
module Fc.Infrastructure

open Serilog
open System

let (|Id|) (x : Guid) = x.ToString "N" |> FSharp.UMX.UMX.tag
let inline mkId () = Guid.NewGuid() |> (|Id|)
let (|Ids|) (xs : Guid[]) = xs |> Array.map (|Id|)
let (|IdsAtLeastOne|) (Id x, Ids xs) = Seq.append xs (Seq.singleton x) |> Seq.toArray

module EnvVar =

let tryGet k = Environment.GetEnvironmentVariable k |> Option.ofObj

module Cosmos =

open Equinox.Cosmos
let connect () =
match EnvVar.tryGet "EQUINOX_COSMOS_CONNECTION", EnvVar.tryGet "EQUINOX_COSMOS_DATABASE", EnvVar.tryGet "EQUINOX_COSMOS_CONTAINER" with
| Some s, Some d, Some c ->
let appName = "Domain.Tests"
let discovery = Discovery.FromConnectionString s
let connector = Connector(TimeSpan.FromSeconds 5., 5, TimeSpan.FromSeconds 5., Serilog.Log.Logger)
let connection = connector.Connect(appName, discovery) |> Async.RunSynchronously
let context = Context(connection, d, c)
let cache = Equinox.Cache (appName, 10)
context, cache
| s, d, c ->
failwithf "Connection, Database and Container EQUINOX_COSMOS_* Environment variables are required (%b,%b,%b)"
(Option.isSome s) (Option.isSome d) (Option.isSome c)

/// Adapts the XUnit ITestOutputHelper to be a Serilog Sink
type TestOutputAdapter(testOutput : Xunit.Abstractions.ITestOutputHelper) =
let template = "{Timestamp:HH:mm:ss.fff zzz} [{Level:u3}] {Message} {Properties}{NewLine}{Exception}"
let formatter = Serilog.Formatting.Display.MessageTemplateTextFormatter(template, null);
let writeSerilogEvent logEvent =
use writer = new System.IO.StringWriter()
formatter.Format(logEvent, writer)
let messageLine = string writer
testOutput.WriteLine messageLine
System.Diagnostics.Debug.Write messageLine
interface Serilog.Core.ILogEventSink with member __.Emit logEvent = writeSerilogEvent logEvent

/// Creates a Serilog Log chain emitting to the cited Sink (only)
let createLogger sink =
Serilog.LoggerConfiguration()
// .MinimumLevel.Debug()
.Destructure.FSharpTypes()
.WriteTo.Sink(sink)
.CreateLogger()
57 changes: 57 additions & 0 deletions equinox-fc/Domain.Tests/LocationEpochTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module Fc.LocationEpochTests

open FsCheck.Xunit
open Location.Epoch
open Swensen.Unquote

let interpret transactionId delta _balance =
match delta with
| 0 -> (), []
| delta when delta < 0 -> (), [Events.Removed {| delta = -delta; id = transactionId |}]
| delta -> (), [Events.Added {| delta = delta; id = transactionId |}]

let validateAndInterpret transactionId expectedBalance delta balance =
test <@ expectedBalance = balance @>
interpret transactionId delta balance

let verifyDeltaEvent transactionId delta events =
let dEvents = events |> List.filter (function Events.Added _ | Events.Removed _ -> true | _ -> false)
test <@ interpret transactionId delta (Unchecked.defaultof<_>) = ((), dEvents) @>

let [<Property>] properties transactionId carriedForward delta1 closeImmediately delta2 close =

(* Starting with an empty stream, we'll need to supply the balance carried forward, optionally we apply a delta and potentially close *)

let initialShouldClose _state = closeImmediately
let res, events =
sync (Some carriedForward) (validateAndInterpret transactionId carriedForward delta1) initialShouldClose Fold.initial
let cfEvents events = events |> List.filter (function Events.CarriedForward _ -> true | _ -> false)
let closeEvents events = events |> List.filter (function Events.Closed -> true | _ -> false)
let state1 = Fold.fold Fold.initial events
let expectedBalance = carriedForward.initial + delta1
// Only expect closing if it was requested
let expectImmediateClose = closeImmediately
test <@ Option.isSome res.result
&& expectedBalance = res.balance @>
test <@ [Events.CarriedForward { initial = carriedForward }] = cfEvents events
&& (not expectImmediateClose || 1 = Seq.length (closeEvents events)) @>
verifyDeltaEvent transactionId delta1 events

(* After initializing, validate we don't need to supply a carriedForward, and don't produce a CarriedForward event *)

let shouldClose _state = close
let { isOpen = isOpen; result = worked; balance = bal }, events =
sync None (validateAndInterpret transactionId expectedBalance delta2) shouldClose state1
let expectedBalance = if expectImmediateClose then expectedBalance else expectedBalance + delta2
test <@ [] = cfEvents events
&& (expectImmediateClose || not close || 1 = Seq.length (closeEvents events)) @>
test <@ (expectImmediateClose || close || isOpen)
&& expectedBalance = bal @>
if not expectImmediateClose then
test <@ Option.isSome worked @>
verifyDeltaEvent transactionId delta2 events

let [<Property>] ``codec can roundtrip`` event =
let ee = Events.codec.Encode(None, event)
let ie = FsCodec.Core.TimelineEvent.Create(0L, ee.EventType, ee.Data)
test <@ Some event = Events.codec.TryDecode ie @>
44 changes: 44 additions & 0 deletions equinox-fc/Domain.Tests/LocationSeriesTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Fc.LocationSeriesTests

open FsCheck.Xunit
open FSharp.UMX
open Swensen.Unquote
open Location.Series

let [<Property>] properties c1 c2 =
let events = interpretAdvanceIngestionEpoch c1 Fold.initial
let state1 = Fold.fold Fold.initial events
let epoch0 = %0
match c1, events, state1 with
// Started events are not written for < 0
| n, [], activeEpoch when n < epoch0 ->
test <@ None = activeEpoch @>
// Any >=0 value should trigger a Started event, initially
| n, [Events.Started { epoch = ee }], Some activatedEpoch ->
test <@ n >= epoch0 && n = ee && n = activatedEpoch @>
// Nothing else should yield events
| _, l, _ ->
test <@ List.isEmpty l @>

let events = interpretAdvanceIngestionEpoch c2 state1
let state2 = Fold.fold state1 events
match state1, c2, events, state2 with
// Started events are not written for < 0
| None, n, [], activeEpoch when n < epoch0 ->
test <@ None = activeEpoch @>
// Any >= 0 epochId should trigger a Started event if first command didnt do anything
| None, n, [Events.Started { epoch = ee }], Some activatedEpoch ->
let eEpoch = %ee
test <@ n >= epoch0 && n = eEpoch && n = activatedEpoch @>
// Any higher epochId should trigger a Started event (gaps are fine - we are only tying to reduce walks)
| Some s1, n, [Events.Started { epoch = ee }], Some activatedEpoch ->
let eEpoch = %ee
test <@ n > s1 && n = eEpoch && n > epoch0 && n = activatedEpoch @>
// Nothing else should yield events
| _, _, l, _ ->
test <@ List.isEmpty l @>

let [<Property>] ``codec can roundtrip`` event =
let ee = Events.codec.Encode(None, event)
let ie = FsCodec.Core.TimelineEvent.Create(0L, ee.EventType, ee.Data)
test <@ Some event = Events.codec.TryDecode ie @>
Loading