Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
20c6bfd
WIP
bartelink Nov 21, 2021
fc6b79b
Add API
bartelink Nov 21, 2021
c1eec08
Add Read logic
bartelink Nov 21, 2021
418eb20
Add Esdb wiring to Domain
bartelink Nov 21, 2021
7ff20cf
Add Cosmos wiring for API startup
bartelink Nov 21, 2021
b7afa42
Add Esdb wiring for API
bartelink Nov 21, 2021
34b1581
Tidy pricing interface
bartelink Nov 25, 2021
818be10
Remove PricedProductItem types
bartelink Dec 18, 2021
0932a7a
Fix errors
bartelink Dec 18, 2021
338625a
Fix message
bartelink Dec 18, 2021
666ed14
Clarify/clean TODOs
bartelink Dec 18, 2021
ab052ee
Replace IProductPriceCalculator with Async function
bartelink Dec 18, 2021
87ecfc5
Extend InitializeCart API comments
bartelink Dec 18, 2021
0f9b076
Fix comment; remove redundant comment
bartelink Dec 18, 2021
6801abd
Update dependencies
bartelink Dec 18, 2021
847592e
ConfirmedIngester initial impl
bartelink Dec 20, 2021
a065f2d
Add TODO re reading from denormalized
bartelink Dec 20, 2021
fc1fb88
EaxctlyOnceIngester
bartelink Dec 23, 2021
b9f866f
Add Reactor
bartelink Dec 24, 2021
55600f8
Propulsion 2.12.0-rc.3
bartelink Dec 24, 2021
44bfcd0
Fix CartId-Guid parsing
bartelink Dec 30, 2021
30f5ef6
Fix ingestion test
bartelink Dec 30, 2021
317680c
Finish ConfirmedController
bartelink Dec 30, 2021
5bbe577
Switch to FsCodec.SystemTextJson
bartelink Jan 7, 2022
8d01486
remove unused extensions
bartelink Jan 7, 2022
2a456aa
FeedConsumer tmp
bartelink Mar 3, 2022
72313d0
Update Jet Infra deps
bartelink Mar 3, 2022
3e58117
Add/enable sln configs
bartelink May 23, 2022
944cf79
Deps update
bartelink Mar 11, 2022
3602532
Update dependencies; add DynamoDb wiring to domain.
bartelink May 21, 2022
87cf087
Wiring updates
bartelink May 21, 2022
e5a4adf
Update Reactor wiring
bartelink May 23, 2022
3923271
Target Propulsion 2.13.0-beta.4
bartelink May 23, 2022
6e50f5e
Propulsion 2.13.0-beta.5 updates
bartelink May 25, 2022
3f1091d
Cleanup Argument Processing
bartelink May 25, 2022
f9124c6
Equinox/FsCodec updates
bartelink Sep 5, 2022
ff88b72
Port Reactor wiring
bartelink Sep 6, 2022
aa1c3dc
Fix Ingester wiring
bartelink Sep 6, 2022
b70d630
Tidy
bartelink Sep 6, 2022
a501e93
Complete Esdb wiring
bartelink Sep 8, 2022
a06c04e
Polish ESDB wiring
bartelink Sep 8, 2022
a3b1141
Propulsion 3b3
bartelink Sep 8, 2022
11097d1
Eqx4rc1, Prop3b4, Support SSS
bartelink Sep 16, 2022
6e55889
Package updates
bartelink Oct 7, 2022
6a71c15
Equinox 4rc5.2
bartelink Dec 6, 2022
5a0b6e1
Package Updates
bartelink Dec 16, 2022
9e13479
Port to Equinox4rc16, Propulsion3rc10
bartelink Jan 18, 2024
ce74302
Updated Solution to include Equinox projects into build after breakin…
oskardudycz May 17, 2024
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
33 changes: 33 additions & 0 deletions EventSourcing.NetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SwaggerUi.Aspire.Hosting",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Scheduling", "Core.Scheduling\Core.Scheduling.csproj", "{3B754142-CDA4-430F-8DF7-5DC0D204EE35}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ECommerce.Domain", "Sample\ECommerce.Equinox\ECommerce.Domain\ECommerce.Domain.fsproj", "{7C22079D-C359-40DE-8E5F-1DDD9CDD06DD}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ECommerce.Reactor", "Sample\ECommerce.Equinox\ECommerce.Reactor\ECommerce.Reactor.fsproj", "{C3D0553F-5786-4417-97FD-B65440620274}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ECommerce.Api", "Sample\ECommerce.Equinox\ECommerce.Api\ECommerce.Api.fsproj", "{1F43CF34-CD69-4EEB-BF64-5AD649CE3AF5}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ECommerce.Infrastructure", "Sample\ECommerce.Equinox\ECommerce.Infrastructure\ECommerce.Infrastructure.fsproj", "{1C310D3A-BFC5-4984-BB11-4579067CFFB5}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ECommerce.Tests", "Sample\ECommerce.Equinox\ECommerce.Tests\ECommerce.Tests.fsproj", "{16B7A55B-E2E6-4CE2-896A-EF0F02259482}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ECommerce.FeedConsumer", "Sample\ECommerce.Equinox\ECommerce.FeedConsumer\ECommerce.FeedConsumer.fsproj", "{F2D522B1-BDF0-4345-919D-42DD79D4BB24}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ECommerce.Equinox", "ECommerce.Equinox", "{006643C6-E0B6-48E6-ABC6-9BE3DCB293D8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1079,6 +1093,18 @@ Global
{3B754142-CDA4-430F-8DF7-5DC0D204EE35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B754142-CDA4-430F-8DF7-5DC0D204EE35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B754142-CDA4-430F-8DF7-5DC0D204EE35}.Release|Any CPU.Build.0 = Release|Any CPU
{1F43CF34-CD69-4EEB-BF64-5AD649CE3AF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F43CF34-CD69-4EEB-BF64-5AD649CE3AF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C22079D-C359-40DE-8E5F-1DDD9CDD06DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C22079D-C359-40DE-8E5F-1DDD9CDD06DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2D522B1-BDF0-4345-919D-42DD79D4BB24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2D522B1-BDF0-4345-919D-42DD79D4BB24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C310D3A-BFC5-4984-BB11-4579067CFFB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C310D3A-BFC5-4984-BB11-4579067CFFB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3D0553F-5786-4417-97FD-B65440620274}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3D0553F-5786-4417-97FD-B65440620274}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16B7A55B-E2E6-4CE2-896A-EF0F02259482}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16B7A55B-E2E6-4CE2-896A-EF0F02259482}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1276,6 +1302,13 @@ Global
{D4CD0C59-65CC-4051-A77E-2C944F3C27C0} = {A8E25331-55E9-4D1A-87A4-136EC4D2A4B5}
{CCFE487A-A5C5-4DAC-8815-AC486B9625BC} = {0570E45A-2EB6-4C4C-84E4-2C80E1FECEB5}
{3B754142-CDA4-430F-8DF7-5DC0D204EE35} = {0570E45A-2EB6-4C4C-84E4-2C80E1FECEB5}
{006643C6-E0B6-48E6-ABC6-9BE3DCB293D8} = {A7186B6B-D56D-4AEF-B6B7-FAA827764C34}
{1F43CF34-CD69-4EEB-BF64-5AD649CE3AF5} = {006643C6-E0B6-48E6-ABC6-9BE3DCB293D8}
{7C22079D-C359-40DE-8E5F-1DDD9CDD06DD} = {006643C6-E0B6-48E6-ABC6-9BE3DCB293D8}
{F2D522B1-BDF0-4345-919D-42DD79D4BB24} = {006643C6-E0B6-48E6-ABC6-9BE3DCB293D8}
{1C310D3A-BFC5-4984-BB11-4579067CFFB5} = {006643C6-E0B6-48E6-ABC6-9BE3DCB293D8}
{C3D0553F-5786-4417-97FD-B65440620274} = {006643C6-E0B6-48E6-ABC6-9BE3DCB293D8}
{16B7A55B-E2E6-4CE2-896A-EF0F02259482} = {006643C6-E0B6-48E6-ABC6-9BE3DCB293D8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}
Expand Down
6 changes: 6 additions & 0 deletions Sample/ECommerce.Equinox/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<!-- TODO remove when on newer SDK-->
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace ECommerce.Api.Controllers

open Microsoft.AspNetCore.Mvc

open ECommerce.Domain

type TranchesDto = { activeEpochs : TrancheReferenceDto[] }
and TrancheReferenceDto = { seriesId : ConfirmedSeriesId; epochId : ConfirmedEpochId }

module TranchesDto =

let ofEpochId epochId =
{ activeEpochs = [| { seriesId = ConfirmedSeriesId.wellKnownId; epochId = epochId } |]}

type SliceDto = { closed : bool; carts : CartDto[]; position : ConfirmedCheckpoint; checkpoint : ConfirmedCheckpoint }
and CartDto = { id : CartId; items : ItemDto[] }
and ItemDto = { productId : ProductId; unitPrice : decimal; quantity : int }

module CartDto =

let ofDto (x : ConfirmedEpoch.Events.Cart) : CartDto =
{ id = x.cartId
items = [| for x in x.items -> { productId = x.productId; unitPrice = x.unitPrice; quantity = x.quantity } |] }

module Checkpoint =

let ofEpochAndOffset (epoch : ConfirmedEpochId) (offset : int) =
ConfirmedCheckpoint.ofEpochAndOffset epoch offset

let ofState (epochId : ConfirmedEpochId) (s : ConfirmedEpoch.Reader.StateDto) =
ConfirmedCheckpoint.ofEpochContent epochId s.closed s.carts.Length

[<Route("api/[controller]")>]
type ConfirmedFeedController(series : ConfirmedSeries.Service, epochs : ConfirmedEpoch.Reader.Service) =
inherit ControllerBase()

[<HttpGet>]
member _.ListTranches() : Async<TranchesDto> = async {
let! active = series.ReadIngestionEpochId()
return TranchesDto.ofEpochId active
}

[<HttpGet; Route("{epoch}")>]
member _.ReadTranche(epoch : ConfirmedEpochId) : Async<SliceDto> = async {
let! state = epochs.Read(epoch)
// TOCONSIDER closed should control cache header
let pos, checkpoint = Checkpoint.ofEpochAndOffset epoch 0, Checkpoint.ofState epoch state
return { closed = state.closed; carts = Array.map CartDto.ofDto state.carts; position = pos; checkpoint = checkpoint }
}

[<HttpGet; Route("slice/{token?}")>]
member _.Poll(token : System.Nullable<ConfirmedCheckpoint>) : Async<SliceDto> = async {
let pos = if token.HasValue then token.Value else ConfirmedCheckpoint.initial
let epochId, offset = ConfirmedCheckpoint.toEpochAndOffset pos
let! state = epochs.Read(epochId)
// TOCONSIDER closed should control cache header
let pos, checkpoint = Checkpoint.ofEpochAndOffset epochId offset, Checkpoint.ofState epochId state
return { closed = state.closed; carts = Array.skip offset state.carts |> Array.map CartDto.ofDto; position = pos; checkpoint = checkpoint }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
namespace ECommerce.Api.Controllers

open ECommerce.Domain
open Microsoft.AspNetCore.Mvc
open System

type InitializeShoppingCartRequest = { clientId : Guid Nullable }
type AddProductRequest = { productId : Guid; quantity : int }
type RemoveProductRequest = { productId : Guid; price : decimal }

[<ApiController>]
[<Route("api/[controller]")>]
type ShoppingCartsController(carts : ShoppingCart.Service, cartsDenormalized : ShoppingCartSummary.Service) =
inherit ControllerBase()

[<HttpPost>]
member _.InitializeCart([<FromBody>] request : InitializeShoppingCartRequest) : Async<IActionResult> = async {
if obj.ReferenceEquals(null, request) then nameof request |> nullArg

// TODO in these samples in general, the semantics should be extended to be more representative of the real world
// - you don't want to create orphan carts esp if they need to retry this API call
// - you may want to allow users to shop before logging in, keeping the clientId in a cookie.
// After some time, you let them log in, but then you need to merge the content into their real cart
// For now, the code remains in line with the C# version
let cartId = CartId.generate();
do! carts.Initialize(cartId, ClientId.parse request.clientId)
return CreatedResult("api/ShoppingCarts", cartId) :> _
}

[<HttpPost("{id}/products")>]
member _.AddProduct([<FromRoute>] id : Guid Nullable, [<FromBody>] request : AddProductRequest) : Async<IActionResult> = async {
if obj.ReferenceEquals(null, request) then nameof request |> nullArg

let CartId.ParseGuid cartId, ProductId.Parse productId = id, request.productId
do! carts.Add(cartId, productId, request.quantity)
return OkResult() :> _
}

[<HttpDelete("{id}/products")>]
member _.RemoveProduct([<FromRoute>] id : Guid Nullable, [<FromBody>] request : RemoveProductRequest) : Async<IActionResult> = async {
if obj.ReferenceEquals(null, request) then nameof request |> nullArg

let CartId.ParseGuid cartId, ProductId.Parse productId = id, request.productId
do! carts.Remove(cartId, productId, request.price)
return OkResult() :> _
}

[<HttpDelete("{id}/confirmation")>]
member _.ConfirmCart([<FromRoute>] id : Guid Nullable(*, [<FromBody>] request : ConfirmShoppingCartRequest*)) : Async<IActionResult> = async {
// if obj.ReferenceEquals(null, request) then nameof request |> nullArg // TODO only relevant if we follow version-contingent style

let (CartId.ParseGuid cartId) = id
do! carts.Confirm(cartId, DateTimeOffset.UtcNow)
return OkResult() :> _
}

/// Reads from write side
[<HttpGet("{id}")>]
member _.Get([<FromRoute>] id : Guid Nullable) : Async<IActionResult> = async {
let (CartId.ParseGuid cartId) = id
match! carts.Read cartId with
| Some (res : ShoppingCart.Details.View) -> return OkObjectResult res :> _
| None -> return NotFoundResult() :> _
}

/// Reads from denormalized view
[<HttpGet("{id}/summary")>]
member _.GetSummary([<FromRoute>] id : Guid Nullable) : Async<IActionResult> = async {
let (CartId.ParseGuid cartId) = id
match! cartsDenormalized.Read cartId with
| Some (res : ShoppingCartSummary.Details.View) -> return OkObjectResult res :> _
| None -> return NotFoundResult() :> _
}

(* TODO we dont produce a list like this atm - not porting for the moment as having an arbitrarily growing list like this does not really make
sense; instead, we'll produce a cart summaries API
[HttpGet]
public Task<IReadOnlyList<ShoppingCartShortInfo>> Get(
[FromServices] Func<GetCarts, CancellationToken, Task<IReadOnlyList<ShoppingCartShortInfo>>> query,
CancellationToken ct,
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 20
) =>
query(GetCarts.From(pageNumber, pageSize), ct);
*)
28 changes: 28 additions & 0 deletions Sample/ECommerce.Equinox/ECommerce.Api/ECommerce.Api.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

<ItemGroup>
<Compile Include="Controllers\ShoppingCartsController.fs" />
<Compile Include="Controllers\ConfirmedFeedController.fs" />
<Compile Include="Startup.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

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

<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<_ContentIncludedByDefault Remove="Properties\launchSettings.json" />
</ItemGroup>

</Project>
82 changes: 82 additions & 0 deletions Sample/ECommerce.Equinox/ECommerce.Api/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module ECommerce.Api.Program

open ECommerce
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.DependencyInjection
open Serilog
open System

type Configuration(tryGet) =
inherit Args.Configuration(tryGet)

let [<Literal>] AppName = "ECommerce.Web"

module Args =
Comment thread
bartelink marked this conversation as resolved.

open Argu

type [<NoEquality; NoComparison>] Parameters =
| [<AltCommandLine "-V"; Unique>] Verbose
| [<AltCommandLine "-p"; Unique>] PrometheusPort of int
| [<CliPrefix(CliPrefix.None); Last>] Cosmos of ParseResults<Args.Cosmos.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Dynamo of ParseResults<Args.Dynamo.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Esdb of ParseResults<Args.Esdb.Parameters>
| [<CliPrefix(CliPrefix.None); Last>] Sss of ParseResults<Args.Sss.Parameters>
interface IArgParserTemplate with
member a.Usage = a |> function
| Verbose -> "request verbose logging."
| PrometheusPort _ -> "port from which to expose a Prometheus /metrics endpoint. Default: off (optional if environment variable PROMETHEUS_PORT specified)"
| Cosmos _ -> "specify CosmosDB input parameters"
| Dynamo _ -> "specify DynamoDB input parameters"
| Esdb _ -> "specify EventStore input parameters"
| Sss _ -> "specify SqlStreamStore input parameters"
and [<RequireQualifiedAccess>]
Arguments(c : Configuration, p : ParseResults<Parameters>) =
member val Verbose = p.Contains Verbose
member val PrometheusPort = p.TryGetResult PrometheusPort |> Option.orElseWith (fun () -> c.PrometheusPort)
member val CacheSizeMb = 10
member val StoreArgs : Args.StoreArgs =
match p.TryGetSubCommand() with
| Some (Parameters.Cosmos cosmos) -> Args.StoreArgs.Cosmos (Args.Cosmos.Arguments(c, cosmos))
| Some (Parameters.Dynamo dynamo) -> Args.StoreArgs.Dynamo (Args.Dynamo.Arguments(c, dynamo))
| Some (Parameters.Esdb es) -> Args.StoreArgs.Esdb (Args.Esdb.Arguments(c, es))
| Some (Parameters.Sss sss) -> Args.StoreArgs.Sss (Args.Sss.Arguments(c, sss))
| _ -> p.Raise "Must specify one of cosmos, dynamo, esdb or sss for store"
member x.VerboseStore = Args.StoreArgs.verboseRequested x.StoreArgs
member x.Connect(): Store.Config =
let cache = Equinox.Cache (AppName, sizeMb = x.CacheSizeMb)
Args.StoreArgs.connectTarget x.StoreArgs cache

/// Parse the commandline; can throw exceptions in response to missing arguments and/or `-h`/`--help` args
let parse tryGetConfigValue argv =
let programName = Reflection.Assembly.GetEntryAssembly().GetName().Name
let parser = ArgumentParser.Create<Parameters>(programName = programName)
Arguments(Configuration tryGetConfigValue, parser.ParseCommandLine argv)

let run (args : Args.Arguments) =
let store = args.Connect()
let carts = Domain.ShoppingCart.Config.create store
let registerServices (services: IServiceCollection) =
services.AddSingleton(carts) |> ignore
WebHostBuilder()
.UseKestrel()
.UseSerilog()
.ConfigureServices(registerServices)
.UseStartup<Startup>()
.Build()
.Run()

[<EntryPoint>]
let main argv =
try let args = Args.parse EnvVar.tryGet argv
let metrics = Sinks.tags AppName |> Sinks.equinoxMetricsOnly
try Log.Logger <- LoggerConfiguration()
.Configure(args.Verbose)
.MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
.Sinks(metrics, args.VerboseStore)
.CreateLogger()
try run args; 0
with e -> Log.Fatal(e, "Exiting"); 2
finally Log.CloseAndFlush()
with:? Argu.ArguParseException as e -> eprintfn $"%s{e.Message}"; 1
| e -> eprintfn $"Exception %s{e.Message}"; 1
31 changes: 31 additions & 0 deletions Sample/ECommerce.Equinox/ECommerce.Api/Startup.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace ECommerce.Api

open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Prometheus
open Serilog

type Startup() =

member _.ConfigureServices(services : IServiceCollection) : unit =
services.AddMvc() |> ignore
services.AddControllers()
.AddNewtonsoftJson() |> ignore
// TODO AddSwaggerGen

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
member _.Configure(app : IApplicationBuilder, env : IHostEnvironment) : unit =
if env.IsDevelopment() then
app.UseDeveloperExceptionPage() |> ignore

app
.UseRouting()
//.UseAuthorization()
.UseSerilogRequestLogging() // see https://nblumhardt.com/2019/10/serilog-in-aspnetcore-3/
.UseEndpoints(fun endpoints ->
endpoints.MapControllers() |> ignore
endpoints.MapMetrics() |> ignore)
|> ignore
// app.UseSwagger();
// app.UseSwaggerUI
Loading