Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4513be6
added SnapshotTypeMap and source generator for types registration
AlexTroshkin Nov 28, 2025
82536dc
stream reading with snapshot support
AlexTroshkin Nov 28, 2025
9c7a110
fix ReadStreamAfterSnapshot
AlexTroshkin Nov 29, 2025
1537602
added sample to demonstrate the use of snapshots
AlexTroshkin Nov 29, 2025
abf7c3f
fix withdraw endpoint
AlexTroshkin Nov 29, 2025
8cfd469
change get snapshot types method
AlexTroshkin Nov 29, 2025
7f46c76
added snapshot storage strategy
AlexTroshkin Dec 7, 2025
a3a060b
enhance SnapshotMappingsGenerator and SnapshotTypeMap to support stor…
AlexTroshkin Dec 7, 2025
430b8d9
add ISnapshotStore interface and Snapshot record for snapshot management
AlexTroshkin Dec 7, 2025
fa96de6
implement ISnapshotStore support in LoadAggregate and LoadState funct…
AlexTroshkin Dec 7, 2025
f9bd21b
fix build
AlexTroshkin Dec 7, 2025
5771bb9
add ISnapshotStore support to CommandService and FunctionalCommandSer…
AlexTroshkin Dec 7, 2025
1c614d7
bug fixes for the SeparateStream storage strategy
AlexTroshkin Dec 7, 2025
dbba34a
added postgres snapshot store
AlexTroshkin Dec 8, 2025
4a6bec3
moved snapshot scripts to separate folder
AlexTroshkin Dec 8, 2025
0c6ce31
fix snapshot store drilling
AlexTroshkin Dec 8, 2025
13d1248
added code demonstrating the use of snapshots in a separate storage (…
AlexTroshkin Dec 8, 2025
1b177a9
moved code related to snapshots to appropriate folder
AlexTroshkin Dec 8, 2025
241b86a
added `UseSnapshotStrategy` to command services
AlexTroshkin Dec 8, 2025
0a24b03
added sqlserver snapshot store
AlexTroshkin Dec 9, 2025
e0db97c
added mongodb snapshot store
AlexTroshkin Dec 9, 2025
5286bb8
fixed snapshot reading from mongodb
AlexTroshkin Dec 9, 2025
4a1720e
fixed sqlserver duplicate constraint name
AlexTroshkin Dec 9, 2025
9a48a0c
added redis snapshot store
AlexTroshkin Dec 9, 2025
cdac775
fixed snapshotstore capture
AlexTroshkin Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
<TUnitVersion>0.77.3</TUnitVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="13.0.2" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="13.0.2" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="13.0.2" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="13.0.2" />
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.KurrentDB" Version="13.0.0" />
<PackageVersion Include="CommunityToolkit.Aspire.KurrentDB" Version="13.0.0" />
<PackageVersion Include="FluentValidation" Version="12.0.0" />
<PackageVersion Include="KurrentDB.Client" Version="1.2.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftExtensionsVer)" />
Expand Down
6 changes: 6 additions & 0 deletions Eventuous.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
</Folder>
<Folder Name="/Mongo/" />
<Folder Name="/Mongo/src/">
<Project Path="src/Mongo/src/Eventuous.MongoDB/Eventuous.MongoDB.csproj" />
<Project Path="src/Mongo/src/Eventuous.Projections.MongoDB/Eventuous.Projections.MongoDB.csproj" />
</Folder>
<Folder Name="/Mongo/test/">
Expand All @@ -140,6 +141,11 @@
<Project Path="src/SqlServer/test/Eventuous.Tests.SqlServer/Eventuous.Tests.SqlServer.csproj" />
</Folder>
<Folder Name="/Samples/" />
<Folder Name="/Samples/Banking/">
<Project Path="samples/banking/Banking.Api/Banking.Api.csproj" />
<Project Path="samples/banking/Banking.AppHost/Banking.AppHost.csproj" />
<Project Path="samples/banking/Banking.Domain/Banking.Domain.csproj" />
</Folder>
<Folder Name="/Samples/Esdb/">
<Project Path="samples/esdb/Bookings.Domain/Bookings.Domain.csproj" />
<Project Path="samples/esdb/Bookings.Payments/Bookings.Payments.csproj" />
Expand Down
25 changes: 25 additions & 0 deletions samples/banking/Banking.Api/Banking.Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.Aspire.KurrentDB" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(SrcRoot)\Core\src\Eventuous.Application\Eventuous.Application.csproj" />
<ProjectReference Include="$(SrcRoot)\Core\gen\Eventuous.Shared.Generators\Eventuous.Shared.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="$(SrcRoot)\Extensions\src\Eventuous.Extensions.DependencyInjection\Eventuous.Extensions.DependencyInjection.csproj" />
<ProjectReference Include="$(SrcRoot)\KurrentDB\src\Eventuous.KurrentDB\Eventuous.KurrentDB.csproj" />
<ProjectReference Include="$(SrcRoot)\Postgres\src\Eventuous.Postgresql\Eventuous.Postgresql.csproj" />
<ProjectReference Include="$(SrcRoot)\SqlServer\src\Eventuous.SqlServer\Eventuous.SqlServer.csproj" />
<ProjectReference Include="$(SrcRoot)\Mongo\src\Eventuous.MongoDB\Eventuous.MongoDB.csproj" />
<ProjectReference Include="$(SrcRoot)\Redis\src\Eventuous.Redis\Eventuous.Redis.csproj" />
<ProjectReference Include="..\Banking.Domain\Banking.Domain.csproj" />
</ItemGroup>

</Project>
65 changes: 65 additions & 0 deletions samples/banking/Banking.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Banking.Api.Services;
using Banking.Domain.Accounts;
using Eventuous.KurrentDB;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

//----------------------------------------------------------------
// Snapshot store registration

var postgresSnapshotDbConnectionString = builder.Configuration.GetConnectionString("postgresSnapshotsDb");
if (postgresSnapshotDbConnectionString == null) {
throw new InvalidOperationException("postgres snapshots db conenction string should be not null");
}

//builder.Services.AddPostgresSnapshotStore(postgresSnapshotDbConnectionString, initializeDatabase: true);

var sqlServerSnapshotsDbConnectionString = builder.Configuration.GetConnectionString("sqlServerSnapshotsDb");
if (sqlServerSnapshotsDbConnectionString == null) {
throw new InvalidOperationException("sqlServer snapshots db connection string should be not null");
}

//builder.Services.AddSqlServerSnapshotStore(sqlServerSnapshotsDbConnectionString, initializeDatabase: true);

var mongoDbSnapshotsDbConnectionString = builder.Configuration.GetConnectionString("mongoDbSnapshotsDb");
if (mongoDbSnapshotsDbConnectionString == null) {
throw new InvalidOperationException("mongodb snapshots db connection string should be not null");
}

//builder.Services.AddMongoSnapshotStore(mongoDbSnapshotsDbConnectionString, initializeIndexes: true);

var redisConnectionString = builder.Configuration.GetConnectionString("redis");
if (redisConnectionString == null) {
throw new InvalidOperationException("redis connection string should be not null");
}

builder.Services.AddRedisSnapshotStore(redisConnectionString, database: 0);

//----------------------------------------------------------------
// Event store registration

builder.AddKurrentDBClient("kurrentdb");
builder.Services.AddEventStore<KurrentDBEventStore>();

//----------------------------------------------------------------

builder.Services.AddCommandService<AccountService, AccountState>();

var app = builder.Build();

app.MapGet("/accounts/{id}/deposit/{amount}", async ([FromRoute] string id, [FromRoute] decimal amount, [FromServices] AccountService accountService) => {
var cmd = new AccountService.Deposit(id, amount);
var res = await accountService.Handle(cmd, default);

return res.Match<object>(ok => ok, err => err);
});

app.MapGet("/accounts/{id}/withdraw/{amount}", async ([FromRoute] string id, [FromRoute] decimal amount, [FromServices] AccountService accountService) => {
var cmd = new AccountService.Withdraw(id, amount);
var res = await accountService.Handle(cmd, default);

return res.Match<object>(ok => ok, err => err);
});

app.Run();
35 changes: 35 additions & 0 deletions samples/banking/Banking.Api/Services/AccountService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (C) Eventuous HQ OÜ. All rights reserved
// Licensed under the Apache License, Version 2.0.

using Banking.Domain.Accounts;
using Eventuous;

namespace Banking.Api.Services;

public class AccountService : CommandService<AccountState> {
public record Deposit(string AccountId, decimal Amount);
public record Withdraw(string AccountId, decimal Amount);

public AccountService(IEventStore store, ISnapshotStore snapshotStore) : base(store, snapshotStore: snapshotStore) {

UseSnapshotStrategy(
predicate: (events, _) => events.Count() >= 5,
produce: (_, state) => new AccountEvents.V1.Snapshot(state.Balance));

On<Deposit>()
.InState(ExpectedState.Any)
.GetStream(cmd => StreamName.ForState<AccountState>(cmd.AccountId))
.Act((_, __, cmd) => [new AccountEvents.V1.Deposited(cmd.Amount)]);

On<Withdraw>()
.InState(ExpectedState.Any)
.GetStream(cmd => StreamName.ForState<AccountState>(cmd.AccountId))
.Act(static (state, __, cmd) => {
if (state.Balance < cmd.Amount) {
throw new InvalidOperationException();
}

return [new AccountEvents.V1.Withdrawn(cmd.Amount)];
});
}
}
8 changes: 8 additions & 0 deletions samples/banking/Banking.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/banking/Banking.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
37 changes: 37 additions & 0 deletions samples/banking/Banking.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
var builder = DistributedApplication.CreateBuilder(args);

var kurrentdb = builder.AddKurrentDB("eventuous-kurrentdb", 2113)
.WithEnvironment("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true");

var postgres = builder.AddPostgres("eventuous-postgres")
.WithPgWeb();

var postgresSnapshotsDb = postgres.AddDatabase("snapshots");

var sqlServer = builder.AddSqlServer("eventuous-sqlserver");
var sqlServerSnapshotsDb = sqlServer.AddDatabase("eventuous-sqlserver-snapshotsdb", "snapshots");

var mongodb = builder.AddMongoDB("eventuous-mongodb")
.WithMongoExpress();

var mongodbSnapshotsDb = mongodb.AddDatabase("eventuous-mongodb-snapshotsdb", "snapshots");

var redis = builder.AddRedis("eventuous-redis")
.WithRedisInsight();

builder
.AddProject<Projects.Banking_Api>("banking-api")
.WithReference(kurrentdb, "kurrentdb")
.WaitFor(kurrentdb)
.WithReference(postgresSnapshotsDb, "postgresSnapshotsDb")
.WaitFor(postgresSnapshotsDb)
.WithReference(sqlServerSnapshotsDb, "sqlServerSnapshotsDb")
.WaitFor(sqlServerSnapshotsDb)
.WithReference(mongodbSnapshotsDb, "mongoDbSnapshotsDb")
.WaitFor(mongodbSnapshotsDb)
.WithReference(redis, "redis")
.WaitFor(redis);

builder
.Build()
.Run();
23 changes: 23 additions & 0 deletions samples/banking/Banking.AppHost/Banking.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Aspire.AppHost.Sdk/13.0.1">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>3f5f156a-5139-482c-a37e-99163d5f39d7</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.MongoDB" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" />
<PackageReference Include="Aspire.Hosting.Redis" />
<PackageReference Include="Aspire.Hosting.SqlServer" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.KurrentDB" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Banking.Api\Banking.Api.csproj" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions samples/banking/Banking.AppHost/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/banking/Banking.AppHost/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
16 changes: 16 additions & 0 deletions samples/banking/Banking.Domain/Accounts/AccountEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Eventuous;

namespace Banking.Domain.Accounts;

public static class AccountEvents {
public static class V1 {
[EventType("V1.Deposited")]
public record Deposited(decimal Amount);

[EventType("V1.Withdrawn")]
public record Withdrawn(decimal Amount);

[EventType("V1.Snapshot")]
public record Snapshot(decimal Balance);
}
}
26 changes: 26 additions & 0 deletions samples/banking/Banking.Domain/Accounts/AccountState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Eventuous;

namespace Banking.Domain.Accounts;

[Snapshots(typeof(AccountEvents.V1.Snapshot), StorageStrategy = SnapshotStorageStrategy.SeparateStore)]
public record AccountState : State<AccountState> {
public decimal Balance { get; init; }

public AccountState() {
On<AccountEvents.V1.Snapshot>(When);
On<AccountEvents.V1.Deposited>(When);
On<AccountEvents.V1.Withdrawn>(When);
}

private AccountState When(AccountState state, AccountEvents.V1.Snapshot e) => state with {
Balance = e.Balance
};

private AccountState When(AccountState state, AccountEvents.V1.Deposited e) => state with {
Balance = state.Balance + e.Amount
};

private AccountState When(AccountState state, AccountEvents.V1.Withdrawn e) => state with {
Balance = state.Balance - e.Amount
};
}
6 changes: 6 additions & 0 deletions samples/banking/Banking.Domain/Banking.Domain.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="$(CoreRoot)\Eventuous.Domain\Eventuous.Domain.csproj"/>
<ProjectReference Include="$(CoreRoot)\Eventuous.Shared\Eventuous.Shared.csproj"/>
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions samples/banking/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### Project description

A small example demonstrating the use of snapshots stored as an event in the aggregate stream

### How to use snapshots?

First, declare the type of event that represents a snapshot of the current state. In this example, see AccountEvents.V1.Snapshot. This name is chosen just for the example; ideally, it should correspond to something from your domain, for example, the closing of a shift

Once we have a declared snapshot type, we need to link it to the state. Note the declaration of the AccountState type - it has the Snapshots attribute applied to it, which is given a list of types that are snapshot events. This attribute helps the EventReader understand that it needs to read the stream in reverse order up to the first encountered snapshot, and not load events prior to the snapshot

You also need to define the logic according to which the snapshot event will occur. Since a snapshot is a regular event, you can create it at any moment during command processing, based on the available data to make the decision. An example can be seen in AccountService - the ApplySnapshot method.

Essentially, this is all you need to start using snapshots (it is assumed that a package with source generators has been added to the project).
3 changes: 3 additions & 0 deletions src/Core/gen/Eventuous.Shared.Generators/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace Eventuous.Shared.Generators;

internal static class Constants {
public const string BaseNamespace = "Eventuous";
public const string StateType = "State";
public const string EventTypeAttribute = "EventTypeAttribute";
public const string EventTypeAttrFqcn = $"{BaseNamespace}.{EventTypeAttribute}";
public const string SnapshotsAttribute = "SnapshotsAttribute";
public const string SnapshotsAttrFqcn = $"{BaseNamespace}.{SnapshotsAttribute}";
}
8 changes: 8 additions & 0 deletions src/Core/gen/Eventuous.Shared.Generators/Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (C) Eventuous HQ OÜ. All rights reserved
// Licensed under the Apache License, Version 2.0.

namespace Eventuous.Shared.Generators;

internal static class Helpers {
public static string MakeGlobal(string typeName) => !typeName.StartsWith("global::") ? $"global::{typeName}" : typeName;
}
Loading
Loading