Skip to content

Commit 4eb66db

Browse files
Copilotaaronpowellfredimachado
authored
Add KurrentDB integration and mark EventStore as obsolete (#895)
* Initial plan * Add KurrentDB integration and mark EventStore as obsolete Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Add tests for KurrentDB integration Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Add KurrentDB examples and update test workflow Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Revert API file changes and remove manually created KurrentDB API files Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Update README to use correct KurrentDB type names and GitHub links Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Remove EventStore test projects from repository and CI workflow Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Update solution file to include KurrentDB projects and remove EventStore test projects Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Removing test from the test list * Registering the right client * Fixing a bunch of places that were still using EventStoreClient * Fix KurrentDB tracing --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> Co-authored-by: Aaron Powell <me@aaron-powell.com> Co-authored-by: Fredi Machado <fredisoft@gmail.com>
1 parent 765fb1f commit 4eb66db

File tree

41 files changed

+1239
-265
lines changed

Some content is hidden

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

41 files changed

+1239
-265
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ jobs:
2828
Hosting.Dapr.Tests,
2929
Hosting.DbGate.Tests,
3030
Hosting.Deno.Tests,
31-
Hosting.EventStore.Tests,
3231
Hosting.Flagd.Tests,
3332
Hosting.GoFeatureFlag.Tests,
3433
Hosting.Golang.Tests,
3534
Hosting.Java.Tests,
3635
Hosting.Keycloak.Extensions.Tests,
3736
Hosting.k6.Tests,
37+
Hosting.KurrentDB.Tests,
3838
Hosting.LavinMQ.Tests,
3939
Hosting.MailPit.Tests,
4040
Hosting.McpInspector.Tests,
@@ -60,8 +60,8 @@ jobs:
6060
Hosting.SurrealDb.Tests,
6161

6262
# Client integration tests
63-
EventStore.Tests,
6463
GoFeatureFlag.Tests,
64+
KurrentDB.Tests,
6565
MassTransit.RabbitMQ.Tests,
6666
Meilisearch.Tests,
6767
Microsoft.Data.Sqlite.Tests,

CommunityToolkit.Aspire.slnx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
<Project Path="examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj" />
3636
<Project Path="examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj" />
3737
</Folder>
38+
<Folder Name="/examples/kurrentdb/">
39+
<Project Path="examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService.csproj" />
40+
<Project Path="examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost.csproj" />
41+
<Project Path="examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj" />
42+
</Folder>
3843
<Folder Name="/examples/flagd/">
3944
<Project Path="examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj" />
4045
</Folder>
@@ -180,6 +185,7 @@
180185
<Project Path="src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj" />
181186
<Project Path="src/CommunityToolkit.Aspire.Hosting.Java/CommunityToolkit.Aspire.Hosting.Java.csproj" />
182187
<Project Path="src/CommunityToolkit.Aspire.Hosting.k6/CommunityToolkit.Aspire.Hosting.k6.csproj" />
188+
<Project Path="src/CommunityToolkit.Aspire.Hosting.KurrentDB/CommunityToolkit.Aspire.Hosting.KurrentDB.csproj" />
183189
<Project Path="src/CommunityToolkit.Aspire.Hosting.LavinMQ/CommunityToolkit.Aspire.Hosting.LavinMQ.csproj" />
184190
<Project Path="src/CommunityToolkit.Aspire.Hosting.MailPit/CommunityToolkit.Aspire.Hosting.MailPit.csproj" />
185191
<Project Path="src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj" />
@@ -203,6 +209,7 @@
203209
<Project Path="src/CommunityToolkit.Aspire.Hosting.Sqlite/CommunityToolkit.Aspire.Hosting.Sqlite.csproj" />
204210
<Project Path="src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj" />
205211
<Project Path="src/CommunityToolkit.Aspire.Hosting.SurrealDb/CommunityToolkit.Aspire.Hosting.SurrealDb.csproj" />
212+
<Project Path="src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj" />
206213
<Project Path="src/CommunityToolkit.Aspire.MassTransit.RabbitMQ/CommunityToolkit.Aspire.MassTransit.RabbitMQ.csproj" />
207214
<Project Path="src/CommunityToolkit.Aspire.Meilisearch/CommunityToolkit.Aspire.Meilisearch.csproj" />
208215
<Project Path="src/CommunityToolkit.Aspire.Microsoft.Data.Sqlite/CommunityToolkit.Aspire.Microsoft.Data.Sqlite.csproj" />
@@ -219,20 +226,19 @@
219226
<Project Path="src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj" />
220227
</Folder>
221228
<Folder Name="/tests/">
222-
<Project Path="tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj" />
223229
<Project Path="tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests.csproj" />
224230
<Project Path="tests/CommunityToolkit.Aspire.Hosting.ActiveMQ.Tests/CommunityToolkit.Aspire.Hosting.ActiveMQ.Tests.csproj" />
225231
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests.csproj" />
226232
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests.csproj" />
227233
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Bun.Tests/CommunityToolkit.Aspire.Hosting.Bun.Tests.csproj" />
228234
<Project Path="tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests.csproj" />
229235
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Deno.Tests/CommunityToolkit.Aspire.Hosting.Deno.Tests.csproj" />
230-
<Project Path="tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj" />
231236
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj" />
232237
<Project Path="tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj" />
233238
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/CommunityToolkit.Aspire.Hosting.Golang.Tests.csproj" />
234239
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Java.Tests/CommunityToolkit.Aspire.Hosting.Java.Tests.csproj" />
235240
<Project Path="tests/CommunityToolkit.Aspire.Hosting.k6.Tests/CommunityToolkit.Aspire.Hosting.k6.Tests.csproj" />
241+
<Project Path="tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests.csproj" />
236242
<Project Path="tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests.csproj" />
237243
<Project Path="tests/CommunityToolkit.Aspire.Hosting.MailPit.Tests/CommunityToolkit.Aspire.Hosting.MailPit.Tests.csproj" />
238244
<Project Path="tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests.csproj" />
@@ -256,6 +262,7 @@
256262
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests.csproj" />
257263
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj" />
258264
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests.csproj" />
265+
<Project Path="tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj" />
259266
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Keycloak.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Keycloak.Extensions.Tests.csproj" Type="C#" />
260267
<Project Path="tests/CommunityToolkit.Aspire.MassTransit.RabbitMQ.Tests/CommunityToolkit.Aspire.MassTransit.RabbitMQ.Tests.csproj" />
261268
<Project Path="tests/CommunityToolkit.Aspire.Meilisearch.Tests/CommunityToolkit.Aspire.Meilisearch.Tests.csproj" />

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="$(AspireVersion)" />
2020
<PackageVersion Include="Aspire.Hosting.MySql" Version="$(AspireVersion)" />
2121
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
22+
<PackageVersion Include="KurrentDB.Client" Version="1.0.0" />
2223
</ItemGroup>
2324
<ItemGroup Label="Core Packages">
2425
<!-- AspNetCore packages -->
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;
4+
5+
public class Account
6+
{
7+
public Guid Id { get; private set; }
8+
public string? Name { get; private set; }
9+
public decimal Balance { get; private set; }
10+
11+
[JsonIgnore]
12+
public int Version { get; private set; } = -1;
13+
14+
[NonSerialized]
15+
private readonly Queue<object> uncommittedEvents = new();
16+
17+
public static Account Create(Guid id, string name)
18+
=> new(id, name);
19+
20+
public void Deposit(decimal amount)
21+
{
22+
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0, nameof(amount));
23+
24+
var @event = new AccountFundsDeposited(Id, amount);
25+
26+
uncommittedEvents.Enqueue(@event);
27+
Apply(@event);
28+
}
29+
30+
public void Withdraw(decimal amount)
31+
{
32+
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0, nameof(amount));
33+
ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, Balance, nameof(amount));
34+
35+
var @event = new AccountFundsWithdrew(Id, amount);
36+
37+
uncommittedEvents.Enqueue(@event);
38+
Apply(@event);
39+
}
40+
41+
public void When(object @event)
42+
{
43+
switch (@event)
44+
{
45+
case AccountCreated accountCreated:
46+
Apply(accountCreated);
47+
break;
48+
case AccountFundsDeposited accountFundsDeposited:
49+
Apply(accountFundsDeposited);
50+
break;
51+
case AccountFundsWithdrew accountFundsWithdrew:
52+
Apply(accountFundsWithdrew);
53+
break;
54+
}
55+
}
56+
57+
public object[] DequeueUncommittedEvents()
58+
{
59+
var dequeuedEvents = uncommittedEvents.ToArray();
60+
61+
uncommittedEvents.Clear();
62+
63+
return dequeuedEvents;
64+
}
65+
66+
private Account()
67+
{
68+
}
69+
70+
private Account(Guid id, string name)
71+
{
72+
if (id == Guid.Empty)
73+
{
74+
throw new ArgumentException("Id cannot be empty.", nameof(id));
75+
}
76+
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
77+
78+
var @event = new AccountCreated(id, name);
79+
80+
uncommittedEvents.Enqueue(@event);
81+
Apply(@event);
82+
}
83+
84+
private void Apply(AccountCreated @event)
85+
{
86+
Version++;
87+
88+
Id = @event.Id;
89+
Name = @event.Name;
90+
}
91+
92+
private void Apply(AccountFundsDeposited @event)
93+
{
94+
Version++;
95+
96+
Balance += @event.Amount;
97+
}
98+
99+
private void Apply(AccountFundsWithdrew @event)
100+
{
101+
Version++;
102+
103+
Balance -= @event.Amount;
104+
}
105+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;
2+
3+
public record AccountCreated(Guid Id, string Name);
4+
5+
public record AccountFundsDeposited(Guid Id, decimal Amount);
6+
7+
public record AccountFundsWithdrew(Guid Id, decimal Amount);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
10+
<PackageReference Include="Swashbuckle.AspNetCore" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.KurrentDB\CommunityToolkit.Aspire.KurrentDB.csproj" />
15+
<ProjectReference Include="..\CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults\CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Text.Json;
2+
using System.Text;
3+
using KurrentDB.Client;
4+
5+
namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;
6+
7+
public static class KurrentDBExtensions
8+
{
9+
public static async Task<Account?> GetAccount(this KurrentDBClient eventStore, Guid id, CancellationToken cancellationToken)
10+
{
11+
var readResult = eventStore.ReadStreamAsync(
12+
Direction.Forwards,
13+
$"account-{id:N}",
14+
StreamPosition.Start,
15+
cancellationToken: cancellationToken
16+
);
17+
18+
var readState = await readResult.ReadState;
19+
if (readState == ReadState.StreamNotFound)
20+
{
21+
return null;
22+
}
23+
24+
var account = (Account)Activator.CreateInstance(typeof(Account), true)!;
25+
26+
await foreach (var resolvedEvent in readResult)
27+
{
28+
var @event = resolvedEvent.Deserialize();
29+
30+
account.When(@event!);
31+
}
32+
33+
return account;
34+
}
35+
36+
public static async Task AppendAccountEvents(this KurrentDBClient eventStore, Account account, CancellationToken cancellationToken)
37+
{
38+
var events = account.DequeueUncommittedEvents();
39+
40+
var eventsToAppend = events
41+
.Select(@event => @event.Serialize()).ToArray();
42+
43+
var expectedVersion = account.Version - events.Length;
44+
await eventStore.AppendToStreamAsync(
45+
$"account-{account.Id:N}",
46+
expectedVersion == 0 ? StreamState.NoStream : StreamState.StreamRevision((ulong)expectedVersion),
47+
eventsToAppend,
48+
cancellationToken: cancellationToken
49+
);
50+
}
51+
52+
private static object? Deserialize(this ResolvedEvent resolvedEvent)
53+
{
54+
var eventClrTypeName = JsonDocument.Parse(resolvedEvent.Event.Metadata)
55+
.RootElement
56+
.GetProperty("EventClrTypeName")
57+
.GetString();
58+
59+
return JsonSerializer.Deserialize(
60+
Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span),
61+
Type.GetType(eventClrTypeName!)!);
62+
}
63+
64+
private static EventData Serialize(this object @event)
65+
{
66+
return new EventData(
67+
Uuid.NewUuid(),
68+
@event.GetType().Name,
69+
data: Encoding.UTF8.GetBytes(JsonSerializer.Serialize(@event)),
70+
metadata: Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new Dictionary<string, string>
71+
{
72+
{ "EventClrTypeName", @event.GetType().AssemblyQualifiedName! }
73+
}))
74+
);
75+
}
76+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;
2+
using KurrentDB.Client;
3+
4+
var builder = WebApplication.CreateBuilder(args);
5+
6+
builder.AddServiceDefaults();
7+
8+
builder.AddKurrentDBClient("kurrentdb");
9+
10+
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
11+
builder.Services.AddEndpointsApiExplorer();
12+
builder.Services.AddSwaggerGen();
13+
14+
var app = builder.Build();
15+
16+
app.UseHttpsRedirection();
17+
18+
app.MapDefaultEndpoints();
19+
20+
if (app.Environment.IsDevelopment())
21+
{
22+
app.UseSwagger();
23+
app.UseSwaggerUI();
24+
}
25+
26+
app.MapPost("/account/create", async (KurrentDBClient eventStore, CancellationToken cancellationToken) =>
27+
{
28+
var account = Account.Create(Guid.NewGuid(), "John Doe");
29+
30+
account.Deposit(100);
31+
32+
await eventStore.AppendAccountEvents(account, cancellationToken);
33+
34+
return Results.Created($"/account/{account.Id}", account);
35+
});
36+
37+
app.MapGet("/account/{id:guid}", async (Guid id, KurrentDBClient eventStore, CancellationToken cancellationToken) =>
38+
{
39+
var account = await eventStore.GetAccount(id, cancellationToken);
40+
if (account is null)
41+
{
42+
return Results.NotFound();
43+
}
44+
45+
return TypedResults.Ok(account);
46+
});
47+
48+
app.MapPost("/account/{id:guid}/deposit", async (Guid id, DepositRequest request, KurrentDBClient eventStore, CancellationToken cancellationToken) =>
49+
{
50+
var account = await eventStore.GetAccount(id, cancellationToken);
51+
if (account is null)
52+
{
53+
return Results.NotFound();
54+
}
55+
56+
account.Deposit(request.Amount);
57+
58+
await eventStore.AppendAccountEvents(account, cancellationToken);
59+
60+
return Results.Ok();
61+
});
62+
63+
app.MapPost("/account/{id:guid}/withdraw", async (Guid id, WithdrawRequest request, KurrentDBClient eventStore, CancellationToken cancellationToken) =>
64+
{
65+
var account = await eventStore.GetAccount(id, cancellationToken);
66+
if (account is null)
67+
{
68+
return Results.NotFound();
69+
}
70+
71+
account.Withdraw(request.Amount);
72+
73+
await eventStore.AppendAccountEvents(account, cancellationToken);
74+
75+
return Results.Ok();
76+
});
77+
78+
app.Run();
79+
80+
public record DepositRequest(decimal Amount);
81+
public record WithdrawRequest(decimal Amount);

0 commit comments

Comments
 (0)