Skip to content

Commit f7de114

Browse files
committed
Added Multi stream projection exercise to introduction to Event Sourcing workshop
Renamed also the further exercises
1 parent 1b536c5 commit f7de114

File tree

221 files changed

+618
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

221 files changed

+618
-12
lines changed

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(ls:*)"
5+
]
6+
}
7+
}

EventSourcing.NetCore.slnx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,10 @@
251251
<Project Path="Workshops/IntroductionToEventSourcing/12-Projections.SingleStream/12-Projections.SingleStream.csproj" />
252252
<Project Path="Workshops/IntroductionToEventSourcing/13-Projections.SingleStream.Idempotency/13-Projections.SingleStream.Idempotency.csproj" />
253253
<Project Path="Workshops/IntroductionToEventSourcing/14-Projections.SingleStream.EventualConsistency/14-Projections.SingleStream.EventualConsistency.csproj" />
254-
<Project Path="Workshops/IntroductionToEventSourcing/15-EventsDefinition/15-EventsDefinition.csproj" />
255-
<Project Path="Workshops/IntroductionToEventSourcing/16-EntitiesDefinition/16-EntitiesDefinition.csproj" />
256-
<Project Path="Workshops/IntroductionToEventSourcing/17-BusinessProcesses/17-BusinessProcesses.csproj" />
254+
<Project Path="Workshops/IntroductionToEventSourcing/15-Projections.MultiStream/15-Projections.MultiStream.csproj" />
255+
<Project Path="Workshops/IntroductionToEventSourcing/19-EventsDefinition/19-EventsDefinition.csproj" />
256+
<Project Path="Workshops/IntroductionToEventSourcing/20-EntitiesDefinition/20-EntitiesDefinition.csproj" />
257+
<Project Path="Workshops/IntroductionToEventSourcing/21-BusinessProcesses/21-BusinessProcesses.csproj" />
257258
</Folder>
258259
<Folder Name="/Workshops/IntroductionToEventSourcing/docker/">
259260
<File Path="Workshops/IntroductionToEventSourcing/docker-compose.yml" />
@@ -278,9 +279,10 @@
278279
<Project Path="Workshops/IntroductionToEventSourcing/Solved/12-Projections.SingleStream/12-Projections.SingleStream.csproj" />
279280
<Project Path="Workshops/IntroductionToEventSourcing/Solved/13-Projections.SingleStream.Idempotency/13-Projections.SingleStream.Idempotency.csproj" />
280281
<Project Path="Workshops/IntroductionToEventSourcing/Solved/14-Projections.SingleStream.EventualConsistency/14-Projections.SingleStream.EventualConsistency.csproj" />
281-
<Project Path="Workshops/IntroductionToEventSourcing/Solved/15-EventsDefinition/15-EventsDefinition.csproj" />
282-
<Project Path="Workshops/IntroductionToEventSourcing/Solved/16-EntitiesDefinition/16-EntitiesDefinition.csproj" />
283-
<Project Path="Workshops/IntroductionToEventSourcing/Solved/17-BusinessProcesses/17-BusinessProcesses.csproj" />
282+
<Project Path="Workshops/IntroductionToEventSourcing/Solved/15-Projections.MultiStream/15-Projections.MultiStream.csproj" />
283+
<Project Path="Workshops/IntroductionToEventSourcing/Solved/19-EventsDefinition/19-EventsDefinition.csproj" />
284+
<Project Path="Workshops/IntroductionToEventSourcing/Solved/20-EntitiesDefinition/20-EntitiesDefinition.csproj" />
285+
<Project Path="Workshops/IntroductionToEventSourcing/Solved/21-BusinessProcesses/21-BusinessProcesses.csproj" />
284286
</Folder>
285287
<Properties Name="RiderSharedRunConfigurations" Scope="PostLoad" />
286288
</Solution>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<RootNamespace>IntroductionToEventSourcing.Projections.MultiStream</RootNamespace>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="FluentAssertions" />
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
11+
<PackageReference Include="GitHubActionsTestLogger">
12+
<PrivateAssets>all</PrivateAssets>
13+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
14+
</PackageReference>
15+
<PackageReference Include="xunit" />
16+
<PackageReference Include="xunit.runner.visualstudio">
17+
<PrivateAssets>all</PrivateAssets>
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
19+
</PackageReference>
20+
<PackageReference Include="coverlet.collector">
21+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
22+
<PrivateAssets>all</PrivateAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using FluentAssertions;
2+
using IntroductionToEventSourcing.Projections.MultiStream.Tools;
3+
using Xunit;
4+
5+
namespace IntroductionToEventSourcing.Projections.MultiStream;
6+
7+
// EVENTS
8+
public record PaymentRecorded(
9+
Guid PaymentId,
10+
Guid OrderId,
11+
decimal Amount
12+
);
13+
14+
public record MerchantLimitsChecked(
15+
Guid PaymentId,
16+
Guid MerchantId,
17+
bool IsWithinLimits
18+
);
19+
20+
public record FraudScoreCalculated(
21+
Guid PaymentId,
22+
decimal Score,
23+
bool IsAcceptable
24+
);
25+
26+
public record PaymentVerificationCompleted(
27+
Guid PaymentId,
28+
bool IsApproved
29+
);
30+
31+
// ENUMS
32+
public enum VerificationStatus
33+
{
34+
Pending,
35+
Passed,
36+
Failed
37+
}
38+
39+
public enum PaymentStatus
40+
{
41+
Pending,
42+
Approved,
43+
Rejected
44+
}
45+
46+
// READ MODEL
47+
public class PaymentVerification
48+
{
49+
public Guid Id { get; set; }
50+
public PaymentStatus Status { get; set; }
51+
}
52+
53+
public class ProjectionsTests
54+
{
55+
[Fact]
56+
[Trait("Category", "SkipCI")]
57+
public void MultiStreamProjection_ForPaymentVerification_ShouldSucceed()
58+
{
59+
var payment1Id = Guid.CreateVersion7();
60+
var payment2Id = Guid.CreateVersion7();
61+
var payment3Id = Guid.CreateVersion7();
62+
var payment4Id = Guid.CreateVersion7();
63+
64+
var order1Id = Guid.CreateVersion7();
65+
var order2Id = Guid.CreateVersion7();
66+
var order3Id = Guid.CreateVersion7();
67+
var order4Id = Guid.CreateVersion7();
68+
69+
var merchant1Id = Guid.CreateVersion7();
70+
var merchant2Id = Guid.CreateVersion7();
71+
72+
var fraudCheck1Id = Guid.CreateVersion7();
73+
var fraudCheck2Id = Guid.CreateVersion7();
74+
var fraudCheck3Id = Guid.CreateVersion7();
75+
76+
var eventStore = new EventStore();
77+
var database = new Database();
78+
79+
// TODO:
80+
// 1. Create a PaymentVerificationProjection class that handles each event type.
81+
// Events arrive on different streams (payment, merchant, fraud check),
82+
// but they share PaymentId — use PaymentId as the read model key.
83+
// 2. Register your event handlers using `eventStore.Register`.
84+
85+
// Payment 1: Approved — all checks pass
86+
eventStore.Append(payment1Id, new PaymentRecorded(payment1Id, order1Id, 100m));
87+
eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment1Id, merchant1Id, true));
88+
eventStore.Append(fraudCheck1Id, new FraudScoreCalculated(payment1Id, 0.1m, true));
89+
eventStore.Append(payment1Id, new PaymentVerificationCompleted(payment1Id, true));
90+
91+
// Payment 2: Merchant rejected — exceeds merchant limits
92+
eventStore.Append(payment2Id, new PaymentRecorded(payment2Id, order2Id, 5000m));
93+
eventStore.Append(merchant2Id, new MerchantLimitsChecked(payment2Id, merchant2Id, false));
94+
eventStore.Append(fraudCheck2Id, new FraudScoreCalculated(payment2Id, 0.2m, true));
95+
eventStore.Append(payment2Id, new PaymentVerificationCompleted(payment2Id, false));
96+
97+
// Payment 3: Fraud rejected — high fraud score
98+
eventStore.Append(payment3Id, new PaymentRecorded(payment3Id, order3Id, 200m));
99+
eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment3Id, merchant1Id, true));
100+
eventStore.Append(fraudCheck3Id, new FraudScoreCalculated(payment3Id, 0.95m, false));
101+
eventStore.Append(payment3Id, new PaymentVerificationCompleted(payment3Id, false));
102+
103+
// Payment 4: Pending — still awaiting fraud check and final decision
104+
eventStore.Append(payment4Id, new PaymentRecorded(payment4Id, order4Id, 50m));
105+
eventStore.Append(merchant1Id, new MerchantLimitsChecked(payment4Id, merchant1Id, true));
106+
107+
// Assert Payment 1: Approved
108+
var payment1 = database.Get<PaymentVerification>(payment1Id)!;
109+
payment1.Should().NotBeNull();
110+
payment1.Id.Should().Be(payment1Id);
111+
payment1.Status.Should().Be(PaymentStatus.Approved);
112+
113+
// Assert Payment 2: Merchant rejected
114+
var payment2 = database.Get<PaymentVerification>(payment2Id)!;
115+
payment2.Should().NotBeNull();
116+
payment2.Id.Should().Be(payment2Id);
117+
payment2.Status.Should().Be(PaymentStatus.Rejected);
118+
119+
// Assert Payment 3: Fraud rejected
120+
var payment3 = database.Get<PaymentVerification>(payment3Id)!;
121+
payment3.Should().NotBeNull();
122+
payment3.Id.Should().Be(payment3Id);
123+
payment3.Status.Should().Be(PaymentStatus.Rejected);
124+
125+
// Assert Payment 4: Pending
126+
var payment4 = database.Get<PaymentVerification>(payment4Id)!;
127+
payment4.Should().NotBeNull();
128+
payment4.Id.Should().Be(payment4Id);
129+
payment4.Status.Should().Be(PaymentStatus.Pending);
130+
}
131+
}
Lines changed: 29 additions & 0 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Text.Json;
2+
3+
namespace IntroductionToEventSourcing.Projections.MultiStream.Tools;
4+
5+
public class Database
6+
{
7+
private readonly Dictionary<string, object> storage = new();
8+
9+
public void Store<T>(Guid id, T obj) where T: class =>
10+
storage[GetId<T>(id)] = obj;
11+
12+
public void Delete<T>(Guid id) =>
13+
storage.Remove(GetId<T>(id));
14+
15+
public T? Get<T>(Guid id) where T: class =>
16+
storage.TryGetValue(GetId<T>(id), out var result) ?
17+
// Clone to simulate getting new instance on loading
18+
JsonSerializer.Deserialize<T>(JsonSerializer.Serialize((T)result))
19+
: null;
20+
21+
private static string GetId<T>(Guid id) => $"{typeof(T).Name}-{id}";
22+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace IntroductionToEventSourcing.Projections.MultiStream.Tools;
2+
3+
public record EventMetadata(
4+
string EventId,
5+
ulong StreamPosition,
6+
ulong LogPosition
7+
)
8+
{
9+
public static EventMetadata For(ulong streamPosition, ulong logPosition) =>
10+
new(Guid.CreateVersion7().ToString(), streamPosition, logPosition);
11+
}
12+
13+
public record EventEnvelope(
14+
object Data,
15+
EventMetadata Metadata
16+
);
17+
18+
public record EventEnvelope<T>(
19+
T Data,
20+
EventMetadata Metadata
21+
): EventEnvelope(Data, Metadata) where T : notnull
22+
{
23+
public new T Data => (T)base.Data;
24+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace IntroductionToEventSourcing.Projections.MultiStream.Tools;
2+
3+
public class EventStore
4+
{
5+
private readonly Dictionary<Type, List<Action<EventEnvelope>>> handlers = new();
6+
private readonly Dictionary<Guid, List<EventEnvelope>> events = new();
7+
8+
public void Register<TEvent>(Action<EventEnvelope<TEvent>> handler) where TEvent : notnull
9+
{
10+
var eventType = typeof(TEvent);
11+
12+
void WrappedHandler(object @event) => handler((EventEnvelope<TEvent>)@event);
13+
14+
if (handlers.TryGetValue(eventType, out var handler1))
15+
handler1.Add(WrappedHandler);
16+
else
17+
handlers.Add(eventType, [WrappedHandler]);
18+
}
19+
20+
public void Append<TEvent>(Guid streamId, TEvent @event) where TEvent : notnull
21+
{
22+
if (!events.ContainsKey(streamId))
23+
events[streamId] = [];
24+
25+
var eventEnvelope = new EventEnvelope<TEvent>(@event,
26+
EventMetadata.For(
27+
(ulong)events[streamId].Count + 1,
28+
(ulong)events.Values.Sum(s => s.Count)
29+
)
30+
);
31+
32+
events[streamId].Add(eventEnvelope);
33+
34+
if (!handlers.TryGetValue(eventEnvelope.Data.GetType(), out var eventHandlers)) return;
35+
36+
foreach (var handle in eventHandlers)
37+
{
38+
handle(eventEnvelope);
39+
}
40+
}
41+
}

Workshops/IntroductionToEventSourcing/15-EventsDefinition/15-EventsDefinition.csproj renamed to Workshops/IntroductionToEventSourcing/19-EventsDefinition/19-EventsDefinition.csproj

File renamed without changes.

Workshops/IntroductionToEventSourcing/15-EventsDefinition/EventsDefinitionTests.cs renamed to Workshops/IntroductionToEventSourcing/19-EventsDefinition/EventsDefinitionTests.cs

File renamed without changes.

0 commit comments

Comments
 (0)