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