Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ jobs:
Hosting.Dapr.Tests,
Hosting.DbGate.Tests,
Hosting.Deno.Tests,
Hosting.EventStore.Tests,
Hosting.Flagd.Tests,
Hosting.GoFeatureFlag.Tests,
Hosting.Golang.Tests,
Hosting.Java.Tests,
Hosting.k6.Tests,
Hosting.KurrentDB.Tests,
Hosting.LavinMQ.Tests,
Hosting.MailPit.Tests,
Hosting.McpInspector.Tests,
Expand All @@ -59,8 +59,8 @@ jobs:
Hosting.SurrealDb.Tests,

# Client integration tests
EventStore.Tests,
GoFeatureFlag.Tests,
KurrentDB.Tests,
MassTransit.RabbitMQ.Tests,
Meilisearch.Tests,
Microsoft.Data.Sqlite.Tests,
Expand Down
11 changes: 9 additions & 2 deletions CommunityToolkit.Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
<Project Path="examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj" />
<Project Path="examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj" />
</Folder>
<Folder Name="/examples/kurrentdb/">
<Project Path="examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService.csproj" />
<Project Path="examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost.csproj" />
<Project Path="examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj" />
</Folder>
<Folder Name="/examples/flagd/">
<Project Path="examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj" />
</Folder>
Expand Down Expand Up @@ -174,6 +179,7 @@
<Project Path="src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Java/CommunityToolkit.Aspire.Hosting.Java.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.k6/CommunityToolkit.Aspire.Hosting.k6.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.KurrentDB/CommunityToolkit.Aspire.Hosting.KurrentDB.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.LavinMQ/CommunityToolkit.Aspire.Hosting.LavinMQ.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.MailPit/CommunityToolkit.Aspire.Hosting.MailPit.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.McpInspector/CommunityToolkit.Aspire.Hosting.McpInspector.csproj" />
Expand All @@ -197,6 +203,7 @@
<Project Path="src/CommunityToolkit.Aspire.Hosting.Sqlite/CommunityToolkit.Aspire.Hosting.Sqlite.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.SurrealDb/CommunityToolkit.Aspire.Hosting.SurrealDb.csproj" />
<Project Path="src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj" />
<Project Path="src/CommunityToolkit.Aspire.MassTransit.RabbitMQ/CommunityToolkit.Aspire.MassTransit.RabbitMQ.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Meilisearch/CommunityToolkit.Aspire.Meilisearch.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Microsoft.Data.Sqlite/CommunityToolkit.Aspire.Microsoft.Data.Sqlite.csproj" />
Expand All @@ -212,20 +219,19 @@
<Project Path="src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.ActiveMQ.Tests/CommunityToolkit.Aspire.Hosting.ActiveMQ.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests/CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Bun.Tests/CommunityToolkit.Aspire.Hosting.Bun.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Deno.Tests/CommunityToolkit.Aspire.Hosting.Deno.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/CommunityToolkit.Aspire.Hosting.Golang.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Java.Tests/CommunityToolkit.Aspire.Hosting.Java.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.k6.Tests/CommunityToolkit.Aspire.Hosting.k6.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.MailPit.Tests/CommunityToolkit.Aspire.Hosting.MailPit.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests/CommunityToolkit.Aspire.Hosting.McpInspector.Tests.csproj" />
Expand All @@ -249,6 +255,7 @@
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.MassTransit.RabbitMQ.Tests/CommunityToolkit.Aspire.MassTransit.RabbitMQ.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Meilisearch.Tests/CommunityToolkit.Aspire.Meilisearch.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Microsoft.Data.Sqlite.Tests/CommunityToolkit.Aspire.Microsoft.Data.Sqlite.Tests.csproj" />
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.MySql" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
<PackageVersion Include="KurrentDB.Client" Version="1.0.0" />
</ItemGroup>
<ItemGroup Label="Core Packages">
<!-- AspNetCore packages -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Text.Json.Serialization;

namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;

public class Account
{
public Guid Id { get; private set; }
public string? Name { get; private set; }
public decimal Balance { get; private set; }

[JsonIgnore]
public int Version { get; private set; } = -1;

[NonSerialized]
private readonly Queue<object> uncommittedEvents = new();

public static Account Create(Guid id, string name)
=> new(id, name);

public void Deposit(decimal amount)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0, nameof(amount));

var @event = new AccountFundsDeposited(Id, amount);

uncommittedEvents.Enqueue(@event);
Apply(@event);
}

public void Withdraw(decimal amount)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0, nameof(amount));
ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, Balance, nameof(amount));

var @event = new AccountFundsWithdrew(Id, amount);

uncommittedEvents.Enqueue(@event);
Apply(@event);
}

public void When(object @event)
{
switch (@event)
{
case AccountCreated accountCreated:
Apply(accountCreated);
break;
case AccountFundsDeposited accountFundsDeposited:
Apply(accountFundsDeposited);
break;
case AccountFundsWithdrew accountFundsWithdrew:
Apply(accountFundsWithdrew);
break;
}
}

public object[] DequeueUncommittedEvents()
{
var dequeuedEvents = uncommittedEvents.ToArray();

uncommittedEvents.Clear();

return dequeuedEvents;
}

private Account()
{
}

private Account(Guid id, string name)
{
if (id == Guid.Empty)
{
throw new ArgumentException("Id cannot be empty.", nameof(id));
}
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));

var @event = new AccountCreated(id, name);

uncommittedEvents.Enqueue(@event);
Apply(@event);
}

private void Apply(AccountCreated @event)
{
Version++;

Id = @event.Id;
Name = @event.Name;
}

private void Apply(AccountFundsDeposited @event)
{
Version++;

Balance += @event.Amount;
}

private void Apply(AccountFundsWithdrew @event)
{
Version++;

Balance -= @event.Amount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;

public record AccountCreated(Guid Id, string Name);

public record AccountFundsDeposited(Guid Id, decimal Amount);

public record AccountFundsWithdrew(Guid Id, decimal Amount);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.KurrentDB\CommunityToolkit.Aspire.KurrentDB.csproj" />
<ProjectReference Include="..\CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults\CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Text.Json;
using System.Text;
using KurrentDB.Client;

namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;

public static class KurrentDBExtensions
{
public static async Task<Account?> GetAccount(this KurrentDBClient eventStore, Guid id, CancellationToken cancellationToken)
{
var readResult = eventStore.ReadStreamAsync(
Direction.Forwards,
$"account-{id:N}",
StreamPosition.Start,
cancellationToken: cancellationToken
);

var readState = await readResult.ReadState;
if (readState == ReadState.StreamNotFound)
{
return null;
}

var account = (Account)Activator.CreateInstance(typeof(Account), true)!;

await foreach (var resolvedEvent in readResult)
{
var @event = resolvedEvent.Deserialize();

account.When(@event!);
}

return account;
}

public static async Task AppendAccountEvents(this KurrentDBClient eventStore, Account account, CancellationToken cancellationToken)
{
var events = account.DequeueUncommittedEvents();

var eventsToAppend = events
.Select(@event => @event.Serialize()).ToArray();

var expectedVersion = account.Version - events.Length;
await eventStore.AppendToStreamAsync(
$"account-{account.Id:N}",
expectedVersion == 0 ? StreamState.NoStream : StreamState.StreamRevision((ulong)expectedVersion),
eventsToAppend,
cancellationToken: cancellationToken
);
}

private static object? Deserialize(this ResolvedEvent resolvedEvent)
{
var eventClrTypeName = JsonDocument.Parse(resolvedEvent.Event.Metadata)
.RootElement
.GetProperty("EventClrTypeName")
.GetString();

return JsonSerializer.Deserialize(
Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span),
Type.GetType(eventClrTypeName!)!);
}

private static EventData Serialize(this object @event)
{
return new EventData(
Uuid.NewUuid(),
@event.GetType().Name,
data: Encoding.UTF8.GetBytes(JsonSerializer.Serialize(@event)),
metadata: Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new Dictionary<string, string>
{
{ "EventClrTypeName", @event.GetType().AssemblyQualifiedName! }
}))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService;
using KurrentDB.Client;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.AddKurrentDBClient("kurrentdb");

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseHttpsRedirection();

app.MapDefaultEndpoints();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.MapPost("/account/create", async (KurrentDBClient eventStore, CancellationToken cancellationToken) =>
{
var account = Account.Create(Guid.NewGuid(), "John Doe");

account.Deposit(100);

await eventStore.AppendAccountEvents(account, cancellationToken);

return Results.Created($"/account/{account.Id}", account);
});

app.MapGet("/account/{id:guid}", async (Guid id, KurrentDBClient eventStore, CancellationToken cancellationToken) =>
{
var account = await eventStore.GetAccount(id, cancellationToken);
if (account is null)
{
return Results.NotFound();
}

return TypedResults.Ok(account);
});

app.MapPost("/account/{id:guid}/deposit", async (Guid id, DepositRequest request, KurrentDBClient eventStore, CancellationToken cancellationToken) =>
{
var account = await eventStore.GetAccount(id, cancellationToken);
if (account is null)
{
return Results.NotFound();
}

account.Deposit(request.Amount);

await eventStore.AppendAccountEvents(account, cancellationToken);

return Results.Ok();
});

app.MapPost("/account/{id:guid}/withdraw", async (Guid id, WithdrawRequest request, KurrentDBClient eventStore, CancellationToken cancellationToken) =>
{
var account = await eventStore.GetAccount(id, cancellationToken);
if (account is null)
{
return Results.NotFound();
}

account.Withdraw(request.Amount);

await eventStore.AppendAccountEvents(account, cancellationToken);

return Results.Ok();
});

app.Run();

public record DepositRequest(decimal Amount);
public record WithdrawRequest(decimal Amount);
Loading
Loading