diff --git a/EventSourcing.NetCore.slnx b/EventSourcing.NetCore.slnx index 0a3878d1f..427336a06 100644 --- a/EventSourcing.NetCore.slnx +++ b/EventSourcing.NetCore.slnx @@ -251,9 +251,15 @@ - - - + + + + + + + + + @@ -278,9 +284,15 @@ - - - + + + + + + + + + diff --git a/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/15-Projections.MultiStream.csproj b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/15-Projections.MultiStream.csproj new file mode 100644 index 000000000..156a21617 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/15-Projections.MultiStream.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/ProjectionsTests.cs new file mode 100644 index 000000000..79718f7f9 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/ProjectionsTests.cs @@ -0,0 +1,131 @@ +using FluentAssertions; +using IntroductionToEventSourcing.Projections.MultiStream.Tools; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +public record PaymentVerificationCompleted( + Guid PaymentId, + bool IsApproved +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public PaymentStatus Status { get; set; } +} + +public class ProjectionsTests +{ + [Fact] + [Trait("Category", "SkipCI")] + public void MultiStreamProjection_ForPaymentVerification_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + + var eventStore = new EventStore(); + var database = new Database(); + + // TODO: + // 1. Create a PaymentVerificationProjection class that handles each event type. + // Events arrive on different streams (payment, merchant, fraud check), + // but they share PaymentId — use PaymentId as the read model key. + // 2. Register your event handlers using `eventStore.Register`. + + // Payment 1: Approved — all checks pass + eventStore.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + eventStore.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + eventStore.Append(payment1Id, new PaymentVerificationCompleted(payment1Id, true)); + + // Payment 2: Merchant rejected — exceeds merchant limits + eventStore.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + eventStore.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + eventStore.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + eventStore.Append(payment2Id, new PaymentVerificationCompleted(payment2Id, false)); + + // Payment 3: Fraud rejected — high fraud score + eventStore.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + eventStore.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + eventStore.Append(payment3Id, new PaymentVerificationCompleted(payment3Id, false)); + + // Payment 4: Pending — still awaiting fraud check and final decision + eventStore.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + // Assert Payment 1: Approved + var payment1 = database.Get(payment1Id)!; + payment1.Should().NotBeNull(); + payment1.Id.Should().Be(payment1Id); + payment1.Status.Should().Be(PaymentStatus.Approved); + + // Assert Payment 2: Merchant rejected + var payment2 = database.Get(payment2Id)!; + payment2.Should().NotBeNull(); + payment2.Id.Should().Be(payment2Id); + payment2.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 3: Fraud rejected + var payment3 = database.Get(payment3Id)!; + payment3.Should().NotBeNull(); + payment3.Id.Should().Be(payment3Id); + payment3.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 4: Pending + var payment4 = database.Get(payment4Id)!; + payment4.Should().NotBeNull(); + payment4.Id.Should().Be(payment4Id); + payment4.Status.Should().Be(PaymentStatus.Pending); + } +} diff --git a/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/README.md b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/README.md new file mode 100644 index 000000000..2323fee96 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/README.md @@ -0,0 +1,29 @@ +# Exercise 15 - Multi-Stream Projections + +In exercises 12-14 you built projections from a single event stream. Now you'll combine events from **multiple streams** into a single read model. + +## Scenario: Payment Verification + +A payment verification requires data from three independent checks, each producing events on its own stream: + +1. **Payment recorded** — from the payment service (amount, order reference) +2. **Merchant limits checked** — from the merchant service (within daily limits?) +3. **Fraud score calculated** — from the fraud detection service (risk score, acceptable?) +4. **Verification completed** — final decision event (approved or rejected) + +All events share a `PaymentId` that ties them to the same payment verification read model. + +## What to implement + +With the [Database](./Tools/Database.cs) interface representing the sample database, implement a `PaymentVerification` read model and projection: + +1. Define the `PaymentVerification` read model properties — the test assertions tell you what shape it needs. +2. Create a `PaymentVerificationProjection` class with typed `Handle` methods for each event. +3. Register handlers in the test using `eventStore.Register`. + +The key difference from single-stream projections: each event arrives on a **different stream ID**, but they all reference the same `PaymentId`. Your projection must use `PaymentId` (not the stream ID) as the read model key. + +## Reference + +Read more about multi-stream projections and handling events from multiple sources: +- [Handling Events Coming in an Unknown Order](https://www.architecture-weekly.com/p/handling-events-coming-in-an-unknown) diff --git a/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/Database.cs b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/Database.cs new file mode 100644 index 000000000..c80ac405a --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/Database.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace IntroductionToEventSourcing.Projections.MultiStream.Tools; + +public class Database +{ + private readonly Dictionary storage = new(); + + public void Store(Guid id, T obj) where T: class => + storage[GetId(id)] = obj; + + public void Delete(Guid id) => + storage.Remove(GetId(id)); + + public T? Get(Guid id) where T: class => + storage.TryGetValue(GetId(id), out var result) ? + // Clone to simulate getting new instance on loading + JsonSerializer.Deserialize(JsonSerializer.Serialize((T)result)) + : null; + + private static string GetId(Guid id) => $"{typeof(T).Name}-{id}"; +} diff --git a/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/EventEnvelope.cs new file mode 100644 index 000000000..613642c97 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/EventEnvelope.cs @@ -0,0 +1,24 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.Tools; + +public record EventMetadata( + string EventId, + ulong StreamPosition, + ulong LogPosition +) +{ + public static EventMetadata For(ulong streamPosition, ulong logPosition) => + new(Guid.CreateVersion7().ToString(), streamPosition, logPosition); +} + +public record EventEnvelope( + object Data, + EventMetadata Metadata +); + +public record EventEnvelope( + T Data, + EventMetadata Metadata +): EventEnvelope(Data, Metadata) where T : notnull +{ + public new T Data => (T)base.Data; +} diff --git a/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/EventStore.cs b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/EventStore.cs new file mode 100644 index 000000000..ed4bed2eb --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/Tools/EventStore.cs @@ -0,0 +1,41 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.Tools; + +public class EventStore +{ + private readonly Dictionary>> handlers = new(); + private readonly Dictionary> events = new(); + + public void Register(Action> handler) where TEvent : notnull + { + var eventType = typeof(TEvent); + + void WrappedHandler(object @event) => handler((EventEnvelope)@event); + + if (handlers.TryGetValue(eventType, out var handler1)) + handler1.Add(WrappedHandler); + else + handlers.Add(eventType, [WrappedHandler]); + } + + public void Append(Guid streamId, TEvent @event) where TEvent : notnull + { + if (!events.ContainsKey(streamId)) + events[streamId] = []; + + var eventEnvelope = new EventEnvelope(@event, + EventMetadata.For( + (ulong)events[streamId].Count + 1, + (ulong)events.Values.Sum(s => s.Count) + ) + ); + + events[streamId].Add(eventEnvelope); + + if (!handlers.TryGetValue(eventEnvelope.Data.GetType(), out var eventHandlers)) return; + + foreach (var handle in eventHandlers) + { + handle(eventEnvelope); + } + } +} diff --git a/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/16-Projections.MultiStream.OutOfOrder.csproj b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/16-Projections.MultiStream.OutOfOrder.csproj new file mode 100644 index 000000000..600056afb --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/16-Projections.MultiStream.OutOfOrder.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/ProjectionsTests.cs new file mode 100644 index 000000000..4150b847b --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/ProjectionsTests.cs @@ -0,0 +1,134 @@ +using FluentAssertions; +using IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +public enum DataQuality +{ + Partial, + Sufficient, + Complete +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public PaymentStatus Status { get; set; } +} + +public class ProjectionsTests +{ + [Fact] + [Trait("Category", "SkipCI")] + public void MultiStreamProjection_WithOutOfOrderEvents_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + var fraudCheck4Id = Guid.CreateVersion7(); + + var eventStore = new EventStore(); + var database = new Database(); + + // TODO: + // 1. Create a PaymentVerificationProjection class that handles each event type. + // 2. Each handler must work even if events arrive out of order (e.g., fraud score before payment). + // 3. The projection should derive the Status based on available data: + // - Pending: waiting for required data + // - Rejected: merchant limits failed OR fraud score unacceptable + // - Approved: all checks passed + // 4. Register your event handlers using `eventStore.Register`. + + // Payment 1: Approved — events arrive out of order (fraud score first!) + eventStore.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + eventStore.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + + // Payment 2: Merchant rejected — merchant check arrives first + eventStore.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + eventStore.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + eventStore.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + + // Payment 3: Fraud rejected — payment recorded last + eventStore.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + eventStore.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + + // Payment 4: Pending — missing fraud check (payment recorded, merchant checked, but no fraud score yet) + eventStore.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + // Assert Payment 1: Approved (all data arrived, all checks passed) + var payment1 = database.Get(payment1Id)!; + payment1.Should().NotBeNull(); + payment1.Id.Should().Be(payment1Id); + payment1.Status.Should().Be(PaymentStatus.Approved); + + // Assert Payment 2: Merchant rejected + var payment2 = database.Get(payment2Id)!; + payment2.Should().NotBeNull(); + payment2.Id.Should().Be(payment2Id); + payment2.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 3: Fraud rejected + var payment3 = database.Get(payment3Id)!; + payment3.Should().NotBeNull(); + payment3.Id.Should().Be(payment3Id); + payment3.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 4: Pending (waiting for fraud check) + var payment4 = database.Get(payment4Id)!; + payment4.Should().NotBeNull(); + payment4.Id.Should().Be(payment4Id); + payment4.Status.Should().Be(PaymentStatus.Pending); + } +} diff --git a/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/README.md b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/README.md new file mode 100644 index 000000000..674b24ae9 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/README.md @@ -0,0 +1,36 @@ +# Exercise 16 - Multi-Stream Projections with Out-of-Order Events + +In exercise 15 you built multi-stream projections assuming events arrive in a predictable order. The real world isn't that kind. Events arrive **in any order**, especially when they come from different services. + +## Scenario: Payment Verification with Race Conditions + +The same payment verification domain, but now events arrive scrambled: + +1. **FraudScoreCalculated** might arrive before **PaymentRecorded** +2. **MerchantLimitsChecked** could be first or last +3. No **PaymentVerificationCompleted** event — your projection derives the decision when enough data arrives + +This teaches you to build **resilient read models** using the phantom record pattern. + +## Key Differences from Exercise 15 + +1. **No final decision event** — the projection determines approval/rejection based on available data +2. **Handle partial state** — the read model exists even with incomplete information +3. **Derive status** — when you have all required data, calculate the final status + +## What to implement + +With the [Database](./Tools/Database.cs) interface representing the sample database, implement a resilient `PaymentVerification` projection: + +1. Define additional `PaymentVerification` properties to store data from each event — but design it to handle missing data +2. Create a `PaymentVerificationProjection` class with typed `Handle` methods for each event +3. Each handler should work even if other events haven't arrived yet +4. When enough data exists, derive the final `Status` (Approved/Rejected/Pending) +5. Register handlers in the test using `eventStore.Register` + +The test will append events **in scrambled order** and verify your projection handles partial state correctly. + +## Reference + +Read more about handling out-of-order events and phantom records: +- [Dealing with Race Conditions in Event-Driven Architecture](https://www.architecture-weekly.com/p/dealing-with-race-conditions-in-event) diff --git a/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/Database.cs b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/Database.cs new file mode 100644 index 000000000..85ac1ebc4 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/Database.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; + +public class Database +{ + private readonly Dictionary storage = new(); + + public void Store(Guid id, T obj) where T: class => + storage[GetId(id)] = obj; + + public void Delete(Guid id) => + storage.Remove(GetId(id)); + + public T? Get(Guid id) where T: class => + storage.TryGetValue(GetId(id), out var result) ? + // Clone to simulate getting new instance on loading + JsonSerializer.Deserialize(JsonSerializer.Serialize((T)result)) + : null; + + private static string GetId(Guid id) => $"{typeof(T).Name}-{id}"; +} diff --git a/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/EventEnvelope.cs new file mode 100644 index 000000000..033a300c8 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/EventEnvelope.cs @@ -0,0 +1,24 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; + +public record EventMetadata( + string EventId, + ulong StreamPosition, + ulong LogPosition +) +{ + public static EventMetadata For(ulong streamPosition, ulong logPosition) => + new(Guid.CreateVersion7().ToString(), streamPosition, logPosition); +} + +public record EventEnvelope( + object Data, + EventMetadata Metadata +); + +public record EventEnvelope( + T Data, + EventMetadata Metadata +): EventEnvelope(Data, Metadata) where T : notnull +{ + public new T Data => (T)base.Data; +} diff --git a/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/EventStore.cs b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/EventStore.cs new file mode 100644 index 000000000..e80209f2d --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/16-Projections.MultiStream.OutOfOrder/Tools/EventStore.cs @@ -0,0 +1,41 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; + +public class EventStore +{ + private readonly Dictionary>> handlers = new(); + private readonly Dictionary> events = new(); + + public void Register(Action> handler) where TEvent : notnull + { + var eventType = typeof(TEvent); + + void WrappedHandler(object @event) => handler((EventEnvelope)@event); + + if (handlers.TryGetValue(eventType, out var handler1)) + handler1.Add(WrappedHandler); + else + handlers.Add(eventType, [WrappedHandler]); + } + + public void Append(Guid streamId, TEvent @event) where TEvent : notnull + { + if (!events.ContainsKey(streamId)) + events[streamId] = []; + + var eventEnvelope = new EventEnvelope(@event, + EventMetadata.For( + (ulong)events[streamId].Count + 1, + (ulong)events.Values.Sum(s => s.Count) + ) + ); + + events[streamId].Add(eventEnvelope); + + if (!handlers.TryGetValue(eventEnvelope.Data.GetType(), out var eventHandlers)) return; + + foreach (var handle in eventHandlers) + { + handle(eventEnvelope); + } + } +} diff --git a/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/17-Projections.MultiStream.Marten.csproj b/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/17-Projections.MultiStream.Marten.csproj new file mode 100644 index 000000000..1abd0cec2 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/17-Projections.MultiStream.Marten.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream.Marten + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/ProjectionsTests.cs new file mode 100644 index 000000000..1fb6ad81d --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/ProjectionsTests.cs @@ -0,0 +1,143 @@ +using FluentAssertions; +using JasperFx; +using Marten; +using Marten.Events.Projections; +using Weasel.Core; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream.Marten; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +public record PaymentVerificationCompleted( + Guid PaymentId, + bool IsApproved +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public PaymentStatus Status { get; set; } +} + +public class ProjectionsTests +{ + private const string ConnectionString = + "PORT = 5432; HOST = localhost; TIMEOUT = 15; POOLING = True; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"; + + [Fact] + [Trait("Category", "SkipCI")] + public async Task MultiStreamProjection_WithMarten_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + + await using var documentStore = DocumentStore.For(options => + { + options.Connection(ConnectionString); + options.DatabaseSchemaName = options.Events.DatabaseSchemaName = "Exercise17MultiStreamMarten"; + options.AutoCreateSchemaObjects = AutoCreate.All; + + // TODO: + // 1. Create a PaymentVerificationProjection class that inherits from MultiStreamProjection + // 2. Register the projection here using: options.Projections.Add(ProjectionLifecycle.Inline); + }); + + await using var session = documentStore.LightweightSession(); + + // Payment 1: Approved — all checks pass + session.Events.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + session.Events.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + session.Events.Append(payment1Id, new PaymentVerificationCompleted(payment1Id, true)); + + // Payment 2: Merchant rejected — exceeds merchant limits + session.Events.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + session.Events.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + session.Events.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + session.Events.Append(payment2Id, new PaymentVerificationCompleted(payment2Id, false)); + + // Payment 3: Fraud rejected — high fraud score + session.Events.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + session.Events.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + session.Events.Append(payment3Id, new PaymentVerificationCompleted(payment3Id, false)); + + // Payment 4: Pending — still awaiting fraud check and final decision + session.Events.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + await session.SaveChangesAsync(); + + // Assert Payment 1: Approved + var payment1 = await session.LoadAsync(payment1Id); + payment1.Should().NotBeNull(); + payment1!.Id.Should().Be(payment1Id); + payment1.Status.Should().Be(PaymentStatus.Approved); + + // Assert Payment 2: Merchant rejected + var payment2 = await session.LoadAsync(payment2Id); + payment2.Should().NotBeNull(); + payment2!.Id.Should().Be(payment2Id); + payment2.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 3: Fraud rejected + var payment3 = await session.LoadAsync(payment3Id); + payment3.Should().NotBeNull(); + payment3!.Id.Should().Be(payment3Id); + payment3.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 4: Pending + var payment4 = await session.LoadAsync(payment4Id); + payment4.Should().NotBeNull(); + payment4!.Id.Should().Be(payment4Id); + payment4.Status.Should().Be(PaymentStatus.Pending); + } +} diff --git a/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/README.md b/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/README.md new file mode 100644 index 000000000..e06196198 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/17-Projections.MultiStream.Marten/README.md @@ -0,0 +1,41 @@ +# Exercise 17 - Multi-Stream Projections with Marten + +In exercises 15-16 you built multi-stream projections manually. Now you'll use Marten's `MultiStreamProjection` to handle the complexity for you. + +## Scenario: Payment Verification with Marten + +The same payment verification domain from exercise 15, but using Marten's built-in multi-stream projection support. + +## What to implement + +Implement a `PaymentVerificationProjection` using Marten's `MultiStreamProjection`: + +1. Inherit from `MultiStreamProjection` +2. In the constructor, use `Identity` to specify how each event type maps to the `PaymentId` +3. Define `Apply` methods for each event type to update the read model +4. Register the projection with Marten's projection system in the test + +## Marten Multi-Stream Projection Pattern + +```csharp +public class YourProjection: MultiStreamProjection +{ + public YourProjection() + { + // Tell Marten which property on each event contains the document ID + Identity(e => e.DocumentId); + Identity(e => e.DocumentId); + } + + // Define how each event updates the read model + public void Apply(YourReadModel model, Event1 @event) + { + // Update model based on event + } +} +``` + +##Reference + +Read more about Marten's multi-stream projections: +- [Marten Multi-Stream Projections](https://martendb.io/events/projections/multi-stream-projections.html) diff --git a/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/18-Projections.MultiStream.OutOfOrder.Marten.csproj b/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/18-Projections.MultiStream.OutOfOrder.Marten.csproj new file mode 100644 index 000000000..7566c70b7 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/18-Projections.MultiStream.OutOfOrder.Marten.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Marten + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/ProjectionsTests.cs new file mode 100644 index 000000000..7713b53e3 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/ProjectionsTests.cs @@ -0,0 +1,144 @@ +using FluentAssertions; +using JasperFx; +using Marten; +using Marten.Events.Projections; +using Weasel.Core; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Marten; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +public enum DataQuality +{ + Partial, + Sufficient, + Complete +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public PaymentStatus Status { get; set; } +} + +public class ProjectionsTests +{ + private const string ConnectionString = + "PORT = 5432; HOST = localhost; TIMEOUT = 15; POOLING = True; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"; + + [Fact] + [Trait("Category", "SkipCI")] + public async Task MultiStreamProjection_WithOutOfOrderEventsAndMarten_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + + await using var documentStore = DocumentStore.For(options => + { + options.Connection(ConnectionString); + options.DatabaseSchemaName = options.Events.DatabaseSchemaName = "Exercise18MultiStreamOutOfOrderMarten"; + options.AutoCreateSchemaObjects = AutoCreate.All; + + // TODO: + // 1. Create a PaymentVerificationProjection class that inherits from MultiStreamProjection + // 2. Each Apply method must work even if events arrive out of order + // 3. Derive the Status based on available data in each Apply method + // 4. Register the projection here using: options.Projections.Add(ProjectionLifecycle.Inline); + }); + + await using var session = documentStore.LightweightSession(); + + // Payment 1: Approved — events arrive out of order (fraud score first!) + session.Events.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + session.Events.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + + // Payment 2: Merchant rejected — merchant check arrives first + session.Events.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + session.Events.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + session.Events.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + + // Payment 3: Fraud rejected — payment recorded last + session.Events.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + session.Events.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + + // Payment 4: Pending — missing fraud check (payment recorded, merchant checked, but no fraud score yet) + session.Events.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + await session.SaveChangesAsync(); + + // Assert Payment 1: Approved (all data arrived, all checks passed) + var payment1 = await session.LoadAsync(payment1Id); + payment1.Should().NotBeNull(); + payment1!.Id.Should().Be(payment1Id); + payment1.Status.Should().Be(PaymentStatus.Approved); + + // Assert Payment 2: Merchant rejected + var payment2 = await session.LoadAsync(payment2Id); + payment2.Should().NotBeNull(); + payment2!.Id.Should().Be(payment2Id); + payment2.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 3: Fraud rejected + var payment3 = await session.LoadAsync(payment3Id); + payment3.Should().NotBeNull(); + payment3!.Id.Should().Be(payment3Id); + payment3.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 4: Pending (waiting for fraud check) + var payment4 = await session.LoadAsync(payment4Id); + payment4.Should().NotBeNull(); + payment4!.Id.Should().Be(payment4Id); + payment4.Status.Should().Be(PaymentStatus.Pending); + } +} diff --git a/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/README.md b/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/README.md new file mode 100644 index 000000000..ba69950fa --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/18-Projections.MultiStream.OutOfOrder.Marten/README.md @@ -0,0 +1,33 @@ +# Exercise 18 - Multi-Stream Projections with Out-of-Order Events (Marten) + +Combining exercises 16 and 17: handle out-of-order events from multiple streams using Marten's `MultiStreamProjection`. + +## Scenario: Payment Verification with Out-of-Order Events (Marten) + +Events arrive in any order, and you need a resilient projection that: +- Handles partial state +- Derives decisions from available data +- Uses Marten's multi-stream projection capabilities + +## Key Differences from Exercise 17 + +1. **No final decision event** — the projection determines approval/rejection based on available data +2. **Handle partial state** — the read model exists even with incomplete information +3. **Derive status** — when you have all required data, calculate the final status using Marten + +## What to implement + +Implement a resilient `PaymentVerificationProjection` using Marten's `MultiStreamProjection`: + +1. Inherit from `MultiStreamProjection` +2. Use `Identity` to map each event to the PaymentId +3. Define `Apply` methods that work even if other events haven't arrived yet +4. Implement logic to derive the final `Status` when enough data exists +5. Register the projection with Marten's projection system + +The test will append events **in scrambled order** and verify your projection handles partial state correctly. + +## Reference + +- [Dealing with Race Conditions in Event-Driven Architecture](https://www.architecture-weekly.com/p/dealing-with-race-conditions-in-event) +- [Marten Multi-Stream Projections](https://martendb.io/events/projections/multi-stream-projections.html) diff --git a/Workshops/IntroductionToEventSourcing/15-EventsDefinition/15-EventsDefinition.csproj b/Workshops/IntroductionToEventSourcing/19-EventsDefinition/19-EventsDefinition.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/15-EventsDefinition/15-EventsDefinition.csproj rename to Workshops/IntroductionToEventSourcing/19-EventsDefinition/19-EventsDefinition.csproj diff --git a/Workshops/IntroductionToEventSourcing/15-EventsDefinition/EventsDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/19-EventsDefinition/EventsDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/15-EventsDefinition/EventsDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/19-EventsDefinition/EventsDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/15-EventsDefinition/GroupCheckoutEvents.cs b/Workshops/IntroductionToEventSourcing/19-EventsDefinition/GroupCheckoutEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/15-EventsDefinition/GroupCheckoutEvents.cs rename to Workshops/IntroductionToEventSourcing/19-EventsDefinition/GroupCheckoutEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/15-EventsDefinition/GuestStayAccountEvents.cs b/Workshops/IntroductionToEventSourcing/19-EventsDefinition/GuestStayAccountEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/15-EventsDefinition/GuestStayAccountEvents.cs rename to Workshops/IntroductionToEventSourcing/19-EventsDefinition/GuestStayAccountEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/16-EntitiesDefinition.csproj b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/20-EntitiesDefinition.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/16-EntitiesDefinition.csproj rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/20-EntitiesDefinition.csproj diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/Database.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/EventCatcher.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/EventCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/EventCatcher.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/EventCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/EventEnvelope.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/EventEnvelope.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/EventEnvelope.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/EventStore.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/EventStore.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/Core/EventStore.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/Core/EventStore.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/GroupCheckoutEvents.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/GroupCheckoutEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/GroupCheckoutEvents.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/GroupCheckoutEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/GuestStayAccountEvents.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/GuestStayAccountEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/GuestStayAccountEvents.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/GuestStayAccountEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/17-BusinessProcesses.csproj b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/21-BusinessProcesses.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/17-BusinessProcesses.csproj rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/21-BusinessProcesses.csproj diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/CommandBus.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/CommandBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/CommandBus.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/CommandBus.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/Database.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/EventEnvelope.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/EventEnvelope.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/EventEnvelope.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/EventStore.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/EventStore.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/EventStore.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/EventStore.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/MessageCatcher.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/MessageCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Core/MessageCatcher.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Core/MessageCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/Core/Aggregate.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/Core/Aggregate.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/Core/Aggregate.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/Core/Aggregate.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/GroupCheckouts/GroupCheckoutEvent.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/GroupCheckouts/GroupCheckoutEvent.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/GroupCheckouts/GroupCheckoutEvent.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/GroupCheckouts/GroupCheckoutEvent.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version1-Aggregates/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version1-Aggregates/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/Core/DictionaryExtensions.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/Core/DictionaryExtensions.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/Core/DictionaryExtensions.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/Core/DictionaryExtensions.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutEvents.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutEvents.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/17-BusinessProcesses/Version2-ImmutableEntities/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/21-BusinessProcesses/Version2-ImmutableEntities/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/18-Idempotency.csproj b/Workshops/IntroductionToEventSourcing/22-Idempotency/22-Idempotency.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/18-Idempotency.csproj rename to Workshops/IntroductionToEventSourcing/22-Idempotency/22-Idempotency.csproj diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Core/CommandBus.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Core/CommandBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Core/CommandBus.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Core/CommandBus.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Core/Database.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Core/EventEnvelope.cs similarity index 93% rename from Workshops/IntroductionToEventSourcing/19-Consistency/EventEnvelope.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Core/EventEnvelope.cs index 28bc6e9e6..26181d72e 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/EventEnvelope.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Core/EventEnvelope.cs @@ -1,4 +1,4 @@ -namespace BusinessProcesses.Core; +namespace Idempotency.Core; public record EventMetadata( string EventId, diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/EventStore.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Core/EventStore.cs similarity index 98% rename from Workshops/IntroductionToEventSourcing/19-Consistency/EventStore.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Core/EventStore.cs index 4dc2df9c1..b52d284ea 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/EventStore.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Core/EventStore.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; -namespace BusinessProcesses.Core; +namespace Idempotency.Core; public class EventStore { diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Core/MessageCatcher.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Core/MessageCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Core/MessageCatcher.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Core/MessageCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs similarity index 98% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs index c43b61616..0f7dbbd2d 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs @@ -18,6 +18,7 @@ namespace Idempotency.Sagas.Version1_Aggregates; public class BusinessProcessTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_ShouldComplete() { // Given; @@ -57,6 +58,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_Sho } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldComplete() { // Given; @@ -106,6 +108,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldCom } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettled_ShouldFail() { // Given; @@ -164,6 +167,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettle [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllUnsettled_ShouldFail() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs index f388f7581..6abda2266 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs @@ -15,6 +15,7 @@ namespace Idempotency.Sagas.Version1_Aggregates; public class EntityDefinitionTests { [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingInGuest_Succeeds() { // Given @@ -30,6 +31,7 @@ public async Task CheckingInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingChargeForCheckedInGuest_Succeeds() { // Given @@ -48,6 +50,7 @@ public async Task RecordingChargeForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuest_Succeeds() { // Given @@ -66,6 +69,7 @@ public async Task RecordingPaymentForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() { // Given @@ -85,6 +89,7 @@ public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_Succeeds() { // Given @@ -106,6 +111,7 @@ public async Task CheckingOutGuestWithSettledBalance_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed() { // Given @@ -134,6 +140,7 @@ public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFaile } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStay_ShouldBeInitiated() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs similarity index 86% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs index fd4997410..b79a887da 100644 --- a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs @@ -18,7 +18,7 @@ public async ValueTask InitiateGroupCheckout(InitiateGroupCheckout command, Canc ); await database.Store(command.GroupCheckoutId, groupCheckout, ct); - await eventStore.AppendToStream(groupCheckout.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), groupCheckout.DequeueUncommittedEvents(), ct); } public async ValueTask RecordGuestCheckoutCompletion( @@ -32,7 +32,7 @@ public async ValueTask RecordGuestCheckoutCompletion( groupCheckout.RecordGuestCheckoutCompletion(command.GuestStayId, command.CompletedAt); await database.Store(command.GroupCheckoutId, groupCheckout, ct); - await eventStore.AppendToStream(groupCheckout.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), groupCheckout.DequeueUncommittedEvents(), ct); } public async ValueTask RecordGuestCheckoutFailure( @@ -46,7 +46,7 @@ public async ValueTask RecordGuestCheckoutFailure( groupCheckout.RecordGuestCheckoutFailure(command.GuestStayId, command.FailedAt); await database.Store(command.GroupCheckoutId, groupCheckout, ct); - await eventStore.AppendToStream(groupCheckout.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), groupCheckout.DequeueUncommittedEvents(), ct); } } diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs similarity index 83% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs index 2aeed574f..f271c2e06 100644 --- a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs @@ -1,4 +1,3 @@ -using BusinessProcesses.Core; using Idempotency.Core; using Idempotency.Sagas.Version1_Aggregates.GroupCheckouts; @@ -14,7 +13,7 @@ public async ValueTask CheckInGuest(CheckInGuest command, CancellationToken ct = var account = GuestStayAccount.CheckIn(command.GuestStayId, command.Now); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = default) @@ -25,7 +24,7 @@ public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = account.RecordCharge(command.Amount, command.Now); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct = default) @@ -36,7 +35,7 @@ public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct account.RecordPayment(command.Amount, command.Now); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct = default) @@ -47,11 +46,11 @@ public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct account.CheckOut(command.Now, command.GroupCheckOutId); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public ValueTask InitiateGroupCheckout(InitiateGroupCheckout command, CancellationToken ct = default) => - eventStore.AppendToStream([ + eventStore.AppendToStream(command.GroupCheckoutId.ToString(), [ new GroupCheckoutEvent.GroupCheckoutInitiated( command.GroupCheckoutId, command.ClerkId, diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs similarity index 97% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs index aa76e69b5..05af0cdf1 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs @@ -15,6 +15,7 @@ namespace Idempotency.Sagas.Version1_Aggregates; public class IdempotencyTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutIsInitiatedOnceForTheSameId() { // Given @@ -35,6 +36,7 @@ public async Task GroupCheckoutIsInitiatedOnceForTheSameId() } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutCompletion() { // Given @@ -59,6 +61,7 @@ await groupCheckoutFacade.InitiateGroupCheckout(new InitiateGroupCheckout(groupC } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutFailure() { // Given diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs similarity index 98% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs index 3a1c28d7d..2d79c3cd3 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs @@ -17,6 +17,7 @@ namespace Idempotency.Sagas.Version2_ImmutableEntities; public class BusinessProcessTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_ShouldComplete() { // Given; @@ -56,6 +57,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_Sho } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldComplete() { // Given; @@ -105,6 +107,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldCom } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettled_ShouldFail() { // Given; @@ -163,6 +166,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettle [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllUnsettled_ShouldFail() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs index 75753138c..619f0417e 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs @@ -13,6 +13,7 @@ namespace Idempotency.Sagas.Version2_ImmutableEntities; public class EntityDefinitionTests { [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingInGuest_Succeeds() { // Given @@ -28,6 +29,7 @@ public async Task CheckingInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingChargeForCheckedInGuest_Succeeds() { // Given @@ -46,6 +48,7 @@ public async Task RecordingChargeForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuest_Succeeds() { // Given @@ -64,6 +67,7 @@ public async Task RecordingPaymentForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() { // Given @@ -83,6 +87,7 @@ public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_Succeeds() { // Given @@ -104,6 +109,7 @@ public async Task CheckingOutGuestWithSettledBalance_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed() { // Given diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs similarity index 88% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs index b9faa26f3..96b45249e 100644 --- a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs @@ -17,7 +17,7 @@ public async ValueTask InitiateGroupCheckout(InitiateGroupCheckout command, Canc ); await database.Store(command.GroupCheckoutId, GroupCheckOut.Initial.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), [@event], ct); } public async ValueTask RecordGuestCheckoutCompletion( @@ -36,7 +36,7 @@ public async ValueTask RecordGuestCheckoutCompletion( await database.Store(command.GroupCheckoutId, events.Aggregate(groupCheckout, (state, @event) => state.Evolve(@event)), ct); - await eventStore.AppendToStream(events.Cast().ToArray(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), events.Cast().ToArray(), ct); } public async ValueTask RecordGuestCheckoutFailure( @@ -56,7 +56,7 @@ public async ValueTask RecordGuestCheckoutFailure( await database.Store(command.GroupCheckoutId, newState, ct); - await eventStore.AppendToStream(events.Cast().ToArray(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), events.Cast().ToArray(), ct); } } diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs similarity index 85% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs index 630759e35..a1306805d 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs @@ -12,7 +12,7 @@ public async ValueTask CheckInGuest(CheckInGuest command, CancellationToken ct = var @event = GuestStayAccount.CheckIn(command.GuestStayId, command.Now); await database.Store(command.GuestStayId, GuestStayAccount.Initial.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [@event], ct); } public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = default) @@ -23,7 +23,7 @@ public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = var @event = account.RecordCharge(command.Amount, command.Now); await database.Store(command.GuestStayId, account.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [@event], ct); } public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct = default) @@ -34,7 +34,7 @@ public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct var @event = account.RecordPayment(command.Amount, command.Now); await database.Store(command.GuestStayId, account.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [@event], ct); } public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct = default) @@ -47,12 +47,12 @@ public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct case GuestCheckedOut checkedOut: { await database.Store(command.GuestStayId, account.Evolve(checkedOut), ct); - await eventStore.AppendToStream([checkedOut], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [checkedOut], ct); return; } case GuestCheckOutFailed checkOutFailed: { - await eventStore.AppendToStream([checkOutFailed], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [checkOutFailed], ct); return; } } diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs similarity index 97% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs index dbae97ec7..2a178a285 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs +++ b/Workshops/IntroductionToEventSourcing/22-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs @@ -15,6 +15,7 @@ namespace Idempotency.Sagas.Version2_ImmutableEntities; public class IdempotencyTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutIsInitiatedOnceForTheSameId() { // Given @@ -35,6 +36,7 @@ public async Task GroupCheckoutIsInitiatedOnceForTheSameId() } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutCompletion() { // Given @@ -59,6 +61,7 @@ await groupCheckoutFacade.InitiateGroupCheckout(new InitiateGroupCheckout(groupC } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutFailure() { // Given diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/19-Consistency.csproj b/Workshops/IntroductionToEventSourcing/23-Consistency/23-Consistency.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/19-Consistency.csproj rename to Workshops/IntroductionToEventSourcing/23-Consistency/23-Consistency.csproj diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Core/CommandBus.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Core/CommandBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Core/CommandBus.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Core/CommandBus.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Core/Database.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Core/MessageCatcher.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Core/MessageCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Core/MessageCatcher.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Core/MessageCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Core/TimeoutException.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Core/TimeoutException.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Core/TimeoutException.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Core/TimeoutException.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Core/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/EventEnvelope.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Core/EventEnvelope.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/EventEnvelope.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Core/EventStore.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/EventStore.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Core/EventStore.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/EventStore.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs similarity index 98% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs index aefee6e75..23ecbb7c1 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs @@ -1,4 +1,5 @@ using Bogus; +using BusinessProcesses.Core; using Consistency.Core; using Consistency.Sagas.Version1_Aggregates.GroupCheckouts; using Consistency.Sagas.Version1_Aggregates.GuestStayAccounts; @@ -18,6 +19,7 @@ namespace Consistency.Sagas.Version1_Aggregates; public class BusinessProcessTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_ShouldComplete() { // Given; @@ -57,6 +59,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_Sho } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldComplete() { // Given; @@ -106,6 +109,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldCom } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettled_ShouldFail() { // Given; @@ -164,6 +168,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettle [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllUnsettled_ShouldFail() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs similarity index 95% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs index 83cb84be7..7474cd2b7 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs @@ -1,4 +1,5 @@ using Bogus; +using BusinessProcesses.Core; using Consistency.Core; using Consistency.Sagas.Version1_Aggregates.GroupCheckouts; using Consistency.Sagas.Version1_Aggregates.GuestStayAccounts; @@ -15,6 +16,7 @@ namespace Consistency.Sagas.Version1_Aggregates; public class EntityDefinitionTests { [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingInGuest_Succeeds() { // Given @@ -30,6 +32,7 @@ public async Task CheckingInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingChargeForCheckedInGuest_Succeeds() { // Given @@ -48,6 +51,7 @@ public async Task RecordingChargeForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuest_Succeeds() { // Given @@ -66,6 +70,7 @@ public async Task RecordingPaymentForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() { // Given @@ -85,6 +90,7 @@ public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_Succeeds() { // Given @@ -106,6 +112,7 @@ public async Task CheckingOutGuestWithSettledBalance_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed() { // Given @@ -134,6 +141,7 @@ public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFaile } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStay_ShouldBeInitiated() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs similarity index 84% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs index fb86a8443..641a844d3 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs @@ -1,4 +1,5 @@ -using Consistency.Core; +using BusinessProcesses.Core; +using Consistency.Core; using Database = Consistency.Core.Database; namespace Consistency.Sagas.Version1_Aggregates.GroupCheckouts; @@ -21,7 +22,7 @@ public async ValueTask InitiateGroupCheckout(InitiateGroupCheckout command, Canc ); await database.Store(command.GroupCheckoutId, groupCheckout, ct); - await eventStore.AppendToStream(groupCheckout.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), groupCheckout.DequeueUncommittedEvents(), ct); } public async ValueTask RecordGuestCheckoutCompletion( @@ -35,7 +36,7 @@ public async ValueTask RecordGuestCheckoutCompletion( groupCheckout.RecordGuestCheckoutCompletion(command.GuestStayId, command.CompletedAt); await database.Store(command.GroupCheckoutId, groupCheckout, ct); - await eventStore.AppendToStream(groupCheckout.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), groupCheckout.DequeueUncommittedEvents(), ct); } public async ValueTask RecordGuestCheckoutFailure( @@ -49,7 +50,7 @@ public async ValueTask RecordGuestCheckoutFailure( groupCheckout.RecordGuestCheckoutFailure(command.GuestStayId, command.FailedAt); await database.Store(command.GroupCheckoutId, groupCheckout, ct); - await eventStore.AppendToStream(groupCheckout.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), groupCheckout.DequeueUncommittedEvents(), ct); } } diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs index 389184950..babbabec3 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs @@ -1,4 +1,5 @@ -using Consistency.Core; +using BusinessProcesses.Core; +using Consistency.Core; using Consistency.Sagas.Version1_Aggregates.GuestStayAccounts; namespace Consistency.Sagas.Version1_Aggregates.GroupCheckouts; diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs similarity index 82% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs index a093dbc1e..65bc0c0b8 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs @@ -1,3 +1,4 @@ +using BusinessProcesses.Core; using Consistency.Core; using Consistency.Sagas.Version1_Aggregates.GroupCheckouts; @@ -13,7 +14,7 @@ public async ValueTask CheckInGuest(CheckInGuest command, CancellationToken ct = var account = GuestStayAccount.CheckIn(command.GuestStayId, command.Now); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = default) @@ -24,7 +25,7 @@ public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = account.RecordCharge(command.Amount, command.Now); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct = default) @@ -35,7 +36,7 @@ public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct account.RecordPayment(command.Amount, command.Now); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct = default) @@ -46,11 +47,11 @@ public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct account.CheckOut(command.Now, command.GroupCheckOutId); await database.Store(command.GuestStayId, account, ct); - await eventStore.AppendToStream(account.DequeueUncommittedEvents(), ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), account.DequeueUncommittedEvents(), ct); } public ValueTask InitiateGroupCheckout(InitiateGroupCheckout command, CancellationToken ct = default) => - eventStore.AppendToStream([ + eventStore.AppendToStream(command.GroupCheckoutId.ToString(), [ new GroupCheckoutEvent.GroupCheckoutInitiated( command.GroupCheckoutId, command.ClerkId, diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs index 7658a49f0..74fe0ba50 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs @@ -1,4 +1,5 @@ using Bogus; +using BusinessProcesses.Core; using Consistency.Core; using Consistency.Sagas.Version1_Aggregates.GroupCheckouts; using Consistency.Sagas.Version1_Aggregates.GuestStayAccounts; @@ -15,6 +16,7 @@ namespace Consistency.Sagas.Version1_Aggregates; public class IdempotencyTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutIsInitiatedOnceForTheSameId() { // Given @@ -35,6 +37,7 @@ public async Task GroupCheckoutIsInitiatedOnceForTheSameId() } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutCompletion() { // Given @@ -59,6 +62,7 @@ await groupCheckoutFacade.InitiateGroupCheckout(new InitiateGroupCheckout(groupC } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutFailure() { // Given diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs similarity index 98% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs index 4f65b5f12..623991017 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs @@ -1,4 +1,5 @@ using Bogus; +using BusinessProcesses.Core; using Consistency.Core; using Consistency.Sagas.Version2_ImmutableEntities.GroupCheckouts; using Consistency.Sagas.Version2_ImmutableEntities.GuestStayAccounts; @@ -17,6 +18,7 @@ namespace Consistency.Sagas.Version2_ImmutableEntities; public class BusinessProcessTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_ShouldComplete() { // Given; @@ -56,6 +58,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_Sho } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldComplete() { // Given; @@ -105,6 +108,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldCom } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettled_ShouldFail() { // Given; @@ -163,6 +167,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettle [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllUnsettled_ShouldFail() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs similarity index 95% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs index 3f7012db5..c655dfc3d 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs @@ -1,4 +1,5 @@ using Bogus; +using BusinessProcesses.Core; using Consistency.Core; using Consistency.Sagas.Version2_ImmutableEntities.GuestStayAccounts; using Xunit; @@ -13,6 +14,7 @@ namespace Consistency.Sagas.Version2_ImmutableEntities; public class EntityDefinitionTests { [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingInGuest_Succeeds() { // Given @@ -28,6 +30,7 @@ public async Task CheckingInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingChargeForCheckedInGuest_Succeeds() { // Given @@ -46,6 +49,7 @@ public async Task RecordingChargeForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuest_Succeeds() { // Given @@ -64,6 +68,7 @@ public async Task RecordingPaymentForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() { // Given @@ -83,6 +88,7 @@ public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_Succeeds() { // Given @@ -104,6 +110,7 @@ public async Task CheckingOutGuestWithSettledBalance_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed() { // Given diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs similarity index 87% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs index ae476efd1..93ab25bab 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs @@ -1,4 +1,5 @@ -using Consistency.Core; +using BusinessProcesses.Core; +using Consistency.Core; using Database = Consistency.Core.Database; namespace Consistency.Sagas.Version2_ImmutableEntities.GroupCheckouts; @@ -20,7 +21,7 @@ public async ValueTask InitiateGroupCheckout(InitiateGroupCheckout command, Canc ); await database.Store(command.GroupCheckoutId, GroupCheckOut.Initial.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), [@event], ct); } public async ValueTask RecordGuestCheckoutCompletion( @@ -39,7 +40,7 @@ public async ValueTask RecordGuestCheckoutCompletion( await database.Store(command.GroupCheckoutId, events.Aggregate(groupCheckout, (state, @event) => state.Evolve(@event)), ct); - await eventStore.AppendToStream(events.Cast().ToArray(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), events.Cast().ToArray(), ct); } public async ValueTask RecordGuestCheckoutFailure( @@ -59,7 +60,7 @@ public async ValueTask RecordGuestCheckoutFailure( await database.Store(command.GroupCheckoutId, newState, ct); - await eventStore.AppendToStream(events.Cast().ToArray(), ct); + await eventStore.AppendToStream(command.GroupCheckoutId.ToString(), events.Cast().ToArray(), ct); } } diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs index 018420338..7d938998b 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs @@ -1,4 +1,5 @@ -using Consistency.Core; +using BusinessProcesses.Core; +using Consistency.Core; using Consistency.Sagas.Version2_ImmutableEntities.GuestStayAccounts; namespace Consistency.Sagas.Version2_ImmutableEntities.GroupCheckouts; diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs similarity index 84% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs index 95fe81fff..419eae129 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs @@ -1,3 +1,4 @@ +using BusinessProcesses.Core; using Consistency.Core; namespace Consistency.Sagas.Version2_ImmutableEntities.GuestStayAccounts; @@ -12,7 +13,7 @@ public async ValueTask CheckInGuest(CheckInGuest command, CancellationToken ct = var @event = GuestStayAccount.CheckIn(command.GuestStayId, command.Now); await database.Store(command.GuestStayId, GuestStayAccount.Initial.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [@event], ct); } public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = default) @@ -23,7 +24,7 @@ public async ValueTask RecordCharge(RecordCharge command, CancellationToken ct = var @event = account.RecordCharge(command.Amount, command.Now); await database.Store(command.GuestStayId, account.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [@event], ct); } public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct = default) @@ -34,7 +35,7 @@ public async ValueTask RecordPayment(RecordPayment command, CancellationToken ct var @event = account.RecordPayment(command.Amount, command.Now); await database.Store(command.GuestStayId, account.Evolve(@event), ct); - await eventStore.AppendToStream([@event], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [@event], ct); } public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct = default) @@ -47,12 +48,12 @@ public async ValueTask CheckOutGuest(CheckOutGuest command, CancellationToken ct case GuestCheckedOut checkedOut: { await database.Store(command.GuestStayId, account.Evolve(checkedOut), ct); - await eventStore.AppendToStream([checkedOut], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [checkedOut], ct); return; } case GuestCheckOutFailed checkOutFailed: { - await eventStore.AppendToStream([checkOutFailed], ct); + await eventStore.AppendToStream(command.GuestStayId.ToString(), [checkOutFailed], ct); return; } } diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs index d50bd4575..c9c23c5e6 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs +++ b/Workshops/IntroductionToEventSourcing/23-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs @@ -1,4 +1,5 @@ using Bogus; +using BusinessProcesses.Core; using Consistency.Core; using Consistency.Sagas.Version2_ImmutableEntities.GroupCheckouts; using Consistency.Sagas.Version2_ImmutableEntities.GuestStayAccounts; @@ -15,6 +16,7 @@ namespace Consistency.Sagas.Version2_ImmutableEntities; public class IdempotencyTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutIsInitiatedOnceForTheSameId() { // Given @@ -35,6 +37,7 @@ public async Task GroupCheckoutIsInitiatedOnceForTheSameId() } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutCompletion() { // Given @@ -59,6 +62,7 @@ await groupCheckoutFacade.InitiateGroupCheckout(new InitiateGroupCheckout(groupC } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutFailure() { // Given diff --git a/Workshops/IntroductionToEventSourcing/Exercises.slnx b/Workshops/IntroductionToEventSourcing/Exercises.slnx index d33b1ef89..f92d9e5e7 100644 --- a/Workshops/IntroductionToEventSourcing/Exercises.slnx +++ b/Workshops/IntroductionToEventSourcing/Exercises.slnx @@ -25,7 +25,13 @@ - - - + + + + + + + + + diff --git a/Workshops/IntroductionToEventSourcing/Solved.slnx b/Workshops/IntroductionToEventSourcing/Solved.slnx index c419b031b..4d42be06f 100644 --- a/Workshops/IntroductionToEventSourcing/Solved.slnx +++ b/Workshops/IntroductionToEventSourcing/Solved.slnx @@ -25,7 +25,13 @@ - - - + + + + + + + + + diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/15-Projections.MultiStream.csproj b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/15-Projections.MultiStream.csproj new file mode 100644 index 000000000..156a21617 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/15-Projections.MultiStream.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/ProjectionsTests.cs new file mode 100644 index 000000000..620c344f7 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/ProjectionsTests.cs @@ -0,0 +1,211 @@ +using FluentAssertions; +using IntroductionToEventSourcing.Projections.MultiStream.Tools; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +public record PaymentVerificationCompleted( + Guid PaymentId, + bool IsApproved +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public Guid OrderId { get; set; } + public decimal Amount { get; set; } + public VerificationStatus MerchantLimitStatus { get; set; } + public VerificationStatus FraudStatus { get; set; } + public decimal FraudScore { get; set; } + public PaymentStatus Status { get; set; } +} + +public static class DatabaseExtensions +{ + public static void GetAndStore(this Database database, Guid id, Func update) where T : class, new() + { + var item = database.Get(id) ?? new T(); + + database.Store(id, update(item)); + } +} + +public class PaymentVerificationProjection(Database database) +{ + public void Handle(EventEnvelope @event) => + database.GetAndStore(@event.Data.PaymentId, item => + { + item.Id = @event.Data.PaymentId; + item.OrderId = @event.Data.OrderId; + item.Amount = @event.Data.Amount; + + return item; + }); + + public void Handle(EventEnvelope @event) => + database.GetAndStore(@event.Data.PaymentId, item => + { + item.MerchantLimitStatus = @event.Data.IsWithinLimits + ? VerificationStatus.Passed + : VerificationStatus.Failed; + + return item; + }); + + public void Handle(EventEnvelope @event) => + database.GetAndStore(@event.Data.PaymentId, item => + { + item.FraudScore = @event.Data.Score; + item.FraudStatus = @event.Data.IsAcceptable + ? VerificationStatus.Passed + : VerificationStatus.Failed; + + return item; + }); + + public void Handle(EventEnvelope @event) => + database.GetAndStore(@event.Data.PaymentId, item => + { + item.Status = @event.Data.IsApproved + ? PaymentStatus.Approved + : PaymentStatus.Rejected; + + return item; + }); +} + +public class ProjectionsTests +{ + [Fact] + [Trait("Category", "SkipCI")] + public void MultiStreamProjection_ForPaymentVerification_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + + var eventStore = new EventStore(); + var database = new Database(); + + var projection = new PaymentVerificationProjection(database); + + eventStore.Register(projection.Handle); + eventStore.Register(projection.Handle); + eventStore.Register(projection.Handle); + eventStore.Register(projection.Handle); + + // Payment 1: Approved — all checks pass + eventStore.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + eventStore.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + eventStore.Append(payment1Id, new PaymentVerificationCompleted(payment1Id, true)); + + // Payment 2: Merchant rejected — exceeds merchant limits + eventStore.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + eventStore.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + eventStore.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + eventStore.Append(payment2Id, new PaymentVerificationCompleted(payment2Id, false)); + + // Payment 3: Fraud rejected — high fraud score + eventStore.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + eventStore.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + eventStore.Append(payment3Id, new PaymentVerificationCompleted(payment3Id, false)); + + // Payment 4: Pending — still awaiting fraud check and final decision + eventStore.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + // Assert Payment 1: Approved + var payment1 = database.Get(payment1Id)!; + payment1.Should().NotBeNull(); + payment1.Id.Should().Be(payment1Id); + payment1.OrderId.Should().Be(order1Id); + payment1.Amount.Should().Be(100m); + payment1.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment1.FraudStatus.Should().Be(VerificationStatus.Passed); + payment1.FraudScore.Should().Be(0.1m); + payment1.Status.Should().Be(PaymentStatus.Approved); + + // Assert Payment 2: Merchant rejected + var payment2 = database.Get(payment2Id)!; + payment2.Should().NotBeNull(); + payment2.Id.Should().Be(payment2Id); + payment2.OrderId.Should().Be(order2Id); + payment2.Amount.Should().Be(5000m); + payment2.MerchantLimitStatus.Should().Be(VerificationStatus.Failed); + payment2.FraudStatus.Should().Be(VerificationStatus.Passed); + payment2.FraudScore.Should().Be(0.2m); + payment2.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 3: Fraud rejected + var payment3 = database.Get(payment3Id)!; + payment3.Should().NotBeNull(); + payment3.Id.Should().Be(payment3Id); + payment3.OrderId.Should().Be(order3Id); + payment3.Amount.Should().Be(200m); + payment3.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment3.FraudStatus.Should().Be(VerificationStatus.Failed); + payment3.FraudScore.Should().Be(0.95m); + payment3.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 4: Pending + var payment4 = database.Get(payment4Id)!; + payment4.Should().NotBeNull(); + payment4.Id.Should().Be(payment4Id); + payment4.OrderId.Should().Be(order4Id); + payment4.Amount.Should().Be(50m); + payment4.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment4.FraudStatus.Should().Be(VerificationStatus.Pending); + payment4.FraudScore.Should().Be(0m); + payment4.Status.Should().Be(PaymentStatus.Pending); + } +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/Database.cs b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/Database.cs new file mode 100644 index 000000000..c80ac405a --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/Database.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace IntroductionToEventSourcing.Projections.MultiStream.Tools; + +public class Database +{ + private readonly Dictionary storage = new(); + + public void Store(Guid id, T obj) where T: class => + storage[GetId(id)] = obj; + + public void Delete(Guid id) => + storage.Remove(GetId(id)); + + public T? Get(Guid id) where T: class => + storage.TryGetValue(GetId(id), out var result) ? + // Clone to simulate getting new instance on loading + JsonSerializer.Deserialize(JsonSerializer.Serialize((T)result)) + : null; + + private static string GetId(Guid id) => $"{typeof(T).Name}-{id}"; +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/EventEnvelope.cs new file mode 100644 index 000000000..613642c97 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/EventEnvelope.cs @@ -0,0 +1,24 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.Tools; + +public record EventMetadata( + string EventId, + ulong StreamPosition, + ulong LogPosition +) +{ + public static EventMetadata For(ulong streamPosition, ulong logPosition) => + new(Guid.CreateVersion7().ToString(), streamPosition, logPosition); +} + +public record EventEnvelope( + object Data, + EventMetadata Metadata +); + +public record EventEnvelope( + T Data, + EventMetadata Metadata +): EventEnvelope(Data, Metadata) where T : notnull +{ + public new T Data => (T)base.Data; +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/EventStore.cs b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/EventStore.cs new file mode 100644 index 000000000..ed4bed2eb --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/Tools/EventStore.cs @@ -0,0 +1,41 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.Tools; + +public class EventStore +{ + private readonly Dictionary>> handlers = new(); + private readonly Dictionary> events = new(); + + public void Register(Action> handler) where TEvent : notnull + { + var eventType = typeof(TEvent); + + void WrappedHandler(object @event) => handler((EventEnvelope)@event); + + if (handlers.TryGetValue(eventType, out var handler1)) + handler1.Add(WrappedHandler); + else + handlers.Add(eventType, [WrappedHandler]); + } + + public void Append(Guid streamId, TEvent @event) where TEvent : notnull + { + if (!events.ContainsKey(streamId)) + events[streamId] = []; + + var eventEnvelope = new EventEnvelope(@event, + EventMetadata.For( + (ulong)events[streamId].Count + 1, + (ulong)events.Values.Sum(s => s.Count) + ) + ); + + events[streamId].Add(eventEnvelope); + + if (!handlers.TryGetValue(eventEnvelope.Data.GetType(), out var eventHandlers)) return; + + foreach (var handle in eventHandlers) + { + handle(eventEnvelope); + } + } +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/16-Projections.MultiStream.OutOfOrder.csproj b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/16-Projections.MultiStream.OutOfOrder.csproj new file mode 100644 index 000000000..600056afb --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/16-Projections.MultiStream.OutOfOrder.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/ProjectionsTests.cs new file mode 100644 index 000000000..90e81b845 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/ProjectionsTests.cs @@ -0,0 +1,225 @@ +using FluentAssertions; +using IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +public enum DataQuality +{ + Partial, + Sufficient, + Complete +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public Guid? OrderId { get; set; } + public decimal? Amount { get; set; } + public VerificationStatus MerchantLimitStatus { get; set; } + public VerificationStatus FraudStatus { get; set; } + public decimal FraudScore { get; set; } + public PaymentStatus Status { get; set; } + public DataQuality DataQuality { get; set; } +} + +public static class DatabaseExtensions +{ + public static void GetAndStore(this Database database, Guid id, Func update) where T : class, new() + { + var item = database.Get(id) ?? new T(); + + database.Store(id, update(item)); + } +} + +public class PaymentVerificationProjection(Database database) +{ + public void Handle(EventEnvelope @event) + { + database.GetAndStore(@event.Data.PaymentId, item => + { + item.Id = @event.Data.PaymentId; + item.OrderId = @event.Data.OrderId; + item.Amount = @event.Data.Amount; + + return Recalculate(item); + }); + } + + public void Handle(EventEnvelope @event) + { + database.GetAndStore(@event.Data.PaymentId, item => + { + item.Id = @event.Data.PaymentId; + item.MerchantLimitStatus = @event.Data.IsWithinLimits + ? VerificationStatus.Passed + : VerificationStatus.Failed; + + return Recalculate(item); + }); + } + + public void Handle(EventEnvelope @event) + { + database.GetAndStore(@event.Data.PaymentId, item => + { + item.Id = @event.Data.PaymentId; + item.FraudScore = @event.Data.Score; + item.FraudStatus = @event.Data.IsAcceptable + ? VerificationStatus.Passed + : VerificationStatus.Failed; + + return Recalculate(item); + }); + } + + private static PaymentVerification Recalculate(PaymentVerification item) + { + // Check if we have all required data + var hasMerchantCheck = item.MerchantLimitStatus != VerificationStatus.Pending; + var hasFraudCheck = item.FraudStatus != VerificationStatus.Pending; + var hasPaymentData = item.OrderId.HasValue && item.Amount.HasValue; + + // Update data quality + if (hasPaymentData && hasMerchantCheck && hasFraudCheck) + item.DataQuality = DataQuality.Complete; + else if (hasMerchantCheck || hasFraudCheck) + item.DataQuality = DataQuality.Sufficient; + else + item.DataQuality = DataQuality.Partial; + + // Derive status based on available data + // If either check failed, reject immediately (even if we're missing payment data) + if (item.MerchantLimitStatus == VerificationStatus.Failed || + item.FraudStatus == VerificationStatus.Failed) + { + item.Status = PaymentStatus.Rejected; + } + // If we have both checks and they passed, approve + else if (hasMerchantCheck && hasFraudCheck) + { + item.Status = PaymentStatus.Approved; + } + // Otherwise, still pending + else + { + item.Status = PaymentStatus.Pending; + } + + return item; + } +} + +public class ProjectionsTests +{ + [Fact] + [Trait("Category", "SkipCI")] + public void MultiStreamProjection_WithOutOfOrderEvents_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + var fraudCheck4Id = Guid.CreateVersion7(); + + var eventStore = new EventStore(); + var database = new Database(); + + var projection = new PaymentVerificationProjection(database); + + eventStore.Register(projection.Handle); + eventStore.Register(projection.Handle); + eventStore.Register(projection.Handle); + + // Payment 1: Approved — events arrive out of order (fraud score first!) + eventStore.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + eventStore.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + + // Payment 2: Merchant rejected — merchant check arrives first + eventStore.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + eventStore.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + eventStore.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + + // Payment 3: Fraud rejected — payment recorded last + eventStore.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + eventStore.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + + // Payment 4: Pending — missing fraud check (payment recorded, merchant checked, but no fraud score yet) + eventStore.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + // Assert Payment 1: Approved (all data arrived, all checks passed) + var payment1 = database.Get(payment1Id)!; + payment1.Should().NotBeNull(); + payment1.Id.Should().Be(payment1Id); + payment1.Status.Should().Be(PaymentStatus.Approved); + + // Assert Payment 2: Merchant rejected + var payment2 = database.Get(payment2Id)!; + payment2.Should().NotBeNull(); + payment2.Id.Should().Be(payment2Id); + payment2.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 3: Fraud rejected + var payment3 = database.Get(payment3Id)!; + payment3.Should().NotBeNull(); + payment3.Id.Should().Be(payment3Id); + payment3.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 4: Pending (waiting for fraud check) + var payment4 = database.Get(payment4Id)!; + payment4.Should().NotBeNull(); + payment4.Id.Should().Be(payment4Id); + payment4.Status.Should().Be(PaymentStatus.Pending); + } +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/Database.cs b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/Database.cs new file mode 100644 index 000000000..85ac1ebc4 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/Database.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; + +public class Database +{ + private readonly Dictionary storage = new(); + + public void Store(Guid id, T obj) where T: class => + storage[GetId(id)] = obj; + + public void Delete(Guid id) => + storage.Remove(GetId(id)); + + public T? Get(Guid id) where T: class => + storage.TryGetValue(GetId(id), out var result) ? + // Clone to simulate getting new instance on loading + JsonSerializer.Deserialize(JsonSerializer.Serialize((T)result)) + : null; + + private static string GetId(Guid id) => $"{typeof(T).Name}-{id}"; +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/EventEnvelope.cs new file mode 100644 index 000000000..033a300c8 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/EventEnvelope.cs @@ -0,0 +1,24 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; + +public record EventMetadata( + string EventId, + ulong StreamPosition, + ulong LogPosition +) +{ + public static EventMetadata For(ulong streamPosition, ulong logPosition) => + new(Guid.CreateVersion7().ToString(), streamPosition, logPosition); +} + +public record EventEnvelope( + object Data, + EventMetadata Metadata +); + +public record EventEnvelope( + T Data, + EventMetadata Metadata +): EventEnvelope(Data, Metadata) where T : notnull +{ + public new T Data => (T)base.Data; +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/EventStore.cs b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/EventStore.cs new file mode 100644 index 000000000..e80209f2d --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/16-Projections.MultiStream.OutOfOrder/Tools/EventStore.cs @@ -0,0 +1,41 @@ +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Tools; + +public class EventStore +{ + private readonly Dictionary>> handlers = new(); + private readonly Dictionary> events = new(); + + public void Register(Action> handler) where TEvent : notnull + { + var eventType = typeof(TEvent); + + void WrappedHandler(object @event) => handler((EventEnvelope)@event); + + if (handlers.TryGetValue(eventType, out var handler1)) + handler1.Add(WrappedHandler); + else + handlers.Add(eventType, [WrappedHandler]); + } + + public void Append(Guid streamId, TEvent @event) where TEvent : notnull + { + if (!events.ContainsKey(streamId)) + events[streamId] = []; + + var eventEnvelope = new EventEnvelope(@event, + EventMetadata.For( + (ulong)events[streamId].Count + 1, + (ulong)events.Values.Sum(s => s.Count) + ) + ); + + events[streamId].Add(eventEnvelope); + + if (!handlers.TryGetValue(eventEnvelope.Data.GetType(), out var eventHandlers)) return; + + foreach (var handle in eventHandlers) + { + handle(eventEnvelope); + } + } +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-Projections.MultiStream.Marten/17-Projections.MultiStream.Marten.csproj b/Workshops/IntroductionToEventSourcing/Solved/17-Projections.MultiStream.Marten/17-Projections.MultiStream.Marten.csproj new file mode 100644 index 000000000..1abd0cec2 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/17-Projections.MultiStream.Marten/17-Projections.MultiStream.Marten.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream.Marten + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-Projections.MultiStream.Marten/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/Solved/17-Projections.MultiStream.Marten/ProjectionsTests.cs new file mode 100644 index 000000000..d45bf0248 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/17-Projections.MultiStream.Marten/ProjectionsTests.cs @@ -0,0 +1,208 @@ +using FluentAssertions; +using JasperFx; +using JasperFx.Events.Projections; +using Marten; +using Marten.Events.Projections; +using Weasel.Core; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream.Marten; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +public record PaymentVerificationCompleted( + Guid PaymentId, + bool IsApproved +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public Guid OrderId { get; set; } + public decimal Amount { get; set; } + public VerificationStatus MerchantLimitStatus { get; set; } + public VerificationStatus FraudStatus { get; set; } + public decimal FraudScore { get; set; } + public PaymentStatus Status { get; set; } +} + +public class PaymentVerificationProjection: MultiStreamProjection +{ + public PaymentVerificationProjection() + { + // Tell Marten how to extract the PaymentId from each event + Identity(e => e.PaymentId); + Identity(e => e.PaymentId); + Identity(e => e.PaymentId); + Identity(e => e.PaymentId); + } + + public void Apply(PaymentVerification item, PaymentRecorded @event) + { + item.Id = @event.PaymentId; + item.OrderId = @event.OrderId; + item.Amount = @event.Amount; + } + + public void Apply(PaymentVerification item, MerchantLimitsChecked @event) + { + item.MerchantLimitStatus = @event.IsWithinLimits + ? VerificationStatus.Passed + : VerificationStatus.Failed; + } + + public void Apply(PaymentVerification item, FraudScoreCalculated @event) + { + item.FraudScore = @event.Score; + item.FraudStatus = @event.IsAcceptable + ? VerificationStatus.Passed + : VerificationStatus.Failed; + } + + public void Apply(PaymentVerification item, PaymentVerificationCompleted @event) + { + item.Status = @event.IsApproved + ? PaymentStatus.Approved + : PaymentStatus.Rejected; + } +} + +public class ProjectionsTests +{ + private const string ConnectionString = + "PORT = 5432; HOST = localhost; TIMEOUT = 15; POOLING = True; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"; + + [Fact] + [Trait("Category", "SkipCI")] + public async Task MultiStreamProjection_WithMarten_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + + await using var documentStore = DocumentStore.For(options => + { + options.Connection(ConnectionString); + options.DatabaseSchemaName = options.Events.DatabaseSchemaName = "Exercise17MultiStreamMarten"; + options.AutoCreateSchemaObjects = AutoCreate.All; + + options.Projections.Add(ProjectionLifecycle.Inline); + }); + + await using var session = documentStore.LightweightSession(); + + // Payment 1: Approved — all checks pass + session.Events.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + session.Events.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + session.Events.Append(payment1Id, new PaymentVerificationCompleted(payment1Id, true)); + + // Payment 2: Merchant rejected — exceeds merchant limits + session.Events.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + session.Events.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + session.Events.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + session.Events.Append(payment2Id, new PaymentVerificationCompleted(payment2Id, false)); + + // Payment 3: Fraud rejected — high fraud score + session.Events.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + session.Events.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + session.Events.Append(payment3Id, new PaymentVerificationCompleted(payment3Id, false)); + + // Payment 4: Pending — still awaiting fraud check and final decision + session.Events.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + await session.SaveChangesAsync(); + + // Assert Payment 1: Approved + var payment1 = await session.LoadAsync(payment1Id); + payment1.Should().NotBeNull(); + payment1!.Id.Should().Be(payment1Id); + payment1.OrderId.Should().Be(order1Id); + payment1.Amount.Should().Be(100m); + payment1.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment1.FraudStatus.Should().Be(VerificationStatus.Passed); + payment1.FraudScore.Should().Be(0.1m); + payment1.Status.Should().Be(PaymentStatus.Approved); + + // Assert Payment 2: Merchant rejected + var payment2 = await session.LoadAsync(payment2Id); + payment2.Should().NotBeNull(); + payment2!.Id.Should().Be(payment2Id); + payment2.OrderId.Should().Be(order2Id); + payment2.Amount.Should().Be(5000m); + payment2.MerchantLimitStatus.Should().Be(VerificationStatus.Failed); + payment2.FraudStatus.Should().Be(VerificationStatus.Passed); + payment2.FraudScore.Should().Be(0.2m); + payment2.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 3: Fraud rejected + var payment3 = await session.LoadAsync(payment3Id); + payment3.Should().NotBeNull(); + payment3!.Id.Should().Be(payment3Id); + payment3.OrderId.Should().Be(order3Id); + payment3.Amount.Should().Be(200m); + payment3.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment3.FraudStatus.Should().Be(VerificationStatus.Failed); + payment3.FraudScore.Should().Be(0.95m); + payment3.Status.Should().Be(PaymentStatus.Rejected); + + // Assert Payment 4: Pending + var payment4 = await session.LoadAsync(payment4Id); + payment4.Should().NotBeNull(); + payment4!.Id.Should().Be(payment4Id); + payment4.OrderId.Should().Be(order4Id); + payment4.Amount.Should().Be(50m); + payment4.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment4.FraudStatus.Should().Be(VerificationStatus.Pending); + payment4.FraudScore.Should().Be(0m); + payment4.Status.Should().Be(PaymentStatus.Pending); + } +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Projections.MultiStream.OutOfOrder.Marten/18-Projections.MultiStream.OutOfOrder.Marten.csproj b/Workshops/IntroductionToEventSourcing/Solved/18-Projections.MultiStream.OutOfOrder.Marten/18-Projections.MultiStream.OutOfOrder.Marten.csproj new file mode 100644 index 000000000..5143b2778 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/18-Projections.MultiStream.OutOfOrder.Marten/18-Projections.MultiStream.OutOfOrder.Marten.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Marten + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Projections.MultiStream.OutOfOrder.Marten/ProjectionsTests.cs b/Workshops/IntroductionToEventSourcing/Solved/18-Projections.MultiStream.OutOfOrder.Marten/ProjectionsTests.cs new file mode 100644 index 000000000..fdd4a9716 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/18-Projections.MultiStream.OutOfOrder.Marten/ProjectionsTests.cs @@ -0,0 +1,245 @@ +using FluentAssertions; +using JasperFx; +using JasperFx.Events.Projections; +using Marten; +using Marten.Events.Projections; +using Weasel.Core; +using Xunit; + +namespace IntroductionToEventSourcing.Projections.MultiStream.OutOfOrder.Marten; + +// EVENTS +public record PaymentRecorded( + Guid PaymentId, + Guid OrderId, + decimal Amount +); + +public record MerchantLimitsChecked( + Guid PaymentId, + Guid MerchantId, + bool IsWithinLimits +); + +public record FraudScoreCalculated( + Guid PaymentId, + decimal Score, + bool IsAcceptable +); + +// ENUMS +public enum VerificationStatus +{ + Pending, + Passed, + Failed +} + +public enum PaymentStatus +{ + Pending, + Approved, + Rejected +} + +public enum DataQuality +{ + Partial, + Sufficient, + Complete +} + +// READ MODEL +public class PaymentVerification +{ + public Guid Id { get; set; } + public Guid? OrderId { get; set; } + public decimal? Amount { get; set; } + public VerificationStatus MerchantLimitStatus { get; set; } + public VerificationStatus FraudStatus { get; set; } + public decimal FraudScore { get; set; } + public PaymentStatus Status { get; set; } + public DataQuality DataQuality { get; set; } + + public void Recalculate() + { + // Check if we have all required data + var hasMerchantCheck = MerchantLimitStatus != VerificationStatus.Pending; + var hasFraudCheck = FraudStatus != VerificationStatus.Pending; + var hasPaymentData = OrderId.HasValue && Amount.HasValue; + + // Update data quality + if (hasPaymentData && hasMerchantCheck && hasFraudCheck) + DataQuality = DataQuality.Complete; + else if (hasMerchantCheck || hasFraudCheck) + DataQuality = DataQuality.Sufficient; + else + DataQuality = DataQuality.Partial; + + // Derive status based on available data + // If either check failed, reject immediately (even if we're missing payment data) + if (MerchantLimitStatus == VerificationStatus.Failed || + FraudStatus == VerificationStatus.Failed) + { + Status = PaymentStatus.Rejected; + } + // If we have both checks and they passed, approve + else if (hasMerchantCheck && hasFraudCheck) + { + Status = PaymentStatus.Approved; + } + // Otherwise, still pending + else + { + Status = PaymentStatus.Pending; + } + } +} + +public class PaymentVerificationProjection: MultiStreamProjection +{ + public PaymentVerificationProjection() + { + Identity(e => e.PaymentId); + Identity(e => e.PaymentId); + Identity(e => e.PaymentId); + } + + public void Apply(PaymentVerification item, PaymentRecorded @event) + { + item.Id = @event.PaymentId; + item.OrderId = @event.OrderId; + item.Amount = @event.Amount; + + item.Recalculate(); + } + + public void Apply(PaymentVerification item, MerchantLimitsChecked @event) + { + item.Id = @event.PaymentId; + item.MerchantLimitStatus = @event.IsWithinLimits + ? VerificationStatus.Passed + : VerificationStatus.Failed; + + item.Recalculate(); + } + + public void Apply(PaymentVerification item, FraudScoreCalculated @event) + { + item.Id = @event.PaymentId; + item.FraudScore = @event.Score; + item.FraudStatus = @event.IsAcceptable + ? VerificationStatus.Passed + : VerificationStatus.Failed; + + item.Recalculate(); + } +} + +public class ProjectionsTests +{ + private const string ConnectionString = + "PORT = 5432; HOST = localhost; TIMEOUT = 15; POOLING = True; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"; + + [Fact] + [Trait("Category", "SkipCI")] + public async Task MultiStreamProjection_WithOutOfOrderEventsAndMarten_ShouldSucceed() + { + var payment1Id = Guid.CreateVersion7(); + var payment2Id = Guid.CreateVersion7(); + var payment3Id = Guid.CreateVersion7(); + var payment4Id = Guid.CreateVersion7(); + + var order1Id = Guid.CreateVersion7(); + var order2Id = Guid.CreateVersion7(); + var order3Id = Guid.CreateVersion7(); + var order4Id = Guid.CreateVersion7(); + + var merchant1Id = Guid.CreateVersion7(); + var merchant2Id = Guid.CreateVersion7(); + + var fraudCheck1Id = Guid.CreateVersion7(); + var fraudCheck2Id = Guid.CreateVersion7(); + var fraudCheck3Id = Guid.CreateVersion7(); + + await using var documentStore = DocumentStore.For(options => + { + options.Connection(ConnectionString); + options.DatabaseSchemaName = options.Events.DatabaseSchemaName = "Exercise18MultiStreamOutOfOrderMarten"; + options.AutoCreateSchemaObjects = AutoCreate.All; + + options.Projections.Add(ProjectionLifecycle.Inline); + }); + + await using var session = documentStore.LightweightSession(); + + // Payment 1: Approved — events arrive out of order (fraud score first!) + session.Events.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true)); + session.Events.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m)); + + // Payment 2: Merchant rejected — merchant check arrives first + session.Events.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false)); + session.Events.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true)); + session.Events.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m)); + + // Payment 3: Fraud rejected — payment recorded last + session.Events.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true)); + session.Events.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m)); + + // Payment 4: Pending — missing fraud check (payment recorded, merchant checked, but no fraud score yet) + session.Events.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m)); + session.Events.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true)); + + await session.SaveChangesAsync(); + + // Assert Payment 1: Approved (all data arrived, all checks passed) + var payment1 = await session.LoadAsync(payment1Id); + payment1.Should().NotBeNull(); + payment1!.Id.Should().Be(payment1Id); + payment1.OrderId.Should().Be(order1Id); + payment1.Amount.Should().Be(100m); + payment1.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment1.FraudStatus.Should().Be(VerificationStatus.Passed); + payment1.FraudScore.Should().Be(0.1m); + payment1.Status.Should().Be(PaymentStatus.Approved); + payment1.DataQuality.Should().Be(DataQuality.Complete); + + // Assert Payment 2: Merchant rejected + var payment2 = await session.LoadAsync(payment2Id); + payment2.Should().NotBeNull(); + payment2!.Id.Should().Be(payment2Id); + payment2.OrderId.Should().Be(order2Id); + payment2.Amount.Should().Be(5000m); + payment2.MerchantLimitStatus.Should().Be(VerificationStatus.Failed); + payment2.FraudStatus.Should().Be(VerificationStatus.Passed); + payment2.FraudScore.Should().Be(0.2m); + payment2.Status.Should().Be(PaymentStatus.Rejected); + payment2.DataQuality.Should().Be(DataQuality.Complete); + + // Assert Payment 3: Fraud rejected + var payment3 = await session.LoadAsync(payment3Id); + payment3.Should().NotBeNull(); + payment3!.Id.Should().Be(payment3Id); + payment3.OrderId.Should().Be(order3Id); + payment3.Amount.Should().Be(200m); + payment3.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment3.FraudStatus.Should().Be(VerificationStatus.Failed); + payment3.FraudScore.Should().Be(0.95m); + payment3.Status.Should().Be(PaymentStatus.Rejected); + payment3.DataQuality.Should().Be(DataQuality.Complete); + + // Assert Payment 4: Pending (waiting for fraud check) + var payment4 = await session.LoadAsync(payment4Id); + payment4.Should().NotBeNull(); + payment4!.Id.Should().Be(payment4Id); + payment4.OrderId.Should().Be(order4Id); + payment4.Amount.Should().Be(50m); + payment4.MerchantLimitStatus.Should().Be(VerificationStatus.Passed); + payment4.FraudStatus.Should().Be(VerificationStatus.Pending); + payment4.FraudScore.Should().Be(0m); + payment4.Status.Should().Be(PaymentStatus.Pending); + payment4.DataQuality.Should().Be(DataQuality.Sufficient); + } +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/15-EventsDefinition.csproj b/Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/19-EventsDefinition.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/15-EventsDefinition.csproj rename to Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/19-EventsDefinition.csproj diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/EventsDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/EventsDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/EventsDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/EventsDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/GroupCheckoutEvents.cs b/Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/GroupCheckoutEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/GroupCheckoutEvents.cs rename to Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/GroupCheckoutEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/GuestStayAccountEvents.cs b/Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/GuestStayAccountEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/GuestStayAccountEvents.cs rename to Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/GuestStayAccountEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/16-EntitiesDefinition.csproj b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/20-EntitiesDefinition.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/16-EntitiesDefinition.csproj rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/20-EntitiesDefinition.csproj diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/Database.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/EventCatcher.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/EventCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/EventCatcher.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/EventCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/EventEnvelope.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/EventEnvelope.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/EventEnvelope.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/EventStore.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/EventStore.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Core/EventStore.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Core/EventStore.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/Core/Aggregate.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/Core/Aggregate.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/Core/Aggregate.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/Core/Aggregate.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/GroupCheckouts/GroupCheckoutEvents.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/GroupCheckouts/GroupCheckoutEvents.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/GroupCheckouts/GroupCheckoutEvents.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/GroupCheckouts/GroupCheckoutEvents.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution1-Aggregates/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution1-Aggregates/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/Core/DictionaryExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/Core/DictionaryExtensions.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/Core/DictionaryExtensions.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/Core/DictionaryExtensions.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/Solution2-ImmutableEntities/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/17-BusinessProcesses.csproj b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/21-BusinessProcesses.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/17-BusinessProcesses.csproj rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/21-BusinessProcesses.csproj diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccountDecider.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccountDecider.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccountDecider.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayAccountDecider.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Choreography/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/CommandBus.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/CommandBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/CommandBus.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/CommandBus.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/Database.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/EventEnvelope.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/EventEnvelope.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/EventEnvelope.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/EventEnvelope.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/EventStore.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/EventStore.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/EventStore.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/EventStore.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/MessageCatcher.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/MessageCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Core/MessageCatcher.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Core/MessageCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/ProcessManagers/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/Core/Aggregate.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/Core/Aggregate.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/Core/Aggregate.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/Core/Aggregate.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/18-Idempotency.csproj b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/22-Idempotency.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/18-Idempotency.csproj rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/22-Idempotency.csproj diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/CommandBus.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/CommandBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/CommandBus.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/CommandBus.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/Database.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/EventBus.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/EventBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/EventBus.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/EventBus.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/MessageCatcher.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/MessageCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Core/MessageCatcher.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Core/MessageCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/Core/Aggregate.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version1-Aggregates/IdempotencyTests.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/18-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/22-Idempotency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/19-Consistency.csproj b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/23-Consistency.csproj similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/19-Consistency.csproj rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/23-Consistency.csproj diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/CommandBus.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/CommandBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/CommandBus.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/CommandBus.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/Database.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/Database.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/Database.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/Database.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/EventBus.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/EventBus.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/EventBus.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/EventBus.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/MessageCatcher.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/MessageCatcher.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/MessageCatcher.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/MessageCatcher.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/TimeoutException.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/TimeoutException.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Core/TimeoutException.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Core/TimeoutException.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs similarity index 98% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs index aefee6e75..a78c171e6 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/BusinessProcessTests.cs @@ -18,6 +18,7 @@ namespace Consistency.Sagas.Version1_Aggregates; public class BusinessProcessTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_ShouldComplete() { // Given; @@ -57,6 +58,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_Sho } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldComplete() { // Given; @@ -106,6 +108,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldCom } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettled_ShouldFail() { // Given; @@ -164,6 +167,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettle [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllUnsettled_ShouldFail() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/Core/Aggregate.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/Core/Retry.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/Core/Retry.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/Core/Retry.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/Core/Retry.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs index 83cb84be7..c2a61d305 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/EntityDefinitionTests.cs @@ -15,6 +15,7 @@ namespace Consistency.Sagas.Version1_Aggregates; public class EntityDefinitionTests { [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingInGuest_Succeeds() { // Given @@ -30,6 +31,7 @@ public async Task CheckingInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingChargeForCheckedInGuest_Succeeds() { // Given @@ -48,6 +50,7 @@ public async Task RecordingChargeForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuest_Succeeds() { // Given @@ -66,6 +69,7 @@ public async Task RecordingPaymentForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() { // Given @@ -85,6 +89,7 @@ public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_Succeeds() { // Given @@ -106,6 +111,7 @@ public async Task CheckingOutGuestWithSettledBalance_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed() { // Given @@ -134,6 +140,7 @@ public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFaile } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStay_ShouldBeInitiated() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs similarity index 97% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs index 7658a49f0..68331ce95 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version1-Aggregates/IdempotencyTests.cs @@ -15,6 +15,7 @@ namespace Consistency.Sagas.Version1_Aggregates; public class IdempotencyTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutIsInitiatedOnceForTheSameId() { // Given @@ -35,6 +36,7 @@ public async Task GroupCheckoutIsInitiatedOnceForTheSameId() } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutCompletion() { // Given @@ -59,6 +61,7 @@ await groupCheckoutFacade.InitiateGroupCheckout(new InitiateGroupCheckout(groupC } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutFailure() { // Given diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs similarity index 98% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs index 4f65b5f12..223dfeebe 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/BusinessProcessTests.cs @@ -17,6 +17,7 @@ namespace Consistency.Sagas.Version2_ImmutableEntities; public class BusinessProcessTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_ShouldComplete() { // Given; @@ -56,6 +57,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithoutPaymentsAndCharges_Sho } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldComplete() { // Given; @@ -105,6 +107,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithAllStaysSettled_ShouldCom } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettled_ShouldFail() { // Given; @@ -163,6 +166,7 @@ public async Task GroupCheckoutForMultipleGuestStayWithOneSettledAndRestUnsettle [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutForMultipleGuestStayWithAllUnsettled_ShouldFail() { // Given; diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/Core/DictionaryExtensions.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs similarity index 96% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs index 3f7012db5..7ef2f7a82 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/EntityDefinitionTests.cs @@ -13,6 +13,7 @@ namespace Consistency.Sagas.Version2_ImmutableEntities; public class EntityDefinitionTests { [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingInGuest_Succeeds() { // Given @@ -28,6 +29,7 @@ public async Task CheckingInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingChargeForCheckedInGuest_Succeeds() { // Given @@ -46,6 +48,7 @@ public async Task RecordingChargeForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuest_Succeeds() { // Given @@ -64,6 +67,7 @@ public async Task RecordingPaymentForCheckedInGuest_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() { // Given @@ -83,6 +87,7 @@ public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_Succeeds() { // Given @@ -104,6 +109,7 @@ public async Task CheckingOutGuestWithSettledBalance_Succeeds() } [Fact] + [Trait("Category", "SkipCI")] public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed() { // Given diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckout.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutSaga.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GroupCheckouts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GroupCheckoutsConfig.cs diff --git a/Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/Solved/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayAccount.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs similarity index 100% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/GuestStayAccounts/GuestStayFacade.cs diff --git a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs similarity index 97% rename from Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs index d50bd4575..ddfcbec71 100644 --- a/Workshops/IntroductionToEventSourcing/19-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/23-Consistency/Sagas/Version2-ImmutableEntities/IdempotencyTests.cs @@ -15,6 +15,7 @@ namespace Consistency.Sagas.Version2_ImmutableEntities; public class IdempotencyTests { [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutIsInitiatedOnceForTheSameId() { // Given @@ -35,6 +36,7 @@ public async Task GroupCheckoutIsInitiatedOnceForTheSameId() } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutCompletion() { // Given @@ -59,6 +61,7 @@ await groupCheckoutFacade.InitiateGroupCheckout(new InitiateGroupCheckout(groupC } [Fact] + [Trait("Category", "SkipCI")] public async Task GroupCheckoutRecordsOnceGuestCheckoutFailure() { // Given