https://github.com/hona/verticalslicearchitecture.samples.eventsourcingmartendb
https://github.com/hona/verticalslicearchitecture.samples.eventsourcingmartendb
Last synced: 3 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/hona/verticalslicearchitecture.samples.eventsourcingmartendb
- Owner: Hona
- License: mit
- Created: 2024-08-06T03:08:43.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2024-08-07T02:18:00.000Z (10 months ago)
- Last Synced: 2025-03-06T23:11:29.615Z (3 months ago)
- Language: C#
- Size: 23.4 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Vertical Slice Architecture with Event Sourcing using Marten DB
> The goal of this sample is to determine the viability for me to use Event Sourcing + Marten DB on https://tf2jump.xyz
## Domain
Here there are a few main ideas, from the TF2 Jump real world example (https://github.com/Hona/TF2Jump.xyz)
Imagine there are `Map`s. Each `Map` can be speedran by `Player`s. Each `Player` can run many `Map`s.
Here are the events:
```csharp
public record MapAddedEvent(Guid MapId, string MapName);
public record PlayerRegisteredEvent(Guid PlayerId, string PlayerName);
public record PlayerAchievedMapRecordEvent(Guid MapId, Guid PlayerId, TimeSpan Duration);
```We then end up with the write aggregate model:
```csharp
public sealed record MapAggregate(Guid Id, string MapName, List Records)
{
public static MapAggregate Create(MapAddedEvent added) => new(added.MapId, added.MapName, []);public static MapAggregate Apply(
PlayerAchievedMapRecordEvent playerAchievedRecord,
MapAggregate mapAggregate
) =>
mapAggregate with
{
Records = RecordService.TryInsertOrUpdateRecord(
mapAggregate.Records,
playerAchievedRecord
)
};
}
```Note: There is no write model for Players, as in this sample there is no need for business rules inside a DDD context.
Then, a number of projections for all query (read) operations.
```csharp
public class PlayerProjection
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public List Records { get; set; } = [];
}public class PlayerProjector : MultiStreamProjection
{
public PlayerProjector()
{
Identity(x => x.PlayerId);
Identity(x => x.PlayerId);
}public void Apply(PlayerRegisteredEvent @event, PlayerProjection view)
{
view.Id = @event.PlayerId;
view.Name = @event.PlayerName;
view.Records = [];
}public void Apply(PlayerAchievedMapRecordEvent @event, PlayerProjection view)
{
view.Records = RecordService.TryInsertOrUpdateRecord(view.Records, @event);
}
}```
Marten is configured
```csharp
services.AddMarten(options =>
{
options.Connection(connectionString);
options.DatabaseSchemaName = "eventsourcedsandbox";
options.AutoCreateSchemaObjects = AutoCreate.All;options.Projections.Add(ProjectionLifecycle.Inline);
options.Events.AddEventType();
options.Events.AddEventType();
options.Events.AddEventType();
});
```We can query the Aggregate (for writes only) like so
```csharp
var response = await session.Events.AggregateStreamAsync(
request.MapId,
token: cancellationToken
);
```Projections can be treated like traditional Marten documents:
```csharp
var response = await session.LoadAsync(
request.PlayerId,
cancellationToken
);
```A VSA endpoint/slice
```csharp
using EventSourcedSandbox.Domain.Players;
using Marten;namespace EventSourcedSandbox.Features.Players;
internal sealed record ViewPlayerRequest(Guid PlayerId);
internal sealed class ViewPlayerQuery(IQuerySession session)
: Endpoint, NotFound>>
{
public override void Configure()
{
Get($"/players/{{{nameof(ViewPlayerRequest.PlayerId)}}}");
AllowAnonymous();
}public override async Task HandleAsync(
ViewPlayerRequest request,
CancellationToken cancellationToken
)
{
var response = await session.LoadAsync(
request.PlayerId,
cancellationToken
);if (response is null)
{
await SendResultAsync(TypedResults.NotFound());
return;
}await SendResultAsync(TypedResults.Ok(response));
}
}
```Note that there is a nice synergy with CQRS, VSA and Event Sourcing. The `ViewPlayerQuery` is a query endpoint, and the `PlayerProjector` is a projection. The `MapAggregate` is a write model.
Additionally, if you want to see the seed data I'm using for testing:
```csharp
public async Task SeedSampleDataAsync(IServiceProvider services)
{
using var scope = services.CreateScope();var store = scope.ServiceProvider.GetRequiredService();
await store.Advanced.ResetAllData();
await using var session = store.LightweightSession();
var rootTimeStamp = DateTimeOffset.Now;
// Add some maps
var map1 = Guid.NewGuid();
var map2 = Guid.NewGuid();var mapRegistered1 = new MapAddedEvent(map1, "jump_beef");
var mapRegistered2 = new MapAddedEvent(map2, "jump_ice");session.Events.StartStream(map1, mapRegistered1);
session.Events.StartStream(map2, mapRegistered2);// Save the pending changes to db
await session.SaveChangesAsync();// Add some players
var player1 = Guid.NewGuid();
var player2 = Guid.NewGuid();
var player3 = Guid.NewGuid();
var player4 = Guid.NewGuid();
var player5 = Guid.NewGuid();var playerRegistered1 = new PlayerRegisteredEvent(player1, "Alice");
var playerRegistered2 = new PlayerRegisteredEvent(player2, "Bob");
var playerRegistered3 = new PlayerRegisteredEvent(player3, "Charlie");
var playerRegistered4 = new PlayerRegisteredEvent(player4, "David");
var playerRegistered5 = new PlayerRegisteredEvent(player5, "Eve");session.Events.StartStream(player1, playerRegistered1);
session.Events.StartStream(player2, playerRegistered2);
session.Events.StartStream(player3, playerRegistered3);
session.Events.StartStream(player4, playerRegistered4);
session.Events.StartStream(player5, playerRegistered5);// Save the pending changes to db
await session.SaveChangesAsync();// Register some records
var record1 = new PlayerAchievedMapRecordEvent(map1, player1, TimeSpan.FromSeconds(1000));
var record2 = new PlayerAchievedMapRecordEvent(map1, player2, TimeSpan.FromSeconds(900));
var record3 = new PlayerAchievedMapRecordEvent(map1, player3, TimeSpan.FromSeconds(800));
var record4 = new PlayerAchievedMapRecordEvent(map1, player4, TimeSpan.FromSeconds(700));
var record5 = new PlayerAchievedMapRecordEvent(map1, player5, TimeSpan.FromSeconds(600));
var record6 = new PlayerAchievedMapRecordEvent(map2, player1, TimeSpan.FromSeconds(500));
var record7 = new PlayerAchievedMapRecordEvent(map2, player2, TimeSpan.FromSeconds(400));
var record8 = new PlayerAchievedMapRecordEvent(map2, player3, TimeSpan.FromSeconds(300));
var record9 = new PlayerAchievedMapRecordEvent(map2, player4, TimeSpan.FromSeconds(200));
var record10 = new PlayerAchievedMapRecordEvent(map2, player5, TimeSpan.FromSeconds(100));session.Events.Append(map1, record1);
session.Events.Append(map1, record2);
session.Events.Append(map1, record3);
session.Events.Append(map1, record4);
session.Events.Append(map1, record5);
session.Events.Append(map2, record6);
session.Events.Append(map2, record7);
session.Events.Append(map2, record8);
session.Events.Append(map2, record9);
session.Events.Append(map2, record10);// Save the pending changes to db
await session.SaveChangesAsync();
}
```