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: Location with Long Running Inventory #40

Open
wants to merge 51 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
6abe2d5
Add Location initial impl
bartelink Nov 29, 2019
f203680
Add Fc template wrapping
bartelink Nov 29, 2019
7a3690e
Correct AccessStrategy
bartelink Nov 29, 2019
2a7f6ba
Fix streamId ordering
bartelink Nov 29, 2019
5828bb8
Tidy log output
bartelink Nov 29, 2019
6aceccc
Tidy connection logic
bartelink Nov 29, 2019
5bd3ccf
Add README
bartelink Nov 29, 2019
2600c20
Move AggregateId to Events
bartelink Dec 7, 2019
7c7db7d
Aggregate layout/naming consistency
bartelink Dec 7, 2019
d8dfa0e
Sync layout
bartelink Jan 17, 2020
4f599a3
wip
bartelink Feb 12, 2020
ce36a09
wip
bartelink Feb 12, 2020
c7ef3d5
Complete Location, Inventory and LocationTests
bartelink Feb 13, 2020
20605ee
Yes, comments
bartelink Feb 13, 2020
ff7b940
tmp
bartelink Feb 13, 2020
4b6781d
WIP code snipped
bartelink Feb 13, 2020
1c47ee1
Add missing README.md
bartelink Feb 13, 2020
9229fde
Tidy accessStrategy
bartelink Feb 14, 2020
0895776
Formatting
bartelink Feb 14, 2020
2daf7f0
InventoryTransaction wip
bartelink Feb 14, 2020
e6b3e86
Handle FsCodec _ restriction
bartelink Feb 14, 2020
ca1d957
Remove inventoryId from Trans SN
bartelink Feb 15, 2020
40d5b2b
Remove checkpoints from InventorySeries
bartelink Feb 17, 2020
21f50c9
Tidying / formatting
bartelink Feb 17, 2020
eba71f9
Fix batch vs epoch naming in inventory
bartelink Feb 17, 2020
d05eab2
Complete Process Manager Apply
bartelink Feb 18, 2020
e3582b9
Target V2s
bartelink Feb 19, 2020
2c34ca2
wip
bartelink Feb 19, 2020
ca7f60c
Process Manager wip
bartelink Feb 21, 2020
577328b
Minor renames; tests not yet compiling
bartelink Feb 21, 2020
bd6ba8e
WIP
bartelink Feb 23, 2020
2ae86f2
Cover denial of Remove action
bartelink Feb 25, 2020
49ffb38
Add explicit Service action methods on Process Manager
bartelink Feb 26, 2020
5dd465e
Add Watchdog
bartelink Feb 26, 2020
d91dc51
Add Watchdog to Fc sln
bartelink Feb 26, 2020
947eb5d
Tidy wiring
bartelink Feb 27, 2020
8d0e454
Apply style changes from 4.2
bartelink Mar 10, 2020
099cdf7
Stragglers
bartelink Mar 10, 2020
e64fe37
wip
bartelink Apr 12, 2020
cbd6562
Fix typo
bartelink Apr 12, 2020
755d18f
Repurpose Program.fs as skeleton host
bartelink Apr 13, 2020
f0b6e2a
Add ES wiring
bartelink Apr 16, 2020
941032f
Casing
bartelink Jun 8, 2020
3a2dfdc
Cleanup
bartelink Jun 11, 2020
1985df2
Fix changelog
bartelink Jun 11, 2020
c2adeb0
Cleanup more
bartelink Jun 11, 2020
ff265c5
Clean PM naming
bartelink Jun 11, 2020
9d49d38
Extract ProcessManager as Stock*
bartelink Jun 12, 2020
e6d514f
Tidy
bartelink Jun 12, 2020
0314a16
Cleanup from Cosmos
bartelink Jun 12, 2020
d3d6338
Whitespace
bartelink Jun 12, 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 @@ -11,6 +11,7 @@ The `Unreleased` section name is replaced by the expected version of next releas
### Added

- `eqxShipping`: Unit and integration tests [#70](https://github.com/jet/dotnet-templates/pull/70)
- `eqxFc`: Fulfilment-Center inspired example utilizing Process Manager patterns with basic `Equinox.MemoryStore` and `Equinox.EventStore` tests [#40](https://github.com/jet/dotnet-templates/pulls/40)

### Changed
### Removed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ These templates focus solely on Consistent Processing using Equinox Stores:
- [`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.
<a name="eqxFc"></a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this HTML anchor tag a side effect from an IDE or something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, just so I can link to a section within the readme, i.e. github.com/jet/equinox#eqxfc - changelog.md uses it also to provide an alias thats better than default generated ones

- [`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
42 changes: 42 additions & 0 deletions dotnet-templates.sln
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain.Tests", "equinox-shi
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Watchdog.Integration", "equinox-shipping\Watchdog.Integration\Watchdog.Integration.fsproj", "{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eqxFc", "eqxFc", "{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
equinox-fc\Fc.sln = equinox-fc\Fc.sln
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}") = "Web", "equinox-fc\Web\Web.fsproj", "{A6AA8FAA-0D2C-423D-B166-F4B3AF10C442}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -157,6 +170,18 @@ Global
{9AFF6138-B63B-4EBF-B86B-4F626E1F1ADF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AFF6138-B63B-4EBF-B86B-4F626E1F1ADF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AFF6138-B63B-4EBF-B86B-4F626E1F1ADF}.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
{A6AA8FAA-0D2C-423D-B166-F4B3AF10C442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6AA8FAA-0D2C-423D-B166-F4B3AF10C442}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6AA8FAA-0D2C-423D-B166-F4B3AF10C442}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6AA8FAA-0D2C-423D-B166-F4B3AF10C442}.Release|Any CPU.Build.0 = Release|Any CPU
{26BFE6BC-5887-4E40-8CFD-F15332F5A104}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26BFE6BC-5887-4E40-8CFD-F15332F5A104}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26BFE6BC-5887-4E40-8CFD-F15332F5A104}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -169,6 +194,18 @@ Global
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}.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
{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
{6CB10946-AC56-4DE7-AB65-F6B13B86C703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CB10946-AC56-4DE7-AB65-F6B13B86C703}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CB10946-AC56-4DE7-AB65-F6B13B86C703}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CB10946-AC56-4DE7-AB65-F6B13B86C703}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F66A5BFE-7C81-44DC-97DE-3FD8C83B8F06} = {B72FFAAE-7801-41B2-86F5-FD90E97A30F7}
Expand All @@ -182,9 +219,14 @@ 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}
{7B96FCF8-0BB5-4494-A143-628882A6E50A} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{9AFF6138-B63B-4EBF-B86B-4F626E1F1ADF} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{6CB10946-AC56-4DE7-AB65-F6B13B86C703} = {4946576F-1558-49ED-A272-6F4D92FB0031}
{5A45EF21-576B-4B40-86BD-F5960ECD66BF} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{46B8B7C9-3334-4C13-A339-57571C14F2B9} = {4946576F-1558-49ED-A272-6F4D92FB0031}
{A6AA8FAA-0D2C-423D-B166-F4B3AF10C442} = {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": "eqxFc",
"sourceName": "Fc",
"preferNameDirectory": true
}
32 changes: 32 additions & 0 deletions equinox-fc/Domain.Tests/Domain.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<Compile Include="Fixtures.fs" />
<Compile Include="LocationSeriesTests.fs" />
<Compile Include="LocationEpochTests.fs" />
<Compile Include="LocationTests.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />

<PackageReference Include="Destructurama.FSharp" Version="1.1.1-dev-00033" />
<PackageReference Include="Equinox.MemoryStore" Version="2.1.0" />
<PackageReference Include="FsCheck.xUnit" Version="2.14.2" />
<PackageReference Include="unquote" Version="5.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>
53 changes: 53 additions & 0 deletions equinox-fc/Domain.Tests/Fixtures.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[<AutoOpen>]
module Fc.Domain.Tests.Fixtures

open Serilog
open System

module EnvVar =

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

module EventStore =

open Equinox.EventStore
let connect () =
match EnvVar.tryGet "EQUINOX_ES_HOST", EnvVar.tryGet "EQUINOX_ES_USERNAME", EnvVar.tryGet "EQUINOX_ES_PASSWORD" with
| Some h, Some u, Some p ->
let appName = "Domain.Tests"
let discovery = Discovery.GossipDns h
let connector = Connector(u, p, TimeSpan.FromSeconds 5., 5, Logger.SerilogNormal Serilog.Log.Logger)
let connection = connector.Establish(appName, discovery, ConnectionStrategy.ClusterSingle NodePreference.Master) |> Async.RunSynchronously
let context = Context(connection, BatchingPolicy(500))
let cache = Equinox.Cache (appName, 10)
context, cache
| h, u, p ->
failwithf "Host, Username and Password EQUINOX_ES_* Environment variables are required (%b,%b,%b)"
(Option.isSome h) (Option.isSome u) (Option.isSome p)

module TestOutputLogger =

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

let create output =
let logger = TestOutputAdapter output
LoggerConfiguration().Destructure.FSharpTypes().WriteTo.Sink(logger).CreateLogger()

(* Generic FsCheck helpers *)

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|) (Ids xs, Id x) = [| yield x; yield! xs |]
let (|AtLeastOne|) (x, xs) = x::xs

55 changes: 55 additions & 0 deletions equinox-fc/Domain.Tests/LocationEpochTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module Fc.Domain.Tests.LocationEpochTests

open FsCheck.Xunit
open Swensen.Unquote

open Fc.Domain.Location.Epoch

let decide 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 verifyDeltaEvent transactionId delta events =
let dEvents = events |> List.filter (function Events.Added _ | Events.Removed _ -> true | _ -> false)
test <@ decide 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) (decide transactionId delta1) initialShouldClose Fold.initial
let cfEvents events = events |> List.choose (function Events.CarriedForward e -> Some e | _ -> None)
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
let (Fold.Current bal) = res.history
test <@ Option.isSome res.result
&& expectedBalance = bal @>
test <@ carriedForward = List.head (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; history = (Fold.Current bal) }, events =
sync None (decide transactionId 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 @>
45 changes: 45 additions & 0 deletions equinox-fc/Domain.Tests/LocationSeriesTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Fc.Domain.Tests.LocationSeriesTests

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

open Fc.Domain.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 @>
75 changes: 75 additions & 0 deletions equinox-fc/Domain.Tests/LocationTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
module Fc.Domain.Tests.LocationTests

open FsCheck.Xunit
open FSharp.UMX
open Swensen.Unquote
open System

open Fc.Domain
open Fc.Domain.Location

/// Helpers to match `module Cosmos/EventStore` wrapping inside the impl
module Location =

module MemoryStore =

open Equinox.MemoryStore

module Series =

let resolve store = Resolver(store, Series.Events.codec, Series.Fold.fold, Series.Fold.initial).Resolve

module Epoch =

let resolve store = Resolver(store, Epoch.Events.codec, Epoch.Fold.fold, Epoch.Fold.initial).Resolve

let create (zeroBalance, toBalanceCarriedForward, shouldClose) store =
let maxAttempts = Int32.MaxValue
let series = Series.create (fun (id, _opt) -> Series.resolve store id) maxAttempts
let epochs = Epoch.create (Epoch.resolve store) maxAttempts
create (zeroBalance, toBalanceCarriedForward, shouldClose) (series, epochs)

let run (service : Service) (IdsAtLeastOne locations, deltas : _[], transactionId) = Async.RunSynchronously <| async {
let runId = mkId () // Need to make making state in store unique when replaying or shrinking
let locations = locations |> Array.map (fun x -> % (sprintf "%O/%O" x runId))

let updates = deltas |> Seq.mapi (fun i x -> locations.[i % locations.Length], x) |> Seq.cache

(* Apply random deltas *)

let adjust delta (state : Epoch.Fold.State) =
let (Epoch.Fold.Balance bal) = state
let value = max -bal delta
if value = 0 then 0, []
elif value < 0 then value, [Epoch.Events.Removed {| delta = -value; id = transactionId |}]
else value, [Epoch.Events.Added {| delta = value; id = transactionId |}]
let! appliedDeltas = seq { for loc, x in updates -> async { let! eff = service.Execute(loc, adjust x) in return loc,eff } } |> Async.Parallel
let expectedBalances = Seq.append (seq { for l in locations -> l, 0}) appliedDeltas |> Seq.groupBy fst |> Seq.map (fun (l, xs) -> l, xs |> Seq.sumBy snd) |> Set.ofSeq

(* Verify loading yields identical state *)

let! balances = seq { for loc in locations -> async { let! bal = service.Execute(loc,(fun (Epoch.Fold.Balance bal) -> bal, [])) in return loc,bal } } |> Async.Parallel
test <@ expectedBalances = Set.ofSeq balances @> }

let [<Property>] ``MemoryStore properties`` epochLen args =
let store = Equinox.MemoryStore.VolatileStore()

let epochLen, idsWindow = max 1 epochLen, 5
let zero, cf, sc = Epoch.zeroBalance, Epoch.toBalanceCarriedForward idsWindow, Epoch.shouldClose epochLen

let service = Location.MemoryStore.create (zero, cf, sc) store
run service args

type EventStore(testOutput) =

let log = TestOutputLogger.create testOutput
do Serilog.Log.Logger <- log

let context, cache = EventStore.connect ()

let [<Property(MaxTest=5, MaxFail=1)>] properties epochLen args =
let epochLen, idsWindow = max 1 epochLen, 5
let zero, cf, sc = Epoch.zeroBalance, Epoch.toBalanceCarriedForward idsWindow, Epoch.shouldClose epochLen

let service = Location.EventStore.create (zero, cf, sc) (context, cache, 50)
run service args
24 changes: 24 additions & 0 deletions equinox-fc/Domain/Domain.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<Compile Include="Infrastructure.fs" />
<Compile Include="LocationSeries.fs" />
<Compile Include="LocationEpoch.fs" />
<Compile Include="Location.fs" />
<Compile Include="InventoryEpoch.fs" />
<Compile Include="Inventory.fs" />
<Compile Include="StockTransaction.fs" />
<Compile Include="StockProcessManager.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Equinox.EventStore" Version="2.1.0" />
<PackageReference Include="FsCodec.NewtonsoftJson" Version="2.1.0" />
</ItemGroup>

</Project>
Loading