-
-
Notifications
You must be signed in to change notification settings - Fork 547
Port Sample/EventStoreDB/Simple/ECommerce to F#+Equinox #84
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
Merged
oskardudycz
merged 48 commits into
oskardudycz:main
from
bartelink:bartelink/add-equinox
May 17, 2024
Merged
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
20c6bfd
WIP
bartelink fc6b79b
Add API
bartelink c1eec08
Add Read logic
bartelink 418eb20
Add Esdb wiring to Domain
bartelink 7ff20cf
Add Cosmos wiring for API startup
bartelink b7afa42
Add Esdb wiring for API
bartelink 34b1581
Tidy pricing interface
bartelink 818be10
Remove PricedProductItem types
bartelink 0932a7a
Fix errors
bartelink 338625a
Fix message
bartelink 666ed14
Clarify/clean TODOs
bartelink ab052ee
Replace IProductPriceCalculator with Async function
bartelink 87ecfc5
Extend InitializeCart API comments
bartelink 0f9b076
Fix comment; remove redundant comment
bartelink 6801abd
Update dependencies
bartelink 847592e
ConfirmedIngester initial impl
bartelink a065f2d
Add TODO re reading from denormalized
bartelink fc1fb88
EaxctlyOnceIngester
bartelink b9f866f
Add Reactor
bartelink 55600f8
Propulsion 2.12.0-rc.3
bartelink 44bfcd0
Fix CartId-Guid parsing
bartelink 30f5ef6
Fix ingestion test
bartelink 317680c
Finish ConfirmedController
bartelink 5bbe577
Switch to FsCodec.SystemTextJson
bartelink 8d01486
remove unused extensions
bartelink 2a456aa
FeedConsumer tmp
bartelink 72313d0
Update Jet Infra deps
bartelink 3e58117
Add/enable sln configs
bartelink 944cf79
Deps update
bartelink 3602532
Update dependencies; add DynamoDb wiring to domain.
bartelink 87cf087
Wiring updates
bartelink e5a4adf
Update Reactor wiring
bartelink 3923271
Target Propulsion 2.13.0-beta.4
bartelink 6e50f5e
Propulsion 2.13.0-beta.5 updates
bartelink 3f1091d
Cleanup Argument Processing
bartelink f9124c6
Equinox/FsCodec updates
bartelink ff88b72
Port Reactor wiring
bartelink aa1c3dc
Fix Ingester wiring
bartelink b70d630
Tidy
bartelink a501e93
Complete Esdb wiring
bartelink a06c04e
Polish ESDB wiring
bartelink a3b1141
Propulsion 3b3
bartelink 11097d1
Eqx4rc1, Prop3b4, Support SSS
bartelink 6e55889
Package updates
bartelink 6a71c15
Equinox 4rc5.2
bartelink 5a0b6e1
Package Updates
bartelink 9e13479
Port to Equinox4rc16, Propulsion3rc10
bartelink ce74302
Updated Solution to include Equinox projects into build after breakin…
oskardudycz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
59 changes: 59 additions & 0 deletions
59
Sample/ECommerce.Equinox/ECommerce.Api/Controllers/ConfirmedFeedController.fs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } | ||
| } |
85 changes: 85 additions & 0 deletions
85
Sample/ECommerce.Equinox/ECommerce.Api/Controllers/ShoppingCartsController.fs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
28
Sample/ECommerce.Equinox/ECommerce.Api/ECommerce.Api.fsproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = | ||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.