diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5a50c79b4..a3cccd1db 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,7 +22,13 @@
0.77.3
+
+
+
+
+
+
diff --git a/Eventuous.slnx b/Eventuous.slnx
index 0f296de49..34f8578d8 100644
--- a/Eventuous.slnx
+++ b/Eventuous.slnx
@@ -119,6 +119,7 @@
+
@@ -150,6 +151,11 @@
+
+
+
+
+
diff --git a/samples/banking/Banking.Api/Banking.Api.csproj b/samples/banking/Banking.Api/Banking.Api.csproj
new file mode 100644
index 000000000..53090d692
--- /dev/null
+++ b/samples/banking/Banking.Api/Banking.Api.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/banking/Banking.Api/Program.cs b/samples/banking/Banking.Api/Program.cs
new file mode 100644
index 000000000..585ab50fc
--- /dev/null
+++ b/samples/banking/Banking.Api/Program.cs
@@ -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 connection 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();
+
+//----------------------------------------------------------------
+
+builder.Services.AddCommandService();
+
+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
+
+
+ SnapshotStorageStrategy.cs
+
+
diff --git a/src/Core/gen/Eventuous.Shared.Generators/Helpers.cs b/src/Core/gen/Eventuous.Shared.Generators/Helpers.cs
new file mode 100644
index 000000000..9a04cdfa4
--- /dev/null
+++ b/src/Core/gen/Eventuous.Shared.Generators/Helpers.cs
@@ -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;
+}
diff --git a/src/Core/gen/Eventuous.Shared.Generators/SnapshotMappingsGenerator.cs b/src/Core/gen/Eventuous.Shared.Generators/SnapshotMappingsGenerator.cs
new file mode 100644
index 000000000..1e680ea95
--- /dev/null
+++ b/src/Core/gen/Eventuous.Shared.Generators/SnapshotMappingsGenerator.cs
@@ -0,0 +1,266 @@
+// Copyright (C) Eventuous HQ OÜ. All rights reserved
+// Licensed under the Apache License, Version 2.0.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+using System.Text;
+using static Eventuous.Shared.Generators.Constants;
+using static Eventuous.Shared.Generators.Helpers;
+
+namespace Eventuous.Shared.Generators;
+
+[Generator(LanguageNames.CSharp)]
+public sealed class SnapshotMappingsGenerator : IIncrementalGenerator {
+ static Map? GetMapFromSnapshotsAttribute(GeneratorSyntaxContext context) {
+ var classSyntax = (ClassDeclarationSyntax)context.Node;
+ var classSymbol = context.SemanticModel.GetDeclaredSymbol(classSyntax);
+
+ if (classSymbol == null || !IsState(classSymbol)) return null;
+
+ var snapshotsAttr = classSymbol.GetAttributes()
+ .FirstOrDefault(attr => attr.AttributeClass?.Name == "SnapshotsAttribute");
+
+ if (snapshotsAttr == null) return null;
+
+ var snapshotTypes = new HashSet();
+
+ foreach (var arg in snapshotsAttr.ConstructorArguments) {
+ switch (arg.Kind) {
+ case TypedConstantKind.Array: {
+ foreach (var typeConstant in arg.Values) {
+ if (typeConstant.Value is ITypeSymbol typeSymbol) {
+ snapshotTypes.Add(MakeGlobal(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
+ }
+ }
+
+ break;
+ }
+ case TypedConstantKind.Type when arg.Value is ITypeSymbol singleType:
+ snapshotTypes.Add(MakeGlobal(singleType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); break;
+ }
+ }
+
+ var storageStrategy = GetStorageStrategyFromAttribute(snapshotsAttr);
+
+ var stateType = classSymbol.BaseType?.TypeArguments[0];
+
+ if (stateType == null) return null;
+
+ return new Map {
+ SnapshotTypes = snapshotTypes,
+ StateType = MakeGlobal(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
+ StorageStrategy = storageStrategy
+ };
+ }
+
+ static HashSet GetTypesFromSnapshotsAttribute(AttributeData attributeData) {
+ var result = new HashSet();
+
+ foreach (var arg in attributeData.ConstructorArguments) {
+ switch (arg.Kind) {
+ case TypedConstantKind.Array: {
+ foreach (var typeConstant in arg.Values) {
+ if (typeConstant.Value is ITypeSymbol typeSymbol) {
+ result.Add(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ }
+
+ break;
+ }
+ case TypedConstantKind.Type: {
+ if (arg.Value is ITypeSymbol typeSymbol) {
+ result.Add(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+
+ break;
+ }
+ }
+ }
+
+ foreach (var namedArg in attributeData.NamedArguments) {
+ switch (namedArg.Value.Kind) {
+ case TypedConstantKind.Array: {
+ foreach (var typeConstant in namedArg.Value.Values) {
+ if (typeConstant.Value is ITypeSymbol typeSymbol) {
+ result.Add(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ }
+
+ break;
+ }
+ case TypedConstantKind.Type when namedArg.Value.Value is ITypeSymbol typeSymbol:
+ result.Add(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); break;
+ }
+ }
+
+ return result;
+ }
+
+ static ImmutableArray