From b710a4bdd79596d3620efc8acf23fb6b0e152f92 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Thu, 2 May 2024 12:25:59 +0200 Subject: [PATCH] Implemented Optimistic Concurrency with ESDB exercise solution --- .../Settings.cs | 15 -- .../AddProductItemToShoppingCartTests.cs | 28 +-- .../ShoppingCarts/CancelShoppingCartTests.cs | 26 +-- .../ShoppingCarts/ConfirmShoppingCartTests.cs | 28 +-- .../ShoppingCarts/OpenShoppingCartTests.cs | 4 +- .../RemoveProductItemFromShoppingCartTests.cs | 36 ++-- ...-OptimisticConcurrency.EventStoreDB.csproj | 6 +- .../Core/Entities/Aggregate.cs | 19 ++- .../EventStoreDB/EventStoreDBExtensions.cs | 161 ++++++++++++++++++ .../Core/Http/ETagExtensions.cs | 13 +- .../Core/Marten/DocumentSessionExtensions.cs | 54 ------ .../Core/Marten/EventMappings.cs | 12 -- .../Immutable/ShoppingCarts/Api.cs | 43 +++-- .../Immutable/ShoppingCarts/Configure.cs | 49 +++--- .../Immutable/ShoppingCarts/ShoppingCart.cs | 30 ++-- .../Mixed/ShoppingCarts/Api.cs | 45 +++-- .../Mixed/ShoppingCarts/Configure.cs | 58 ++++--- .../{MixedShoppingCart.cs => ShoppingCart.cs} | 16 +- .../Mutable/ShoppingCarts/Api.cs | 45 +++-- .../Mutable/ShoppingCarts/Configure.cs | 48 +++--- ...MutableShoppingCart.cs => ShoppingCart.cs} | 16 +- .../Program.cs | 31 +--- .../appsettings.json | 2 +- 23 files changed, 435 insertions(+), 350 deletions(-) create mode 100644 Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/EventStoreDB/EventStoreDBExtensions.cs delete mode 100644 Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/DocumentSessionExtensions.cs delete mode 100644 Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/EventMappings.cs rename Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/{MixedShoppingCart.cs => ShoppingCart.cs} (93%) rename Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/{MutableShoppingCart.cs => ShoppingCart.cs} (93%) diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/Settings.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/Settings.cs index fb97e8178..217120083 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/Settings.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/Settings.cs @@ -1,18 +1,3 @@ -using Oakton; using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; -[assembly: TestFramework("OptimisticConcurrency.Marten.Tests.AssemblyFixture", "OptimisticConcurrency.Marten.Tests")] [assembly: CollectionBehavior(DisableTestParallelization = true)] - -namespace OptimisticConcurrency.EventStoreDB.Tests; - -public sealed class AssemblyFixture : XunitTestFramework -{ - public AssemblyFixture(IMessageSink messageSink) - :base(messageSink) - { - OaktonEnvironment.AutoStartHost = true; - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/AddProductItemToShoppingCartTests.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/AddProductItemToShoppingCartTests.cs index 7dd1824ac..112b4bee9 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/AddProductItemToShoppingCartTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/AddProductItemToShoppingCartTests.cs @@ -21,7 +21,7 @@ public class AddProductItemToShoppingCartTests(ApiSpecification api): POST, URI(ShoppingCartProductItemsUrl(apiPrefix, ClientId, NotExistingShoppingCartId)), BODY(new AddProductRequest(ProductItem)), - HEADERS(IF_MATCH(0)) + HEADERS(IF_MATCH(-1)) ) .Then(NOT_FOUND); @@ -35,9 +35,9 @@ public class AddProductItemToShoppingCartTests(ApiSpecification api): POST, URI(ctx => ShoppingCartProductItemsUrl(apiPrefix, ClientId, ctx.GetCreatedId())), BODY(new AddProductRequest(ProductItem)), - HEADERS(IF_MATCH(1)) + HEADERS(IF_MATCH(0)) ) - .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(2)); + .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(1)); [Theory] [InlineData("immutable")] @@ -46,15 +46,15 @@ public class AddProductItemToShoppingCartTests(ApiSpecification api): public Task AddsProductItemToNonEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When( POST, URI(ctx => ShoppingCartProductItemsUrl(apiPrefix, ClientId, ctx.GetCreatedId())), BODY(new AddProductRequest(ProductItem)), - HEADERS(IF_MATCH(2)) + HEADERS(IF_MATCH(1)) ) - .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(3)); + .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(2)); [Theory] [InlineData("immutable")] @@ -63,14 +63,14 @@ public class AddProductItemToShoppingCartTests(ApiSpecification api): public Task CantAddProductItemToConfirmedShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenConfirmed(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenConfirmed(apiPrefix, ClientId, 1) ) .When( POST, URI(ctx => ShoppingCartProductItemsUrl(apiPrefix, ClientId, ctx.GetCreatedId())), BODY(new AddProductRequest(ProductItem)), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(1)) ) .Then(CONFLICT); @@ -81,14 +81,14 @@ public class AddProductItemToShoppingCartTests(ApiSpecification api): public Task CantAddProductItemToCanceledShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenCanceled(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenCanceled(apiPrefix, ClientId, 1) ) .When( POST, URI(ctx => ShoppingCartProductItemsUrl(apiPrefix, ClientId, ctx.GetCreatedId())), BODY(new AddProductRequest(ProductItem)), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(2)) ) .Then(CONFLICT); @@ -99,10 +99,10 @@ public class AddProductItemToShoppingCartTests(ApiSpecification api): public Task ReturnsNonEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When(GET, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId()))) - .Then(OK, RESPONSE_ETAG_HEADER(2)); + .Then(OK, RESPONSE_ETAG_HEADER(1)); private static readonly Faker Faker = new(); private readonly Guid NotExistingShoppingCartId = Guid.NewGuid(); diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/CancelShoppingCartTests.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/CancelShoppingCartTests.cs index 9f27312d0..636c25e05 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/CancelShoppingCartTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/CancelShoppingCartTests.cs @@ -20,7 +20,7 @@ public class CancelShoppingCartTests(ApiSpecification api): .When( DELETE, URI(ShoppingCartUrl(apiPrefix, ClientId, NotExistingShoppingCartId)), - HEADERS(IF_MATCH(0)) + HEADERS(IF_MATCH(-1)) ) .Then(NOT_FOUND); @@ -31,14 +31,14 @@ public class CancelShoppingCartTests(ApiSpecification api): public Task CancelsNonEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When( DELETE, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId())), - HEADERS(IF_MATCH(2)) + HEADERS(IF_MATCH(1)) ) - .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(3)); + .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(2)); [Theory] [InlineData("immutable")] @@ -47,13 +47,13 @@ public class CancelShoppingCartTests(ApiSpecification api): public Task CantCancelAlreadyCanceledShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenCanceled(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenCanceled(apiPrefix, ClientId, 1) ) .When( DELETE, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId())), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(2)) ) .Then(CONFLICT); @@ -64,13 +64,13 @@ public class CancelShoppingCartTests(ApiSpecification api): public Task CantCancelConfirmedShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenConfirmed(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenConfirmed(apiPrefix, ClientId, 1) ) .When( DELETE, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId())), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(2)) ) .Then(CONFLICT); @@ -81,11 +81,11 @@ public class CancelShoppingCartTests(ApiSpecification api): public Task ReturnsNonEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenCanceled(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenCanceled(apiPrefix, ClientId, 1) ) .When(GET, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId()))) - .Then(OK, RESPONSE_ETAG_HEADER(3)); + .Then(OK, RESPONSE_ETAG_HEADER(2)); private static readonly Faker Faker = new(); private readonly Guid NotExistingShoppingCartId = Guid.NewGuid(); diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/ConfirmShoppingCartTests.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/ConfirmShoppingCartTests.cs index 4468b36f3..369d9c38d 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/ConfirmShoppingCartTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/ConfirmShoppingCartTests.cs @@ -20,7 +20,7 @@ public class ConfirmShoppingCartTests(ApiSpecification api): .When( POST, URI(ConfirmShoppingCartUrl(apiPrefix, ClientId, NotExistingShoppingCartId)), - HEADERS(IF_MATCH(0)) + HEADERS(IF_MATCH(-1)) ) .Then(NOT_FOUND); @@ -33,7 +33,7 @@ public class ConfirmShoppingCartTests(ApiSpecification api): .When( POST, URI(ctx => ConfirmShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId())), - HEADERS(IF_MATCH(1)) + HEADERS(IF_MATCH(0)) ) .Then(CONFLICT); @@ -44,14 +44,14 @@ public class ConfirmShoppingCartTests(ApiSpecification api): public Task ConfirmsNonEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When( POST, URI(ctx => ConfirmShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId())), - HEADERS(IF_MATCH(2)) + HEADERS(IF_MATCH(1)) ) - .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(3)); + .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(2)); [Theory] [InlineData("immutable")] @@ -60,13 +60,13 @@ public class ConfirmShoppingCartTests(ApiSpecification api): public Task CantConfirmAlreadyConfirmedShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenConfirmed(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenConfirmed(apiPrefix, ClientId, 1) ) .When( POST, URI(ctx => ConfirmShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId())), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(2)) ) .Then(CONFLICT); @@ -77,13 +77,13 @@ public class ConfirmShoppingCartTests(ApiSpecification api): public Task CantConfirmCanceledShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenCanceled(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenCanceled(apiPrefix, ClientId, 1) ) .When( POST, URI(ctx => ConfirmShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId())), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(2)) ) .Then(CONFLICT); @@ -94,11 +94,11 @@ public class ConfirmShoppingCartTests(ApiSpecification api): public Task ReturnsNonEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenConfirmed(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenConfirmed(apiPrefix, ClientId, 1) ) .When(GET, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId()))) - .Then(OK, RESPONSE_ETAG_HEADER(3)); + .Then(OK, RESPONSE_ETAG_HEADER(2)); private static readonly Faker Faker = new(); private readonly Guid NotExistingShoppingCartId = Guid.NewGuid(); diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/OpenShoppingCartTests.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/OpenShoppingCartTests.cs index 525c11e11..fa628f469 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/OpenShoppingCartTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/OpenShoppingCartTests.cs @@ -18,7 +18,7 @@ public class OpenShoppingCartTests(ApiSpecification api): .When(POST, URI(ShoppingCartsUrl(apiPrefix, ClientId))) .Then( CREATED_WITH_DEFAULT_HEADERS(locationHeaderPrefix: ShoppingCartsUrl(apiPrefix, ClientId)), - RESPONSE_ETAG_HEADER(1) + RESPONSE_ETAG_HEADER(0) ); [Theory] @@ -28,7 +28,7 @@ public class OpenShoppingCartTests(ApiSpecification api): public Task ReturnsOpenedShoppingCart(string apiPrefix) => api.Given(OpenedShoppingCart(apiPrefix, ClientId)) .When(GET, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId()))) - .Then(OK, RESPONSE_ETAG_HEADER(1)); + .Then(OK, RESPONSE_ETAG_HEADER(0)); private readonly Guid ClientId = Guid.NewGuid(); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/RemoveProductItemFromShoppingCartTests.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/RemoveProductItemFromShoppingCartTests.cs index e34ce87a1..efb4bc57a 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/RemoveProductItemFromShoppingCartTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB.Tests/ShoppingCarts/RemoveProductItemFromShoppingCartTests.cs @@ -20,7 +20,7 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap .When( DELETE, URI(ShoppingCartProductItemUrl(apiPrefix, ClientId, NotExistingShoppingCartId, ProductItem.ProductId!.Value)), - HEADERS(IF_MATCH(0)) + HEADERS(IF_MATCH(-1)) ) .Then(NOT_FOUND); @@ -33,7 +33,7 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap .When( DELETE, URI(ctx => ShoppingCartProductItemUrl(apiPrefix, ClientId, ctx.GetCreatedId(), ProductItem.ProductId!.Value)), - HEADERS(IF_MATCH(1)) + HEADERS(IF_MATCH(0)) ) .Then(CONFLICT); @@ -44,14 +44,14 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap public Task CanRemoveExistingProductItemFromShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When( DELETE, URI(ctx => ShoppingCartProductItemUrl(apiPrefix, ClientId, ctx.GetCreatedId(), ProductItem.ProductId!.Value)), - HEADERS(IF_MATCH(2)) + HEADERS(IF_MATCH(1)) ) - .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(3)); + .Then(NO_CONTENT, RESPONSE_ETAG_HEADER(2)); [Theory] [InlineData("immutable")] @@ -60,13 +60,13 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap public Task CantRemoveExistingProductItemFromShoppingCartWithWrongETag(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When( DELETE, URI(ctx => ShoppingCartProductItemUrl(apiPrefix, ClientId, ctx.GetCreatedId(), ProductItem.ProductId!.Value)), - HEADERS(IF_MATCH(1)) + HEADERS(IF_MATCH(0)) ) .Then(PRECONDITION_FAILED); @@ -77,7 +77,7 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap public Task CantRemoveExistingProductItemFromShoppingCartWithoutETag(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When( DELETE, @@ -93,12 +93,12 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap public Task CantRemoveNonExistingProductItemFromEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When( DELETE, URI(ctx => ShoppingCartProductItemUrl(apiPrefix, ClientId, ctx.GetCreatedId(), NotExistingProductItem.ProductId!.Value)), - HEADERS(IF_MATCH(2)) + HEADERS(IF_MATCH(1)) ) .Then(CONFLICT); @@ -109,13 +109,13 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap public Task CantRemoveExistingProductItemFromCanceledShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenCanceled(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenCanceled(apiPrefix, ClientId, 1) ) .When( DELETE, URI(ctx => ShoppingCartProductItemUrl(apiPrefix, ClientId, ctx.GetCreatedId(), ProductItem.ProductId!.Value)), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(2)) ) .Then(CONFLICT); @@ -126,13 +126,13 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap public Task CantRemoveExistingProductItemFromConfirmedShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1), - ThenConfirmed(apiPrefix, ClientId, 2) + WithProductItem(apiPrefix, ClientId, ProductItem, 0), + ThenConfirmed(apiPrefix, ClientId, 1) ) .When( DELETE, URI(ctx => ShoppingCartProductItemUrl(apiPrefix, ClientId, ctx.GetCreatedId(), ProductItem.ProductId!.Value)), - HEADERS(IF_MATCH(3)) + HEADERS(IF_MATCH(2)) ) .Then(CONFLICT); @@ -143,10 +143,10 @@ public class RemoveProductItemFromShoppingCartTests(ApiSpecification ap public Task ReturnsNonEmptyShoppingCart(string apiPrefix) => api.Given( OpenedShoppingCart(apiPrefix, ClientId), - WithProductItem(apiPrefix, ClientId, ProductItem, 1) + WithProductItem(apiPrefix, ClientId, ProductItem, 0) ) .When(GET, URI(ctx => ShoppingCartUrl(apiPrefix, ClientId, ctx.GetCreatedId()))) - .Then(OK, RESPONSE_ETAG_HEADER(2)); + .Then(OK, RESPONSE_ETAG_HEADER(1)); private static readonly Faker Faker = new(); private readonly Guid NotExistingShoppingCartId = Guid.NewGuid(); diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/11-OptimisticConcurrency.EventStoreDB.csproj b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/11-OptimisticConcurrency.EventStoreDB.csproj index 4259d2e43..4a6e23a88 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/11-OptimisticConcurrency.EventStoreDB.csproj +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/11-OptimisticConcurrency.EventStoreDB.csproj @@ -9,13 +9,11 @@ - - - + - + diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Entities/Aggregate.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Entities/Aggregate.cs index 679721cce..5d2012686 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Entities/Aggregate.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Entities/Aggregate.cs @@ -2,20 +2,20 @@ namespace OptimisticConcurrency.Core.Entities; public interface IAggregate { - public void Apply(object @event); + public void Evolve(object @event); object[] DequeueUncommittedEvents(); } public abstract class Aggregate: IAggregate { - public Guid Id { get; protected set; } = default!; + public Guid Id { get; protected set; } - private readonly Queue uncommittedEvents = new(); + private readonly Queue uncommittedEvents = new(); - protected virtual void Apply(TEvent @event) { } + public abstract void Evolve(TEvent @event); - public object[] DequeueUncommittedEvents() + public TEvent[] DequeueUncommittedEvents() { var dequeuedEvents = uncommittedEvents.ToArray(); @@ -24,16 +24,19 @@ public object[] DequeueUncommittedEvents() return dequeuedEvents; } - protected void Enqueue(object @event) + protected void Enqueue(TEvent @event) { uncommittedEvents.Enqueue(@event); } - public void Apply(object @event) + public void Evolve(object @event) { if(@event is not TEvent typed) return; - Apply(typed); + Evolve(typed); } + + object[] IAggregate.DequeueUncommittedEvents() => + DequeueUncommittedEvents().Cast().ToArray(); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/EventStoreDB/EventStoreDBExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/EventStoreDB/EventStoreDBExtensions.cs new file mode 100644 index 000000000..4200b8a2d --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/EventStoreDB/EventStoreDBExtensions.cs @@ -0,0 +1,161 @@ +using System.Text.Json; +using EventStore.Client; +using OptimisticConcurrency.Core.Entities; +using OptimisticConcurrency.Core.Exceptions; +using OptimisticConcurrency.Immutable.ShoppingCarts; + +namespace OptimisticConcurrency.Core.EventStoreDB; + +public static class EventStoreDBExtensions +{ + public static Task<(T?, StreamRevision?)> AggregateStream( + this EventStoreClient eventStore, + Func getInitial, + Guid id, + CancellationToken ct = default + ) where T : Aggregate => + eventStore.AggregateStream( + (state, @event) => + { + state.Evolve(@event); + return state; + }, + getInitial, + id, + ct + ); + + public static async Task<(T?, StreamRevision?)> AggregateStream( + this EventStoreClient eventStore, + Func evolve, + Func getInitial, + Guid id, + CancellationToken cancellationToken = default + ) where T : class + { + var result = eventStore.ReadStreamAsync( + Direction.Forwards, + ToStreamName(id), + StreamPosition.Start, + cancellationToken: cancellationToken + ); + + if (await result.ReadState == ReadState.StreamNotFound) + return (null, null); + + StreamRevision? revision = null; + var state = await result + .Select(@event => + { + revision = StreamRevision.FromStreamPosition(@event.Event.EventNumber); + return (TEvent)JsonSerializer.Deserialize( + @event.Event.Data.Span, + Type.GetType(@event.Event.EventType)! + )!; + } + ) + .AggregateAsync( + getInitial(), + evolve, + cancellationToken + ); + + return (state, revision); + } + + public static Task Add(this EventStoreClient eventStore, Guid id, T aggregate, + CancellationToken ct) + where T : class, IAggregate => + eventStore.Add(id, aggregate.DequeueUncommittedEvents(), ct); + + public static async Task Add(this EventStoreClient eventStore, Guid id, object[] events, + CancellationToken ct) + where T : class + { + var result = await eventStore.AppendToStreamAsync( + ToStreamName(id), + StreamState.NoStream, + ToEventData(events), + cancellationToken: ct + ); + + return result.NextExpectedStreamRevision; + } + + public static Task GetAndUpdate( + this EventStoreClient eventStore, + Func getInitial, + Guid id, + StreamRevision expectedRevision, + Action handle, + CancellationToken ct + ) + where T : Aggregate + where TEvent : notnull => + eventStore.GetAndUpdate( + (state, @event) => + { + state.Evolve(@event); + return state; + }, + getInitial, + id, + expectedRevision, + state => + { + handle(state); + var events = state.DequeueUncommittedEvents(); + return events; + }, ct); + + public static Task GetAndUpdate( + this EventStoreClient eventStore, + Func evolve, + Func getInitial, + Guid id, + Func handle, + CancellationToken ct + ) + where T : class + where TEvent : notnull => + eventStore.GetAndUpdate(evolve, getInitial, id, null, handle, ct); + + public static async Task GetAndUpdate( + this EventStoreClient eventStore, + Func evolve, + Func getInitial, + Guid id, + StreamRevision? expectedRevision, + Func handle, + CancellationToken ct + ) + where T : class + where TEvent : notnull + { + var streamName = ToStreamName(id); + var (current, actualRevision) = await eventStore.AggregateStream(evolve, getInitial, id, ct); + + var events = handle(current ?? throw NotFoundException.For(id)); + + var result = await eventStore.AppendToStreamAsync( + streamName, + expectedRevision ?? actualRevision ?? StreamRevision.None, + ToEventData(events.Cast()), + cancellationToken: ct + ); + + return result.NextExpectedStreamRevision; + } + + private static IEnumerable ToEventData(IEnumerable events) => + events.Select(@event => + new EventData( + Uuid.NewUuid(), + @event.GetType().FullName!, + JsonSerializer.SerializeToUtf8Bytes(@event) + ) + ); + + private static string ToStreamName(Guid id) => + $"{typeof(T).Name}-{id}"; +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Http/ETagExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Http/ETagExtensions.cs index 5a143b88c..8b9bafc63 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Http/ETagExtensions.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Http/ETagExtensions.cs @@ -1,3 +1,4 @@ +using EventStore.Client; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -6,7 +7,7 @@ namespace OptimisticConcurrency.Core.Http; public static class ETagExtensions { - public static int ToExpectedVersion(string? eTag) + public static StreamRevision ToExpectedRevision(string? eTag) { if (eTag is null) throw new ArgumentNullException(nameof(eTag)); @@ -16,18 +17,18 @@ public static int ToExpectedVersion(string? eTag) if (value is null) throw new ArgumentNullException(nameof(eTag)); - return int.Parse(value.Substring(1, value.Length - 2)); + return StreamRevision.FromInt64(long.Parse(value.Substring(1, value.Length - 2))); } - public static void SetResponseEtag(this HttpContext httpContext, int? version) + public static void SetResponseEtag(this HttpContext httpContext, StreamRevision? revision) { - if (!version.HasValue) + if (!revision.HasValue) return; - httpContext.Response.Headers.ETag = version.Value.ToWeakEtag(); + httpContext.Response.Headers.ETag = revision.Value.ToInt64().ToWeakEtag(); } - public static StringValues ToWeakEtag(this int version) => + public static StringValues ToWeakEtag(this long version) => $"W/\"{version}\""; } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/DocumentSessionExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/DocumentSessionExtensions.cs deleted file mode 100644 index 7da1d7a7f..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/DocumentSessionExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Marten; -using OptimisticConcurrency.Core.Entities; -using OptimisticConcurrency.Core.Exceptions; - -namespace OptimisticConcurrency.Core.Marten; - -public static class DocumentSessionExtensions -{ - public static Task Add(this IDocumentSession documentSession, Guid id, object @event, CancellationToken ct) - where T : class => - documentSession.Add(id, [@event], ct); - - public static async Task Add(this IDocumentSession documentSession, Guid id, object[] events, CancellationToken ct) - where T : class - { - documentSession.Events.StartStream(id, events); - await documentSession.SaveChangesAsync(token: ct); - - return events.Length; - } - - public static Task GetAndUpdate( - this IDocumentSession documentSession, - Guid id, - int expectedVersion, - Action handle, - CancellationToken ct - ) where T : class, IAggregate - => documentSession.GetAndUpdate(id, expectedVersion, state => - { - handle(state); - return state.DequeueUncommittedEvents(); - }, ct); - - public static async Task GetAndUpdate( - this IDocumentSession documentSession, - Guid id, - int expectedVersion, - Func handle, - CancellationToken ct - ) where T : class - { - var nextExpectedVersion = expectedVersion; - await documentSession.Events.WriteToAggregate(id, expectedVersion, stream => - { - var aggregate = stream.Aggregate ?? throw NotFoundException.For(id); - var events = handle(aggregate); - stream.AppendMany(events); - nextExpectedVersion += events.Length; - }, ct); - - return nextExpectedVersion; - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/EventMappings.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/EventMappings.cs deleted file mode 100644 index 00b56b011..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Core/Marten/EventMappings.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Marten; - -namespace OptimisticConcurrency.Core.Marten; - -public static class EventMappings -{ - public static StoreOptions MapEventWithPrefix(this StoreOptions options, string prefix) where T : class - { - options.Events.MapEventType($"{prefix}-${typeof(T).FullName}"); - return options; - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Api.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Api.cs index 8d86842a1..c6dfba963 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Api.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Api.cs @@ -1,9 +1,8 @@ using Core.Validation; -using Marten; -using Marten.Schema.Identity; +using EventStore.Client; using Microsoft.AspNetCore.Mvc; +using OptimisticConcurrency.Core.EventStoreDB; using OptimisticConcurrency.Core.Http; -using OptimisticConcurrency.Core.Marten; using OptimisticConcurrency.Immutable.Pricing; using static Microsoft.AspNetCore.Http.TypedResults; using static System.DateTimeOffset; @@ -26,16 +25,16 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication shoppingCarts.MapPost("", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid clientId, CancellationToken ct) => { - var shoppingCartId = CombGuidIdGeneration.NewGuid(); + var shoppingCartId = Uuid.NewUuid().ToGuid(); - var nextExpectedVersion = await session.Add(shoppingCartId, + var nextExpectedRevision = await eventStore.Add(shoppingCartId, [Handle(new OpenShoppingCart(shoppingCartId, clientId.NotEmpty(), Now))], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return Created($"/api/immutable/clients/{clientId}/shopping-carts/{shoppingCartId}", shoppingCartId); } @@ -45,7 +44,7 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication async ( HttpContext context, IProductPriceCalculator pricingCalculator, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, AddProductRequest body, [FromIfMatchHeader] string eTag, @@ -53,7 +52,7 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication { var productItem = body.ProductItem.NotNull().ToProductItem(); - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [ Handle(pricingCalculator, @@ -61,7 +60,7 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication state) ], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -70,7 +69,7 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication productItems.MapDelete("{productId:guid}", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromRoute] Guid productId, [FromQuery] int? quantity, @@ -84,11 +83,11 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication unitPrice.NotNull().Positive() ); - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [Handle(new RemoveProductItemFromShoppingCart(shoppingCartId, productItem, Now), state)], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -97,15 +96,15 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication shoppingCart.MapPost("confirm", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromIfMatchHeader] string eTag, CancellationToken ct) => { - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [Handle(new ConfirmShoppingCart(shoppingCartId, Now), state)], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -114,15 +113,15 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication shoppingCart.MapDelete("", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromIfMatchHeader] string eTag, CancellationToken ct) => { - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [Handle(new CancelShoppingCart(shoppingCartId, Now), state)], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -131,13 +130,13 @@ public static WebApplication ConfigureImmutableShoppingCarts(this WebApplication shoppingCart.MapGet("", async Task ( HttpContext context, - IQuerySession session, + EventStoreClient eventStore, Guid shoppingCartId, CancellationToken ct) => { - var result = await session.Events.AggregateStreamAsync(shoppingCartId, token: ct); + var (result, revision) = await eventStore.GetShoppingCart(shoppingCartId, ct); - context.SetResponseEtag(result?.Version); + context.SetResponseEtag(revision); return result is not null ? Ok(result) : NotFound(); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Configure.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Configure.cs index 465373e76..cced0ec01 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Configure.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/Configure.cs @@ -1,30 +1,39 @@ -using Marten; -using OptimisticConcurrency.Core.Marten; +using EventStore.Client; +using OptimisticConcurrency.Core.EventStoreDB; using OptimisticConcurrency.Immutable.Pricing; namespace OptimisticConcurrency.Immutable.ShoppingCarts; -using static ShoppingCartEvent; public static class Configure { - private const string ModulePrefix = "immutable"; - public static IServiceCollection AddImmutableShoppingCarts(this IServiceCollection services) - { + public static IServiceCollection AddImmutableShoppingCarts(this IServiceCollection services) => services.AddSingleton(FakeProductPriceCalculator.Returning(100)); - return services; - } - public static StoreOptions ConfigureImmutableShoppingCarts(this StoreOptions options) - { - options.Projections.LiveStreamAggregation(); + public static Task GetAndUpdate( + this EventStoreClient eventStore, + Guid id, + StreamRevision expectedRevision, + Func handle, + CancellationToken ct + ) => + eventStore.GetAndUpdate( + ShoppingCart.Evolve, + ShoppingCart.Initial, + id, + expectedRevision, + handle, + ct + ); - // this is needed as we're sharing document store and have event types with the same name - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - - return options; - } + public static Task<(ShoppingCart?, StreamRevision?)> GetShoppingCart( + this EventStoreClient eventStore, + Guid id, + CancellationToken ct + ) => + eventStore.AggregateStream( + ShoppingCart.Evolve, + ShoppingCart.Initial, + id, + ct + ); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/ShoppingCart.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/ShoppingCart.cs index f47dfb136..59ea6014b 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/ShoppingCart.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Immutable/ShoppingCarts/ShoppingCart.cs @@ -49,9 +49,6 @@ public record ShoppingCart( DateTimeOffset? CanceledAt = null ) { - // Marten will set it by convention during stream aggregation - public int Version { get; set; } - public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); public bool HasEnough(PricedProductItem productItem) @@ -64,24 +61,23 @@ public bool HasEnough(PricedProductItem productItem) return currentQuantity >= quantity; } - public static ShoppingCart Default() => + public static ShoppingCart Initial() => new (default, default, default, [], default); - public ShoppingCart Apply(ShoppingCartEvent @event) - { - return @event switch + public static ShoppingCart Evolve(ShoppingCart state, ShoppingCartEvent @event) => + @event switch { ShoppingCartOpened(var shoppingCartId, var clientId, _) => - this with + state with { Id = shoppingCartId, ClientId = clientId, Status = ShoppingCartStatus.Pending }, ProductItemAddedToShoppingCart(_, var pricedProductItem, _) => - this with + state with { - ProductItems = ProductItems + ProductItems = state.ProductItems .Concat(new [] { pricedProductItem }) .GroupBy(pi => pi.ProductId) .Select(group => group.Count() == 1? @@ -95,9 +91,9 @@ this with .ToArray() }, ProductItemRemovedFromShoppingCart(_, var pricedProductItem, _) => - this with + state with { - ProductItems = ProductItems + ProductItems = state.ProductItems .Select(pi => pi.ProductId == pricedProductItem.ProductId? pi with { Quantity = pi.Quantity - pricedProductItem.Quantity } :pi @@ -106,23 +102,19 @@ this with .ToArray() }, ShoppingCartConfirmed(_, var confirmedAt) => - this with + state with { Status = ShoppingCartStatus.Confirmed, ConfirmedAt = confirmedAt }, ShoppingCartCanceled(_, var canceledAt) => - this with + state with { Status = ShoppingCartStatus.Canceled, CanceledAt = canceledAt }, - _ => this + _ => state }; - } - - // let's make Marten happy - private ShoppingCart():this(Guid.Empty, Guid.Empty, ShoppingCartStatus.Pending, [], default){} } public enum ShoppingCartStatus diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Api.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Api.cs index 8675af45e..a46cc7171 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Api.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Api.cs @@ -1,9 +1,8 @@ using Core.Validation; -using Marten; -using Marten.Schema.Identity; +using EventStore.Client; using Microsoft.AspNetCore.Mvc; +using OptimisticConcurrency.Core.EventStoreDB; using OptimisticConcurrency.Core.Http; -using OptimisticConcurrency.Core.Marten; using OptimisticConcurrency.Mixed.Pricing; using static Microsoft.AspNetCore.Http.TypedResults; using static System.DateTimeOffset; @@ -23,17 +22,17 @@ public static WebApplication ConfigureMixedShoppingCarts(this WebApplication app shoppingCarts.MapPost("", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid clientId, CancellationToken ct ) => { - var shoppingCartId = CombGuidIdGeneration.NewGuid(); + var shoppingCartId = Uuid.NewUuid().ToGuid(); - var nextExpectedVersion = await session.Add(shoppingCartId, - [MixedShoppingCart.Open(shoppingCartId, clientId.NotEmpty(), Now).Item1], ct); + var nextExpectedRevision = await eventStore.Add(shoppingCartId, + [ShoppingCart.Open(shoppingCartId, clientId.NotEmpty(), Now).Item1], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return Created($"/api/mixed/clients/{clientId}/shopping-carts/{shoppingCartId}", shoppingCartId); } @@ -43,7 +42,7 @@ CancellationToken ct async ( HttpContext context, IProductPriceCalculator pricingCalculator, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, AddProductRequest body, [FromIfMatchHeader] string eTag, @@ -51,10 +50,10 @@ CancellationToken ct { var productItem = body.ProductItem.NotNull().ToProductItem(); - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [state.AddProduct(pricingCalculator, productItem, Now)], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -63,7 +62,7 @@ CancellationToken ct productItems.MapDelete("{productId:guid}", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromRoute] Guid productId, [FromQuery] int? quantity, @@ -78,11 +77,11 @@ CancellationToken ct UnitPrice = unitPrice.NotNull().Positive() }; - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [state.RemoveProduct(productItem, Now)], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -91,15 +90,15 @@ CancellationToken ct shoppingCart.MapPost("confirm", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromIfMatchHeader] string eTag, CancellationToken ct) => { - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [state.Confirm(Now)], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -108,15 +107,15 @@ CancellationToken ct shoppingCart.MapDelete("", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromIfMatchHeader] string eTag, CancellationToken ct) => { - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => [state.Cancel(Now)], ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -125,13 +124,13 @@ CancellationToken ct shoppingCart.MapGet("", async Task ( HttpContext context, - IQuerySession session, + EventStoreClient eventStore, Guid shoppingCartId, CancellationToken ct) => { - var result = await session.Events.AggregateStreamAsync(shoppingCartId, token: ct); + var (result, revision) = await eventStore.GetShoppingCart(shoppingCartId, ct); - context.SetResponseEtag(result?.Version); + context.SetResponseEtag(revision); return result is not null ? Ok(result) : NotFound(); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Configure.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Configure.cs index d54e7f47a..861d06b89 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Configure.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/Configure.cs @@ -1,31 +1,47 @@ -using Marten; -using OptimisticConcurrency.Core.Marten; +using EventStore.Client; +using OptimisticConcurrency.Core.EventStoreDB; using OptimisticConcurrency.Mixed.Pricing; namespace OptimisticConcurrency.Mixed.ShoppingCarts; -using static ShoppingCartEvent; public static class Configure { - private const string ModulePrefix = "Mixed"; - - public static IServiceCollection AddMixedShoppingCarts(this IServiceCollection services) - { + public static IServiceCollection AddMixedShoppingCarts(this IServiceCollection services) => services.AddSingleton(FakeProductPriceCalculator.Returning(100)); - return services; - } - public static StoreOptions ConfigureMixedShoppingCarts(this StoreOptions options) - { - options.Projections.LiveStreamAggregation(); - - // this is needed as we're sharing document store and have event types with the same name - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); + public static Task GetAndUpdate( + this EventStoreClient eventStore, + Guid id, + StreamRevision expectedRevision, + Func handle, + CancellationToken ct + ) => + eventStore.GetAndUpdate( + (state, @event) => + { + state.Evolve(@event); + return state; + }, + ShoppingCart.Initial, + id, + expectedRevision, + handle, + ct + ); - return options; - } + public static Task<(ShoppingCart?, StreamRevision?)> GetShoppingCart( + this EventStoreClient eventStore, + Guid id, + CancellationToken ct + ) => + eventStore.AggregateStream( + (state, @event) => + { + state.Evolve(@event); + return state; + }, + ShoppingCart.Initial, + id, + ct + ); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/MixedShoppingCart.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/ShoppingCart.cs similarity index 93% rename from Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/MixedShoppingCart.cs rename to Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/ShoppingCart.cs index 79eeabccb..68ba85a55 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/MixedShoppingCart.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mixed/ShoppingCarts/ShoppingCart.cs @@ -41,7 +41,7 @@ DateTimeOffset CanceledAt // ENTITY // Note: We need to have prefix to be able to register multiple streams with the same name -public class MixedShoppingCart +public class ShoppingCart { public Guid Id { get; private set; } public Guid ClientId { get; private set; } @@ -49,12 +49,10 @@ public class MixedShoppingCart public IList ProductItems { get; } = new List(); public DateTimeOffset? ConfirmedAt { get; private set; } public DateTimeOffset? CanceledAt { get; private set; } - // Marten will set it by convention during stream aggregation - public int Version { get; set; } public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); - public void Apply(ShoppingCartEvent @event) + public void Evolve(ShoppingCartEvent @event) { switch (@event) { @@ -76,7 +74,7 @@ public void Apply(ShoppingCartEvent @event) } } - public static (ShoppingCartOpened, MixedShoppingCart) Open( + public static (ShoppingCartOpened, ShoppingCart) Open( Guid cartId, Guid clientId, DateTimeOffset now @@ -88,16 +86,16 @@ DateTimeOffset now now ); - return (@event, new MixedShoppingCart(@event)); + return (@event, new ShoppingCart(@event)); } - public static MixedShoppingCart Initial() => new(); + public static ShoppingCart Initial() => new(); - private MixedShoppingCart(ShoppingCartOpened @event) => + private ShoppingCart(ShoppingCartOpened @event) => Apply(@event); //just for default creation of empty object - private MixedShoppingCart() { } + private ShoppingCart() { } private void Apply(ShoppingCartOpened opened) { diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Api.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Api.cs index c69db679d..bc3873288 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Api.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Api.cs @@ -1,9 +1,8 @@ using Core.Validation; -using Marten; -using Marten.Schema.Identity; +using EventStore.Client; using Microsoft.AspNetCore.Mvc; +using OptimisticConcurrency.Core.EventStoreDB; using OptimisticConcurrency.Core.Http; -using OptimisticConcurrency.Core.Marten; using OptimisticConcurrency.Mutable.Pricing; using static Microsoft.AspNetCore.Http.TypedResults; using static System.DateTimeOffset; @@ -24,16 +23,16 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a shoppingCarts.MapPost("", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid clientId, CancellationToken ct) => { - var shoppingCartId = CombGuidIdGeneration.NewGuid(); + var shoppingCartId = Uuid.NewUuid().ToGuid(); - var nextExpectedVersion = await session.Add(shoppingCartId, - MutableShoppingCart.Open(shoppingCartId, clientId.NotEmpty(), Now).DequeueUncommittedEvents(), ct); + var nextExpectedRevision = await eventStore.Add(shoppingCartId, + ShoppingCart.Open(shoppingCartId, clientId.NotEmpty(), Now), ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return Created($"/api/mutable/clients/{clientId}/shopping-carts/{shoppingCartId}", shoppingCartId); } @@ -43,7 +42,7 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a async ( HttpContext context, IProductPriceCalculator pricingCalculator, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, AddProductRequest body, [FromIfMatchHeader] string eTag, @@ -51,10 +50,10 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a { var productItem = body.ProductItem.NotNull().ToProductItem(); - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => state.AddProduct(pricingCalculator, productItem, Now), ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -63,7 +62,7 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a productItems.MapDelete("{productId:guid}", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromRoute] Guid productId, [FromQuery] int? quantity, @@ -78,11 +77,11 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a UnitPrice = unitPrice.NotNull().Positive() }; - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => state.RemoveProduct(productItem, Now), ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -91,15 +90,15 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a shoppingCart.MapPost("confirm", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromIfMatchHeader] string eTag, CancellationToken ct) => { - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => state.Confirm(Now), ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -108,15 +107,15 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a shoppingCart.MapDelete("", async ( HttpContext context, - IDocumentSession session, + EventStoreClient eventStore, Guid shoppingCartId, [FromIfMatchHeader] string eTag, CancellationToken ct) => { - var nextExpectedVersion = await session.GetAndUpdate(shoppingCartId, ToExpectedVersion(eTag), + var nextExpectedRevision = await eventStore.GetAndUpdate(shoppingCartId, ToExpectedRevision(eTag), state => state.Cancel(Now), ct); - context.SetResponseEtag(nextExpectedVersion); + context.SetResponseEtag(nextExpectedRevision); return NoContent(); } @@ -125,13 +124,13 @@ public static WebApplication ConfigureMutableShoppingCarts(this WebApplication a shoppingCart.MapGet("", async Task ( HttpContext context, - IQuerySession session, + EventStoreClient eventStore, Guid shoppingCartId, CancellationToken ct) => { - var result = await session.Events.AggregateStreamAsync(shoppingCartId, token: ct); + var (result, revision) = await eventStore.GetShoppingCart(shoppingCartId, ct); - context.SetResponseEtag(result?.Version); + context.SetResponseEtag(revision); return result is not null ? Ok(result) : NotFound(); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Configure.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Configure.cs index eea90a02b..9bd72fa3d 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Configure.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/Configure.cs @@ -1,31 +1,37 @@ -using Marten; -using OptimisticConcurrency.Core.Marten; +using EventStore.Client; +using OptimisticConcurrency.Core.EventStoreDB; using OptimisticConcurrency.Mutable.Pricing; namespace OptimisticConcurrency.Mutable.ShoppingCarts; -using static ShoppingCartEvent; public static class Configure { - private const string ModulePrefix = "mutable"; - - public static IServiceCollection AddMutableShoppingCarts(this IServiceCollection services) - { + public static IServiceCollection AddMutableShoppingCarts(this IServiceCollection services) => services.AddSingleton(FakeProductPriceCalculator.Returning(100)); - return services; - } - public static StoreOptions ConfigureMutableShoppingCarts(this StoreOptions options) - { - options.Projections.LiveStreamAggregation(); - - // this is needed as we're sharing document store and have event types with the same name - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); - options.MapEventWithPrefix(ModulePrefix); + public static Task GetAndUpdate( + this EventStoreClient eventStore, + Guid id, + StreamRevision expectedRevision, + Action handle, + CancellationToken ct + ) => + eventStore.GetAndUpdate( + ShoppingCart.Initial, + id, + expectedRevision, + handle, + ct + ); - return options; - } + public static Task<(ShoppingCart?, StreamRevision?)> GetShoppingCart( + this EventStoreClient eventStore, + Guid id, + CancellationToken ct + ) => + eventStore.AggregateStream( + ShoppingCart.Initial, + id, + ct + ); } diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/MutableShoppingCart.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/ShoppingCart.cs similarity index 93% rename from Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/MutableShoppingCart.cs rename to Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/ShoppingCart.cs index fe0582bac..e2e044a0e 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/MutableShoppingCart.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Mutable/ShoppingCarts/ShoppingCart.cs @@ -42,19 +42,17 @@ DateTimeOffset CanceledAt // ENTITY // Note: We need to have prefix to be able to register multiple streams with the same name -public class MutableShoppingCart: Aggregate +public class ShoppingCart: Aggregate { public Guid ClientId { get; private set; } public ShoppingCartStatus Status { get; private set; } public IList ProductItems { get; } = new List(); public DateTimeOffset? ConfirmedAt { get; private set; } public DateTimeOffset? CanceledAt { get; private set; } - // Marten will set it by convention during stream aggregation - public int Version { get; set; } public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); - protected override void Apply(ShoppingCartEvent @event) + public override void Evolve(ShoppingCartEvent @event) { switch (@event) { @@ -76,18 +74,18 @@ protected override void Apply(ShoppingCartEvent @event) } } - public static MutableShoppingCart Open( + public static ShoppingCart Open( Guid cartId, Guid clientId, DateTimeOffset now ) { - return new MutableShoppingCart(cartId, clientId, now); + return new ShoppingCart(cartId, clientId, now); } - public static MutableShoppingCart Initial() => new(); + public static ShoppingCart Initial() => new(); - private MutableShoppingCart( + private ShoppingCart( Guid id, Guid clientId, DateTimeOffset now @@ -104,7 +102,7 @@ DateTimeOffset now } //just for default creation of empty object - private MutableShoppingCart() { } + private ShoppingCart() { } private void Apply(ShoppingCartOpened opened) { diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Program.cs b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Program.cs index 9a108945e..1a53ab277 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Program.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/Program.cs @@ -1,7 +1,6 @@ -using Marten; -using Marten.Exceptions; +using Core.Configuration; +using EventStore.Client; using Microsoft.AspNetCore.Diagnostics; -using Oakton; using OptimisticConcurrency.Core.Exceptions; using OptimisticConcurrency.Immutable.ShoppingCarts; using OptimisticConcurrency.Mixed.ShoppingCarts; @@ -9,6 +8,10 @@ var builder = WebApplication.CreateBuilder(args); +var eventStoreClient = new EventStoreClient( + EventStoreClientSettings.Create(builder.Configuration.GetRequiredConnectionString(("ShoppingCarts"))) +); + builder.Services .AddRouting() .AddEndpointsApiExplorer() @@ -16,23 +19,7 @@ .AddMutableShoppingCarts() .AddMixedShoppingCarts() .AddImmutableShoppingCarts() - .AddMarten(options => - { - var schemaName = Environment.GetEnvironmentVariable("SchemaName") ?? "Workshop_Optimistic_ShoppingCarts_Solved"; - options.Events.DatabaseSchemaName = schemaName; - options.DatabaseSchemaName = schemaName; - options.Connection(builder.Configuration.GetConnectionString("ShoppingCarts") ?? - throw new InvalidOperationException()); - - options.ConfigureImmutableShoppingCarts() - .ConfigureMutableShoppingCarts() - .ConfigureMixedShoppingCarts(); - }) - .ApplyAllDatabaseChangesOnStartup() - .OptimizeArtifactWorkflow() - .UseLightweightSessions(); - -builder.Host.ApplyOaktonExtensions(); + .AddSingleton(eventStoreClient); var app = builder.Build(); @@ -54,7 +41,7 @@ { Message: "Required parameter \"string eTag\" was not provided from header." } => StatusCodes.Status412PreconditionFailed, - ConcurrencyException=> StatusCodes.Status412PreconditionFailed, + WrongExpectedVersionException => StatusCodes.Status412PreconditionFailed, _ => StatusCodes.Status500InternalServerError, }; @@ -73,7 +60,7 @@ .UseSwaggerUI(); } -return await app.RunOaktonCommands(args); +await app.RunAsync(); namespace OptimisticConcurrency { diff --git a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/appsettings.json b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/appsettings.json index 26bccde8b..0ddc0b1d1 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/appsettings.json +++ b/Workshops/IntroductionToEventSourcing/Solved/11-OptimisticConcurrency.EventStoreDB/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "ShoppingCarts": "PORT = 5432; HOST = localhost; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'" + "ShoppingCarts": "esdb://localhost:2113?tls=false" }, "KafkaProducer": { "ProducerConfig": {