Skip to content

Commit

Permalink
Hotel Domain with Reactor (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink authored Jan 23, 2023
1 parent 64b9a51 commit e0ca945
Show file tree
Hide file tree
Showing 34 changed files with 1,818 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]

### Added

- `proHotel`: Process Manager based Reactor with Unit and Integration tests wired for MemoryStore, DynamoStore and MessageDb [#127](https:/jet/dotnet-templates/pull/127)

### Changed

- Target `Propulsion` v `3.0.0-rc.2` [#129](https:/jet/dotnet-templates/pull/129)
Expand Down
34 changes: 34 additions & 0 deletions dotnet-templates.sln
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,20 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Watchdog.Lambda", "equinox-
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Watchdog.Lambda.Cdk", "equinox-shipping\Watchdog.Lambda.Cdk\Watchdog.Lambda.Cdk.fsproj", "{C9D85FFB-847F-407D-A76B-38C44A01D8EC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "proHotel", "proHotel", "{C76DEE36-C648-4351-9BB6-F4470D72D473}"
ProjectSection(SolutionItems) = preProject
propulsion-hotel\README.md = propulsion-hotel\README.md
propulsion-hotel\.template.config\template.json = propulsion-hotel\.template.config\template.json
EndProjectSection
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain", "propulsion-hotel\Domain\Domain.fsproj", "{E195C57D-8B64-4B71-88AC-AAACF4F7F5A5}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain.Tests", "propulsion-hotel\Domain.Tests\Domain.Tests.fsproj", "{DE76D4BF-619A-4553-A113-6F8D83CE63D6}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Reactor", "propulsion-hotel\Reactor\Reactor.fsproj", "{0F4E4FF4-0471-4D1E-B80D-F6AB8EA87DD5}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Reactor.Integration", "propulsion-hotel\Reactor.Integration\Reactor.Integration.fsproj", "{36D15020-4ED7-49FE-B0C3-FC89C45655D2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -293,6 +307,22 @@ Global
{C9D85FFB-847F-407D-A76B-38C44A01D8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9D85FFB-847F-407D-A76B-38C44A01D8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9D85FFB-847F-407D-A76B-38C44A01D8EC}.Release|Any CPU.Build.0 = Release|Any CPU
{E195C57D-8B64-4B71-88AC-AAACF4F7F5A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E195C57D-8B64-4B71-88AC-AAACF4F7F5A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E195C57D-8B64-4B71-88AC-AAACF4F7F5A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E195C57D-8B64-4B71-88AC-AAACF4F7F5A5}.Release|Any CPU.Build.0 = Release|Any CPU
{DE76D4BF-619A-4553-A113-6F8D83CE63D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE76D4BF-619A-4553-A113-6F8D83CE63D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE76D4BF-619A-4553-A113-6F8D83CE63D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE76D4BF-619A-4553-A113-6F8D83CE63D6}.Release|Any CPU.Build.0 = Release|Any CPU
{0F4E4FF4-0471-4D1E-B80D-F6AB8EA87DD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F4E4FF4-0471-4D1E-B80D-F6AB8EA87DD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F4E4FF4-0471-4D1E-B80D-F6AB8EA87DD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F4E4FF4-0471-4D1E-B80D-F6AB8EA87DD5}.Release|Any CPU.Build.0 = Release|Any CPU
{36D15020-4ED7-49FE-B0C3-FC89C45655D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36D15020-4ED7-49FE-B0C3-FC89C45655D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36D15020-4ED7-49FE-B0C3-FC89C45655D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36D15020-4ED7-49FE-B0C3-FC89C45655D2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -326,6 +356,10 @@ Global
{B3B0C203-2648-432F-B49B-F9F8E1A4B4F1} = {A3A3AA9F-E039-4D1A-BA64-A8291A239861}
{2D673A49-19A2-41E9-B32E-E73FE05A8457} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{C9D85FFB-847F-407D-A76B-38C44A01D8EC} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{E195C57D-8B64-4B71-88AC-AAACF4F7F5A5} = {C76DEE36-C648-4351-9BB6-F4470D72D473}
{DE76D4BF-619A-4553-A113-6F8D83CE63D6} = {C76DEE36-C648-4351-9BB6-F4470D72D473}
{0F4E4FF4-0471-4D1E-B80D-F6AB8EA87DD5} = {C76DEE36-C648-4351-9BB6-F4470D72D473}
{36D15020-4ED7-49FE-B0C3-FC89C45655D2} = {C76DEE36-C648-4351-9BB6-F4470D72D473}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D8B64643-4049-466D-BAFC-0437B8C1E508}
Expand Down
2 changes: 2 additions & 0 deletions equinox-shipping/Domain/Domain.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<TargetFramework>net6.0</TargetFramework>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- TODO remove, only required if you have a very old SDK -->
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
</PropertyGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions equinox-shipping/Domain/FinalizationTransaction.fs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ module Flow =

type Service internal (resolve : TransactionId -> Equinox.Decider<Events.Event, Fold.State>) =

/// (Optionally) idempotently applies an event representing progress achieved in some aspect of the workflow
/// Yields a `Flow.Action` representing the next activity to be performed as implied by the workflow's State afterwards
/// The workflow concludes when the action returned is `Action.Completed`
member _.Step(transactionId, maybeUpdate) : Async<Flow.Action> =
let decider = resolve transactionId
decider.Transact(Flow.decide maybeUpdate, Flow.nextAction)
Expand Down
4 changes: 1 addition & 3 deletions equinox-shipping/Watchdog.Integration/Generators.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ open FSharp.UMX

type GuidStringN<[<Measure>]'m> = GuidStringN of string<'m> with static member op_Explicit(GuidStringN x) = x

let (|Ids|) xs = Array.map (function GuidStringN x -> x) xs
let (|IdsAtLeastOne|) (x, xs) = Array.append (Array.singleton x) (Array.map (function GuidStringN x -> x) xs)

let genDefault<'t> = ArbMap.defaults |> ArbMap.generate<'t>

type Custom =

static member GuidStringN() = genDefault |> Gen.map (Shipping.Domain.Guid.toStringN >> GuidStringN) |> Arb.fromGen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ let run (log: Serilog.ILogger) (processManager : Shipping.Domain.FinalizationPro

let counts = System.Collections.Generic.Stack()
let mutable timeouts = 0
for GuidStringN tid, GuidStringN cid, IdsAtLeastOne shipmentIds in batches do
for GuidStringN tid, GuidStringN cid, NonEmptyArray shipmentIds in batches do
counts.Push shipmentIds.Length
try let! _ = processManager.TryFinalizeContainer(tid, cid, shipmentIds)
|> Async.timeoutAfter runTimeout
Expand Down
2 changes: 1 addition & 1 deletion equinox-shipping/Watchdog/Handler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type Stats(log, statsInterval, stateInterval, verboseStore, ?logExternalStats) =
if completed <> 0 || deferred <> 0 || failed <> 0 || succeeded <> 0 then
log.Information(" Completed {completed} Deferred {deferred} Failed {failed} Succeeded {succeeded}", completed, deferred, failed, succeeded)
completed <- 0; deferred <- 0; failed <- 0; succeeded <- 0
match logExternalStats with None -> () | Some f -> f Serilog.Log.Logger
match logExternalStats with None -> () | Some f -> let logWithoutContext = Serilog.Log.Logger in f logWithoutContext

override _.Classify(exn) =
match exn with
Expand Down
28 changes: 28 additions & 0 deletions propulsion-hotel/.template.config/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "@bartelink",
"classifications": [
"Equinox",
"Propulsion",
"Event Sourcing",
"Process Manager"
],
"tags": {
"language": "F#",
"type": "solution"
},
"identity": "Propulsion.Hotel",
"name": "Propulsion Hotel Sample",
"shortName": "proHotel",
"sourceName": "Hotel",
"preferNameDirectory": true,
"symbols": {
"skipIntegrationTests": {
"type": "parameter",
"datatype": "bool",
"isRequired": false,
"defaultValue": "false",
"description": "Add Skip attribute to integration tests"
}
}
}
13 changes: 13 additions & 0 deletions propulsion-hotel/Domain.Tests/Arbitraries.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[<Microsoft.FSharp.Core.AutoOpen>]
module Domain.Tests.Arbitraries

open Domain
open FsCheck.FSharp

/// For unit tests, we only ever use the Domain Services wired to a MemoryStore, so we default Store to that
type Generators =

static member MemoryStore = Gen.constant (Config.Store.Memory <| Equinox.MemoryStore.VolatileStore())
static member Store = Arb.fromGen Generators.MemoryStore

[<assembly: FsCheck.Xunit.Properties(Arbitrary = [| typeof<Generators> |])>] do ()
26 changes: 26 additions & 0 deletions propulsion-hotel/Domain.Tests/Domain.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<WarningLevel>5</WarningLevel>
</PropertyGroup>

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

<PackageReference Include="FsCheck.Xunit" Version="3.0.0-beta2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
</ItemGroup>

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

<ItemGroup>
<Compile Include="Arbitraries.fs" />
<Compile Include="GroupCheckoutFlow.fs" />
</ItemGroup>

</Project>
44 changes: 44 additions & 0 deletions propulsion-hotel/Domain.Tests/GroupCheckoutFlow.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Domain.Tests.GroupCheckoutFlow

open Domain
open FsCheck
open FsCheck.Xunit
open Reactor
open Swensen.Unquote

[<Property>]
let ``Happy path including Reaction`` (store, groupCheckoutId, paymentId, stays : _ []) = async {
let staysService = GuestStay.Config.create store
let sut = GroupCheckout.Config.create store
let processor = GroupCheckoutProcess.Service(staysService, sut, 2)
let mutable charged = 0
for stayId, chargeId, PositiveInt amount in stays do
charged <- charged + amount
do! staysService.Charge(stayId, chargeId, amount)
let stays = [| for stayId, _, _ in stays -> stayId |]

match! sut.Merge(groupCheckoutId, stays) with
// If no stays were added to the group checkout, no checkouts actions should be pending
| GroupCheckout.Flow.Ready 0m ->
// We'll run the Reactor, but only to confirm it is a no-op
[||] =! stays
match! processor.React(groupCheckoutId) with
| GroupCheckoutProcess.Outcome.Noop, _ -> ()
| GroupCheckoutProcess.Outcome.Merged _, _ -> failwith "Should be noop"
// If any stays have been added, they should be recorded, and work should be triggered
| GroupCheckout.Flow.MergeStays staysToDo ->
staysToDo =! stays
match! processor.React(groupCheckoutId) with
| GroupCheckoutProcess.Outcome.Merged (ok, fail), _ -> test <@ ok = stays.Length && 0 = fail @>
| GroupCheckoutProcess.Outcome.Noop, _ -> failwith "Should not be noop"
// We should not end up in any other states
| GroupCheckout.Flow.Ready _
| GroupCheckout.Flow.Finished -> failwith "unexpected"

do! sut.Pay(groupCheckoutId, paymentId, charged)

let! _ = sut.Confirm(groupCheckoutId)

let! next = sut.Read(groupCheckoutId)
test <@ GroupCheckout.Flow.Finished = next @>
}
49 changes: 49 additions & 0 deletions propulsion-hotel/Domain/Config.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module Domain.Config

[<RequireQualifiedAccess; NoComparison; NoEquality>]
type Store =
| Memory of Equinox.MemoryStore.VolatileStore<struct (int * System.ReadOnlyMemory<byte>)>
| Dynamo of Equinox.DynamoStore.DynamoStoreContext * Equinox.Core.ICache
| Mdb of Equinox.MessageDb.MessageDbContext * Equinox.Core.ICache

let log = Serilog.Log.ForContext("isMetric", true)
let resolve cat = Equinox.Decider.resolve log cat

module EventCodec =

open FsCodec.SystemTextJson

let private defaultOptions = Options.Create()
let gen<'t when 't :> TypeShape.UnionContract.IUnionContract> =
Codec.Create<'t>(options = defaultOptions)

module Memory =

let create codec initial fold store : Equinox.Category<_, _, _> =
Equinox.MemoryStore.MemoryStoreCategory(store, FsCodec.Deflate.EncodeUncompressed codec, fold, initial)

let defaultCacheDuration = System.TimeSpan.FromMinutes 20.

module Dynamo =

open Equinox.DynamoStore

let private create codec initial fold accessStrategy (context, cache) =
let cacheStrategy = CachingStrategy.SlidingWindow (cache, defaultCacheDuration)
DynamoStoreCategory(context, FsCodec.Deflate.EncodeUncompressed codec, fold, initial, cacheStrategy, accessStrategy)

let createUnoptimized codec initial fold (context, cache) =
let accessStrategy = AccessStrategy.Unoptimized
create codec initial fold accessStrategy (context, cache)

module Mdb =

open Equinox.MessageDb

let private create codec initial fold accessStrategy (context, cache) =
let cacheStrategy = CachingStrategy.SlidingWindow (cache, defaultCacheDuration)
MessageDbCategory(context, codec, fold, initial, cacheStrategy, ?access = accessStrategy)

let createUnoptimized codec initial fold (context, cache) =
let accessStrategy = None
create codec initial fold accessStrategy (context, cache)
25 changes: 25 additions & 0 deletions propulsion-hotel/Domain/Domain.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<WarningLevel>5</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- TODO remove, only required if you have a very old SDK -->
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Equinox.DynamoStore" Version="4.0.0-rc.7" />
<PackageReference Include="Equinox.MemoryStore" Version="4.0.0-rc.7" />
<PackageReference Include="Equinox.MessageDb" Version="4.0.0-rc.7" />
<PackageReference Include="FsCodec.SystemTextJson" Version="3.0.0-rc.9" />
</ItemGroup>

<ItemGroup>
<Compile Include="Config.fs" />
<Compile Include="Types.fs" />
<Compile Include="GuestStay.fs" />
<Compile Include="GroupCheckout.fs" />
</ItemGroup>

</Project>
Loading

0 comments on commit e0ca945

Please sign in to comment.