From 9ffb47edb004c277d39df7a2c171abc6e61f555a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 13 Oct 2025 01:10:44 +0000
Subject: [PATCH 01/12] Initial plan
From 317000b9110533765e3b7453fcd98d50b398384f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 13 Oct 2025 01:18:58 +0000
Subject: [PATCH 02/12] Add KurrentDB integration and mark EventStore as
obsolete
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
.../AspireEventStoreExtensions.cs | 1 +
.../EventStoreSettings.cs | 1 +
.../api/CommunityToolkit.Aspire.EventStore.cs | 2 +
.../EventStoreBuilderExtensions.cs | 1 +
.../EventStoreResource.cs | 1 +
...munityToolkit.Aspire.Hosting.EventStore.cs | 2 +
...ityToolkit.Aspire.Hosting.KurrentDB.csproj | 17 +++
.../KurrentDBBuilderExtensions.cs | 144 ++++++++++++++++++
.../KurrentDBContainerImageTags.cs | 11 ++
.../KurrentDBResource.cs | 28 ++++
.../README.md | 37 +++++
...mmunityToolkit.Aspire.Hosting.KurrentDB.cs | 31 ++++
.../AspireKurrentDBExtensions.cs | 116 ++++++++++++++
.../CommunityToolkit.Aspire.KurrentDB.csproj | 21 +++
.../KurrentDBSettings.cs | 39 +++++
.../README.md | 115 ++++++++++++++
.../api/CommunityToolkit.Aspire.KurrentDB.cs | 31 ++++
17 files changed, 598 insertions(+)
create mode 100644 src/CommunityToolkit.Aspire.Hosting.KurrentDB/CommunityToolkit.Aspire.Hosting.KurrentDB.csproj
create mode 100644 src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs
create mode 100644 src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs
create mode 100644 src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBResource.cs
create mode 100644 src/CommunityToolkit.Aspire.Hosting.KurrentDB/README.md
create mode 100644 src/CommunityToolkit.Aspire.Hosting.KurrentDB/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs
create mode 100644 src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs
create mode 100644 src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj
create mode 100644 src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs
create mode 100644 src/CommunityToolkit.Aspire.KurrentDB/README.md
create mode 100644 src/CommunityToolkit.Aspire.KurrentDB/api/CommunityToolkit.Aspire.KurrentDB.cs
diff --git a/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs
index 29bcef4e0..3e5536949 100644
--- a/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs
+++ b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs
@@ -15,6 +15,7 @@ namespace Microsoft.Extensions.Hosting;
///
/// Provides extension methods for registering EventStore-related services in an .
///
+[Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.KurrentDB and AspireKurrentDBExtensions instead. This integration will be removed in a future release.")]
public static class AspireEventStoreExtensions
{
private const string DefaultConfigSectionName = "Aspire:EventStore:Client";
diff --git a/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs b/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs
index 3c341e7e2..2497d6d9d 100644
--- a/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs
+++ b/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs
@@ -6,6 +6,7 @@ namespace CommunityToolkit.Aspire.EventStore;
///
/// Provides the client configuration settings for connecting to an EventStore server using EventStoreClient.
///
+[Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.KurrentDB and KurrentDBSettings instead. This integration will be removed in a future release.")]
public sealed class EventStoreSettings
{
///
diff --git a/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs b/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs
index 877984e0b..f0c7b4a53 100644
--- a/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs
+++ b/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs
@@ -8,6 +8,7 @@
//------------------------------------------------------------------------------
namespace CommunityToolkit.Aspire.EventStore
{
+ [System.Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.KurrentDB and KurrentDBSettings instead. This integration will be removed in a future release.")]
public sealed partial class EventStoreSettings
{
public string? ConnectionString { get { throw null; } set { } }
@@ -22,6 +23,7 @@ public sealed partial class EventStoreSettings
namespace Microsoft.Extensions.Hosting
{
+ [System.Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.KurrentDB and AspireKurrentDBExtensions instead. This integration will be removed in a future release.")]
public static partial class AspireEventStoreExtensions
{
public static void AddEventStoreClient(this IHostApplicationBuilder builder, string connectionName, System.Action? configureSettings = null) { }
diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs
index a19bafd86..92daf80f2 100644
--- a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs
@@ -13,6 +13,7 @@ namespace Aspire.Hosting;
///
/// Provides extension methods for adding EventStore resources to the application model.
///
+[Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.Hosting.KurrentDB and KurrentDBBuilderExtensions instead. This integration will be removed in a future release.")]
public static class EventStoreBuilderExtensions
{
private const string DataTargetFolder = "/var/lib/eventstore";
diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs
index a0969d78c..cb7d0a8c0 100644
--- a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs
@@ -7,6 +7,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// A resource that represents an EventStore container.
///
/// The name of the resource.
+[Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.Hosting.KurrentDB and KurrentDBResource instead. This integration will be removed in a future release.")]
public class EventStoreResource(string name) : ContainerResource(name), IResourceWithConnectionString
{
internal const string HttpEndpointName = "http";
diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/api/CommunityToolkit.Aspire.Hosting.EventStore.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/api/CommunityToolkit.Aspire.Hosting.EventStore.cs
index d3ff14047..78d52fbbe 100644
--- a/src/CommunityToolkit.Aspire.Hosting.EventStore/api/CommunityToolkit.Aspire.Hosting.EventStore.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/api/CommunityToolkit.Aspire.Hosting.EventStore.cs
@@ -8,6 +8,7 @@
//------------------------------------------------------------------------------
namespace Aspire.Hosting
{
+ [System.Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.Hosting.KurrentDB and KurrentDBBuilderExtensions instead. This integration will be removed in a future release.")]
public static partial class EventStoreBuilderExtensions
{
public static ApplicationModel.IResourceBuilder AddEventStore(this IDistributedApplicationBuilder builder, string name, int? port = null) { throw null; }
@@ -20,6 +21,7 @@ public static partial class EventStoreBuilderExtensions
namespace Aspire.Hosting.ApplicationModel
{
+ [System.Obsolete("EventStore has been rebranded to KurrentDB. Use CommunityToolkit.Aspire.Hosting.KurrentDB and KurrentDBResource instead. This integration will be removed in a future release.")]
public partial class EventStoreResource : ContainerResource, IResourceWithConnectionString, IResource, IManifestExpressionProvider, IValueProvider, IValueWithReferences
{
public EventStoreResource(string name) : base(default!, default) { }
diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/CommunityToolkit.Aspire.Hosting.KurrentDB.csproj b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/CommunityToolkit.Aspire.Hosting.KurrentDB.csproj
new file mode 100644
index 000000000..98b06e700
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/CommunityToolkit.Aspire.Hosting.KurrentDB.csproj
@@ -0,0 +1,17 @@
+
+
+
+ hosting kurrentdb
+ KurrentDB support for .NET Aspire.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs
new file mode 100644
index 000000000..3e6903bf6
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBBuilderExtensions.cs
@@ -0,0 +1,144 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Utils;
+using CommunityToolkit.Aspire.Hosting.KurrentDB;
+using HealthChecks.EventStore.gRPC;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding KurrentDB resources to the application model.
+///
+public static class KurrentDBBuilderExtensions
+{
+ private const string DataTargetFolder = "/var/lib/kurrentdb";
+
+ ///
+ /// Adds a KurrentDB resource to the application model. A container is used for local development.
+ /// The default image is and the tag is .
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The port on which the KurrentDB endpoint will be exposed.
+ /// A reference to the .
+ ///
+ ///
+ /// Add a KurrentDB container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var kurrentdb = builder.AddKurrentDB("kurrentdb");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(kurrentdb);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder AddKurrentDB(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(name);
+
+ var kurrentDBResource = new KurrentDBResource(name);
+
+ string? connectionString = null;
+
+ builder.Eventing.Subscribe(kurrentDBResource, async (@event, cancellationToken) =>
+ {
+ connectionString = await kurrentDBResource.ConnectionStringExpression
+ .GetValueAsync(cancellationToken)
+ .ConfigureAwait(false)
+ ?? throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{kurrentDBResource.Name}' resource but the connection string was null.");
+ });
+
+ var healthCheckKey = $"{name}_check";
+ builder.Services.AddHealthChecks()
+ .Add(new HealthCheckRegistration(
+ healthCheckKey,
+ sp => new EventStoreHealthCheck(connectionString!),
+ failureStatus: default,
+ tags: default,
+ timeout: default));
+
+ return builder
+ .AddResource(kurrentDBResource)
+ .WithHttpEndpoint(port: port, targetPort: KurrentDBResource.DefaultHttpPort, name: KurrentDBResource.HttpEndpointName)
+ .WithImage(KurrentDBContainerImageTags.Image, KurrentDBContainerImageTags.Tag)
+ .WithImageRegistry(KurrentDBContainerImageTags.Registry)
+ .WithEnvironment(ConfigureKurrentDBContainer)
+ .WithHealthCheck(healthCheckKey);
+ }
+
+ ///
+ /// Adds a named volume for the data folder to a KurrentDB container resource.
+ ///
+ /// The resource builder.
+ /// The name of the volume. Defaults to an auto-generated name based on the application and resource names.
+ /// The .
+ ///
+ ///
+ /// Add a KurrentDB container to the application model and reference it in a .NET project. Additionally, in this
+ /// example a data volume is added to the container to allow data to be persisted across container restarts.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var kurrentdb = builder.AddKurrentDB("kurrentdb")
+ /// .WithDataVolume();
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(kurrentdb);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), DataTargetFolder);
+ }
+
+ ///
+ /// Adds a bind mount for the data folder to a KurrentDB container resource.
+ ///
+ /// The resource builder.
+ /// The source directory on the host to mount into the container.
+ /// The .
+ ///
+ ///
+ /// Add a KurrentDB container to the application model and reference it in a .NET project. Additionally, in this
+ /// example a bind mount is added to the container to allow data to be persisted across container restarts.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var kurrentdb = builder.AddKurrentDB("kurrentdb")
+ /// .WithDataBindMount("./data/kurrentdb/data");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(kurrentdb);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(source);
+
+ return builder.WithBindMount(source, DataTargetFolder);
+ }
+
+ private static void ConfigureKurrentDBContainer(EnvironmentCallbackContext context)
+ {
+ context.EnvironmentVariables.Add("EVENTSTORE_CLUSTER_SIZE", "1");
+ context.EnvironmentVariables.Add("EVENTSTORE_RUN_PROJECTIONS", "All");
+ context.EnvironmentVariables.Add("EVENTSTORE_START_STANDARD_PROJECTIONS", "true");
+ context.EnvironmentVariables.Add("EVENTSTORE_NODE_PORT", $"{KurrentDBResource.DefaultHttpPort}");
+ context.EnvironmentVariables.Add("EVENTSTORE_INSECURE", "true");
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs
new file mode 100644
index 000000000..d0088533b
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace CommunityToolkit.Aspire.Hosting.KurrentDB;
+
+internal static class KurrentDBContainerImageTags
+{
+ public const string Registry = "docker.io";
+ public const string Image = "kurrentdb/kurrentdb";
+ public const string Tag = "24.10";
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBResource.cs b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBResource.cs
new file mode 100644
index 000000000..94977c7aa
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBResource.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a KurrentDB container.
+///
+/// The name of the resource.
+public class KurrentDBResource(string name) : ContainerResource(name), IResourceWithConnectionString
+{
+ internal const string HttpEndpointName = "http";
+ internal const int DefaultHttpPort = 2113;
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary endpoint for the KurrentDB server.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, HttpEndpointName);
+
+ ///
+ /// Gets the connection string for the KurrentDB server.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create(
+ $"esdb://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}?tls=false");
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/README.md b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/README.md
new file mode 100644
index 000000000..0f3fb578b
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/README.md
@@ -0,0 +1,37 @@
+# CommunityToolkit.Aspire.Hosting.KurrentDB library
+
+Provides extension methods and resource definitions for the .NET Aspire app host to support running [KurrentDB](https://www.kurrent.io) containers.
+
+## Getting Started
+
+### Install the package
+
+In your app host project, install the package using the following command:
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.KurrentDB
+```
+
+### Example usage
+
+Then, in the _Program.cs_ file of app host, add a KurrentDB resource and consume the connection using the following methods:
+
+```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+var kurrentdb = builder.AddKurrentDB("kurrentdb");
+
+var myService = builder.AddProject()
+ .WithReference(kurrentdb);
+
+builder.Build().Run();
+```
+
+## Additional Information
+
+https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-kurrentdb
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs
new file mode 100644
index 000000000..05b3bd0a1
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs
@@ -0,0 +1,31 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+namespace Aspire.Hosting
+{
+ public static partial class KurrentDBBuilderExtensions
+ {
+ public static ApplicationModel.IResourceBuilder AddKurrentDB(this IDistributedApplicationBuilder builder, string name, int? port = null) { throw null; }
+
+ public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; }
+
+ public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; }
+ }
+}
+
+namespace Aspire.Hosting.ApplicationModel
+{
+ public partial class KurrentDBResource : ContainerResource, IResourceWithConnectionString, IResource, IManifestExpressionProvider, IValueProvider, IValueWithReferences
+ {
+ public KurrentDBResource(string name) : base(default!, default) { }
+
+ public ReferenceExpression ConnectionStringExpression { get { throw null; } }
+
+ public EndpointReference PrimaryEndpoint { get { throw null; } }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs
new file mode 100644
index 000000000..c1e109fbe
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs
@@ -0,0 +1,116 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using CommunityToolkit.Aspire.KurrentDB;
+using EventStore.Client;
+using EventStore.Client.Extensions.OpenTelemetry;
+using HealthChecks.EventStore.gRPC;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Provides extension methods for registering KurrentDB-related services in an .
+///
+public static class AspireKurrentDBExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:KurrentDB:Client";
+
+ ///
+ /// Registers as a singleton in the services provided by the .
+ ///
+ /// The to read config from and add services to.
+ /// The connection name to use to find a connection string.
+ /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
+ public static void AddKurrentDBClient(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNullOrEmpty(connectionName);
+ AddKurrentDBClient(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null);
+ }
+
+ ///
+ /// Registers as a keyed singleton for the given in the services provided by the .
+ ///
+ /// The to read config from and add services to.
+ /// The connection name to use to find a connection string.
+ /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
+ public static void AddKeyedKurrentDBClient(
+ this IHostApplicationBuilder builder,
+ string name,
+ Action? configureSettings = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNullOrEmpty(name);
+ AddKurrentDBClient(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name);
+ }
+
+ private static void AddKurrentDBClient(
+ this IHostApplicationBuilder builder,
+ string configurationSectionName,
+ Action? configureSettings,
+ string connectionName,
+ string? serviceKey)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ var settings = new KurrentDBSettings();
+ builder.Configuration.GetSection(configurationSectionName).Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.ConnectionString = connectionString;
+ }
+
+ configureSettings?.Invoke(settings);
+
+ if (serviceKey is null)
+ {
+ builder.Services.AddSingleton(ConfigureKurrentDBClient);
+ }
+ else
+ {
+ builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureKurrentDBClient(sp));
+ }
+
+ if (!settings.DisableTracing)
+ {
+ builder.Services.AddOpenTelemetry()
+ .WithTracing(traceBuilder => traceBuilder.AddEventStoreClientInstrumentation());
+ }
+
+ if (!settings.DisableHealthChecks)
+ {
+ var healthCheckName = serviceKey is null ? "KurrentDB.Client" : $"KurrentDB.Client_{connectionName}";
+
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ healthCheckName,
+ sp => new EventStoreHealthCheck(settings.ConnectionString!),
+ failureStatus: default,
+ tags: default,
+ timeout: settings.HealthCheckTimeout));
+ }
+
+ EventStoreClient ConfigureKurrentDBClient(IServiceProvider serviceProvider)
+ {
+ if (settings.ConnectionString is not null)
+ {
+ var eventStoreClientSettings = EventStoreClientSettings.Create(settings.ConnectionString!);
+ return new EventStoreClient(eventStoreClientSettings);
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"A KurrentDB client could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " +
+ $"{nameof(settings.ConnectionString)} must be provided " +
+ $"in the '{configurationSectionName}' configuration section.");
+ }
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj b/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj
new file mode 100644
index 000000000..8555cbe4b
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj
@@ -0,0 +1,21 @@
+
+
+
+ KurrentDB client
+ A KurrentDB client that integrates with Aspire, including health checks, logging, and telemetry.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs b/src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs
new file mode 100644
index 000000000..950e2167f
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace CommunityToolkit.Aspire.KurrentDB;
+
+///
+/// Provides the client configuration settings for connecting to a KurrentDB server using EventStoreClient.
+///
+public sealed class KurrentDBSettings
+{
+ ///
+ /// Gets or sets the connection string.
+ ///
+ public string? ConnectionString { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the KurrentDB health check is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableHealthChecks { get; set; }
+
+ ///
+ /// Gets or sets the timeout duration for the health check.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public TimeSpan? HealthCheckTimeout { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableTracing { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.KurrentDB/README.md b/src/CommunityToolkit.Aspire.KurrentDB/README.md
new file mode 100644
index 000000000..4202fab23
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.KurrentDB/README.md
@@ -0,0 +1,115 @@
+# CommunityToolkit.Aspire.KurrentDB
+
+Registers an [EventStoreClient](https://github.com/EventStore/EventStore-Client-Dotnet) in the DI container for connecting to KurrentDB.
+
+## Getting started
+
+### Prerequisites
+
+- KurrentDB cluster.
+
+### Install the package
+
+Install the .NET Aspire KurrentDB Client library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.KurrentDB
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddKurrentDBClient` extension method to register an `EventStoreClient` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddKurrentDBClient("kurrentdb");
+```
+
+## Configuration
+
+The .NET Aspire KurrentDB Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project.
+
+### Use a connection string
+
+When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddKurrentDBClient()`:
+
+```csharp
+builder.AddKurrentDBClient("kurrentdb");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "kurrentdb": "esdb://localhost:22113?tls=false"
+ }
+}
+```
+
+### Use configuration providers
+
+The .NET Aspire KurrentDB Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `KurrentDBSettings` from configuration by using the `Aspire:KurrentDB:Client` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "KurrentDB": {
+ "Client": {
+ "ConnectionString": "esdb://localhost:22113?tls=false",
+ "DisableHealthChecks": true
+ }
+ }
+ }
+}
+```
+
+### Use inline delegates
+
+Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code:
+
+```csharp
+builder.AddKurrentDBClient("kurrentdb", settings => settings.DisableHealthChecks = true);
+```
+
+## AppHost extensions
+
+In your AppHost project, install the `CommunityToolkit.Aspire.Hosting.KurrentDB` library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.KurrentDB
+```
+
+Then, in the _Program.cs_ file of `AppHost`, register KurrentDB and consume the connection using the following methods:
+
+```csharp
+var kurrentdb = builder.AddKurrentDB("kurrentdb");
+
+var myService = builder.AddProject()
+ .WithReference(kurrentdb);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `kurrentdb`. In the _Program.cs_ file of `MyService`, the KurrentDB connection can be consumed using:
+
+```csharp
+builder.AddKurrentDBClient("kurrentdb");
+```
+
+Then, in your service, inject `EventStoreClient` and use it to interact with the KurrentDB API:
+
+```csharp
+public class MyService(EventStoreClient eventStoreClient)
+{
+ // ...
+}
+```
+
+## Additional documentation
+
+- https://github.com/EventStore/EventStore-Client-Dotnet
+- https://www.kurrent.io
+- https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-kurrentdb
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
+
diff --git a/src/CommunityToolkit.Aspire.KurrentDB/api/CommunityToolkit.Aspire.KurrentDB.cs b/src/CommunityToolkit.Aspire.KurrentDB/api/CommunityToolkit.Aspire.KurrentDB.cs
new file mode 100644
index 000000000..5fae44601
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.KurrentDB/api/CommunityToolkit.Aspire.KurrentDB.cs
@@ -0,0 +1,31 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+namespace CommunityToolkit.Aspire.KurrentDB
+{
+ public sealed partial class KurrentDBSettings
+ {
+ public string? ConnectionString { get { throw null; } set { } }
+
+ public bool DisableHealthChecks { get { throw null; } set { } }
+
+ public bool DisableTracing { get { throw null; } set { } }
+
+ public System.TimeSpan? HealthCheckTimeout { get { throw null; } set { } }
+ }
+}
+
+namespace Microsoft.Extensions.Hosting
+{
+ public static partial class AspireKurrentDBExtensions
+ {
+ public static void AddKurrentDBClient(this IHostApplicationBuilder builder, string connectionName, System.Action? configureSettings = null) { }
+
+ public static void AddKeyedKurrentDBClient(this IHostApplicationBuilder builder, string name, System.Action? configureSettings = null) { }
+ }
+}
From 4ce644a00d801ca677ee9b0e462346f81b5a7719 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 13 Oct 2025 01:22:48 +0000
Subject: [PATCH 03/12] Add tests for KurrentDB integration
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
.../AddKurrentDBTests.cs | 90 ++++++
...lkit.Aspire.Hosting.KurrentDB.Tests.csproj | 9 +
.../KurrentDBFunctionalTests.cs | 261 ++++++++++++++++++
.../KurrentDBPublicApiTests.cs | 140 ++++++++++
.../AspireKurrentDBClientExtensionsTest.cs | 89 ++++++
...unityToolkit.Aspire.KurrentDB.Tests.csproj | 12 +
.../ConfigurationTests.cs | 23 ++
.../ConformanceTests.cs | 92 ++++++
.../KurrentDBClientPublicApiTests.cs | 88 ++++++
.../KurrentDBContainerFixture.cs | 49 ++++
10 files changed, 853 insertions(+)
create mode 100644 tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/AddKurrentDBTests.cs
create mode 100644 tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests.csproj
create mode 100644 tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs
create mode 100644 tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBPublicApiTests.cs
create mode 100644 tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs
create mode 100644 tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj
create mode 100644 tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConfigurationTests.cs
create mode 100644 tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs
create mode 100644 tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBClientPublicApiTests.cs
create mode 100644 tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs
diff --git a/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/AddKurrentDBTests.cs b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/AddKurrentDBTests.cs
new file mode 100644
index 000000000..467698783
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/AddKurrentDBTests.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting;
+using System.Net.Sockets;
+
+namespace CommunityToolkit.Aspire.Hosting.KurrentDB.Tests;
+
+public class AddKurrentDBTests
+{
+ [Fact]
+ public async Task AddKurrentDBContainerWithDefaultsAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ var kurrentdb = appBuilder.AddKurrentDB("kurrentdb");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("kurrentdb", containerResource.Name);
+
+ var endpoints = containerResource.Annotations.OfType();
+ Assert.Single(endpoints);
+
+ var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http");
+ Assert.Equal(KurrentDBResource.DefaultHttpPort, primaryEndpoint.TargetPort);
+ Assert.False(primaryEndpoint.IsExternal);
+ Assert.Equal("http", primaryEndpoint.Name);
+ Assert.Null(primaryEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol);
+ Assert.Equal("http", primaryEndpoint.Transport);
+ Assert.Equal("http", primaryEndpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(KurrentDBContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(KurrentDBContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(KurrentDBContainerImageTags.Registry, containerAnnotation.Registry);
+
+ var config = await kurrentdb.Resource.GetEnvironmentVariableValuesAsync();
+
+ Assert.Collection(config,
+ env =>
+ {
+ Assert.Equal("EVENTSTORE_CLUSTER_SIZE", env.Key);
+ Assert.Equal("1", env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("EVENTSTORE_RUN_PROJECTIONS", env.Key);
+ Assert.Equal("All", env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("EVENTSTORE_START_STANDARD_PROJECTIONS", env.Key);
+ Assert.Equal("true", env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("EVENTSTORE_NODE_PORT", env.Key);
+ Assert.Equal($"{KurrentDBResource.DefaultHttpPort}", env.Value);
+ },
+ ext =>
+ {
+ Assert.Equal("EVENTSTORE_INSECURE", ext.Key);
+ Assert.Equal("true", ext.Value);
+ });
+ }
+
+ [Fact]
+ public async Task KurrentDBCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var kurrentdb = appBuilder
+ .AddKurrentDB("kurrentdb")
+ .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 22113));
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString;
+ var connectionString = await connectionStringResource.GetConnectionStringAsync();
+
+ Assert.Equal("esdb://localhost:22113?tls=false", connectionString);
+ Assert.Equal("esdb://{kurrentdb.bindings.http.host}:{kurrentdb.bindings.http.port}?tls=false", connectionStringResource.ConnectionStringExpression.ValueExpression);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests.csproj
new file mode 100644
index 000000000..c484cb2da
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests.csproj
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs
new file mode 100644
index 000000000..8a386876b
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs
@@ -0,0 +1,261 @@
+using Aspire.Components.Common.Tests;
+using Aspire.Hosting;
+using Aspire.Hosting.Utils;
+using EventStore.Client;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using System.Text;
+using System.Text.Json;
+using Xunit.Abstractions;
+
+namespace CommunityToolkit.Aspire.Hosting.KurrentDB.Tests;
+
+[RequiresDocker]
+public class KurrentDBFunctionalTests(ITestOutputHelper testOutputHelper)
+{
+ public const string TestStreamNamePrefix = "account-";
+ public const string TestAccountName = "John Doe";
+
+ [Fact]
+ public async Task VerifyKurrentDBResource()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
+
+ var kurrentdb = builder.AddKurrentDB("kurrentdb");
+
+ using var app = builder.Build();
+
+ await app.StartAsync();
+
+ var rns = app.Services.GetRequiredService();
+
+ await rns.WaitForResourceHealthyAsync(kurrentdb.Resource.Name, default);
+
+ var hostBuilder = Host.CreateApplicationBuilder();
+
+ hostBuilder.Configuration[$"ConnectionStrings:{kurrentdb.Resource.Name}"] = await kurrentdb.Resource.ConnectionStringExpression.GetValueAsync(default);
+
+ hostBuilder.AddKurrentDBClient(kurrentdb.Resource.Name);
+
+ using var host = hostBuilder.Build();
+
+ await host.StartAsync();
+
+ var eventStoreClient = host.Services.GetRequiredService();
+
+ var id = await CreateTestDataAsync(eventStoreClient);
+ await VerifyTestDataAsync(eventStoreClient, id);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
+ {
+ string? volumeName = null;
+ string? bindMountPath = null;
+ Guid? id = null;
+
+ try
+ {
+ using var builder1 = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
+ var kurrentdb1 = builder1.AddKurrentDB("kurrentdb");
+
+ if (useVolume)
+ {
+ // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
+ volumeName = VolumeNameGenerator.Generate(kurrentdb1, nameof(WithDataShouldPersistStateBetweenUsages));
+
+ // if the volume already exists (because of a crashing previous run), delete it
+ DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true);
+ kurrentdb1.WithDataVolume(volumeName);
+ }
+ else
+ {
+ bindMountPath = Directory.CreateTempSubdirectory().FullName;
+
+ if (!OperatingSystem.IsWindows())
+ {
+ // Change permissions for non-root accounts (container user account)
+ const UnixFileMode OwnershipPermissions =
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
+
+ File.SetUnixFileMode(bindMountPath, OwnershipPermissions);
+ }
+
+ kurrentdb1.WithDataBindMount(bindMountPath);
+ }
+
+ using (var app = builder1.Build())
+ {
+ await app.StartAsync();
+
+ var rns = app.Services.GetRequiredService();
+
+ await rns.WaitForResourceHealthyAsync(kurrentdb1.Resource.Name, default);
+
+ try
+ {
+ var hostBuilder = Host.CreateApplicationBuilder();
+
+ hostBuilder.Configuration[$"ConnectionStrings:{kurrentdb1.Resource.Name}"] = await kurrentdb1.Resource.ConnectionStringExpression.GetValueAsync(default);
+
+ hostBuilder.AddKurrentDBClient(kurrentdb1.Resource.Name);
+
+ using (var host = hostBuilder.Build())
+ {
+ await host.StartAsync();
+
+ var eventStoreClient = host.Services.GetRequiredService();
+ id = await CreateTestDataAsync(eventStoreClient);
+ await VerifyTestDataAsync(eventStoreClient, id.Value);
+ }
+ }
+ finally
+ {
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync();
+ }
+ }
+
+ using var builder2 = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
+ var kurrentdb2 = builder2.AddKurrentDB("kurrentdb");
+
+ if (useVolume)
+ {
+ kurrentdb2.WithDataVolume(volumeName);
+ }
+ else
+ {
+ kurrentdb2.WithDataBindMount(bindMountPath!);
+ }
+
+ using (var app = builder2.Build())
+ {
+ await app.StartAsync();
+
+ var rns = app.Services.GetRequiredService();
+
+ await rns.WaitForResourceHealthyAsync(kurrentdb1.Resource.Name, default);
+
+ try
+ {
+ var hostBuilder = Host.CreateApplicationBuilder();
+
+ hostBuilder.Configuration[$"ConnectionStrings:{kurrentdb2.Resource.Name}"] = await kurrentdb2.Resource.ConnectionStringExpression.GetValueAsync(default);
+
+ hostBuilder.AddKurrentDBClient(kurrentdb2.Resource.Name);
+
+ using (var host = hostBuilder.Build())
+ {
+ await host.StartAsync();
+ var eventStoreClient = host.Services.GetRequiredService();
+
+ await VerifyTestDataAsync(eventStoreClient, id.Value);
+ }
+ }
+ finally
+ {
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync();
+ }
+ }
+
+ }
+ finally
+ {
+ if (volumeName is not null)
+ {
+ DockerUtils.AttemptDeleteDockerVolume(volumeName);
+ }
+
+ if (bindMountPath is not null)
+ {
+ try
+ {
+ Directory.Delete(bindMountPath, recursive: true);
+ }
+ catch
+ {
+ // Don't fail test if we can't clean the temporary folder
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public async Task VerifyWaitForKurrentDBBlocksDependentResources()
+ {
+ var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+ using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
+
+ var healthCheckTcs = new TaskCompletionSource();
+ builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
+ {
+ return healthCheckTcs.Task;
+ });
+
+ var resource = builder.AddKurrentDB("resource")
+ .WithHealthCheck("blocking_check");
+
+ var dependentResource = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
+ .WaitFor(resource);
+
+ using var app = builder.Build();
+
+ var pendingStart = app.StartAsync(cts.Token);
+
+ var rns = app.Services.GetRequiredService();
+
+ await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);
+
+ await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
+
+ healthCheckTcs.SetResult(HealthCheckResult.Healthy());
+
+ await rns.WaitForResourceHealthyAsync(resource.Resource.Name, cts.Token);
+
+ await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
+
+ await pendingStart;
+
+ await app.StopAsync();
+ }
+
+ private static async Task CreateTestDataAsync(EventStoreClient eventStoreClient)
+ {
+ var id = Guid.NewGuid();
+ var accountCreated = new AccountCreated(id, TestAccountName);
+ var data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(accountCreated));
+ var eventData = new EventData(Uuid.NewUuid(), nameof(AccountCreated), data);
+ var streamName = $"{TestStreamNamePrefix}{id}";
+
+ var writeResult = await eventStoreClient.AppendToStreamAsync(streamName, StreamRevision.None, [eventData]);
+ Assert.NotNull(writeResult);
+
+ return id;
+ }
+
+ private static async Task VerifyTestDataAsync(EventStoreClient eventStoreClient, Guid id)
+ {
+ var streamName = $"{TestStreamNamePrefix}{id}";
+
+ var readResult = eventStoreClient.ReadStreamAsync(Direction.Forwards, streamName, StreamPosition.Start);
+ Assert.NotNull(readResult);
+
+ var readState = await readResult.ReadState;
+ Assert.Equal(ReadState.Ok, readState);
+
+ await foreach (var resolvedEvent in readResult)
+ {
+ var @event = JsonSerializer.Deserialize(Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span));
+ Assert.NotNull(@event);
+ Assert.Equal(id, @event.Id);
+ Assert.Equal(TestAccountName, @event.Name);
+ }
+ }
+
+ private sealed record AccountCreated(Guid Id, string Name);
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBPublicApiTests.cs
new file mode 100644
index 000000000..f37716a7c
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBPublicApiTests.cs
@@ -0,0 +1,140 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting;
+
+namespace CommunityToolkit.Aspire.Hosting.KurrentDB.Tests;
+
+public class KurrentDBPublicApiTests
+{
+ [Fact]
+ public void AddKurrentDBShouldThrowWhenBuilderIsNull()
+ {
+ IDistributedApplicationBuilder builder = null!;
+ const string name = "kurrentdb";
+
+ var action = () => builder.AddKurrentDB(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKurrentDBShouldThrowWhenNameIsNull()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ string name = null!;
+
+ var action = () => builder.AddKurrentDB(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void WithDataShouldThrowWhenBuilderIsNull(bool useVolume)
+ {
+ IResourceBuilder builder = null!;
+
+ Func>? action = null;
+
+ if (useVolume)
+ {
+ action = () => builder.WithDataVolume();
+ }
+ else
+ {
+ const string source = "/data";
+
+ action = () => builder.WithDataBindMount(source);
+ }
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void WithDataBindMountShouldThrowWhenSourceIsNull()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ var kurrentdb = builder.AddKurrentDB("kurrentdb");
+
+ string source = null!;
+
+ var action = () => kurrentdb.WithDataBindMount(source);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(source), exception.ParamName);
+ }
+
+ [Fact]
+ public void WithDataVolumeShouldAddMountAnnotation()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ var kurrentdb = builder.AddKurrentDB("kurrentdb")
+ .WithDataVolume(name: null);
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = appModel.Resources.OfType().SingleOrDefault();
+
+ Assert.NotNull(resource);
+ Assert.Equal("kurrentdb", resource.Name);
+
+ Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation));
+ Assert.EndsWith("-data", mountAnnotation.Source);
+ Assert.Equal("/var/lib/kurrentdb", mountAnnotation.Target);
+ }
+
+ [Fact]
+ public void WithNamedDataVolumeShouldAddMountAnnotation()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ var kurrentdb = builder.AddKurrentDB("kurrentdb")
+ .WithDataVolume("mydata");
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = appModel.Resources.OfType().SingleOrDefault();
+
+ Assert.NotNull(resource);
+ Assert.Equal("kurrentdb", resource.Name);
+
+ Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation));
+ Assert.Equal("mydata", mountAnnotation.Source);
+ Assert.Equal("/var/lib/kurrentdb", mountAnnotation.Target);
+ }
+
+ [Fact]
+ public void WithDataBindMountShouldAddMountAnnotation()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ var kurrentdb = builder.AddKurrentDB("kurrentdb")
+ .WithDataBindMount("./mydata");
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+ var resource = appModel.Resources.OfType().SingleOrDefault();
+
+ Assert.NotNull(resource);
+ Assert.Equal("kurrentdb", resource.Name);
+
+ Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation));
+ Assert.EndsWith("mydata", mountAnnotation.Source);
+ Assert.Equal("/var/lib/kurrentdb", mountAnnotation.Target);
+ }
+
+ [Fact]
+ public void KurrentDBResourceCtorShouldThrowWhenNameIsNull()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ const string name = null!;
+
+ var action = () => new KurrentDBResource(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs
new file mode 100644
index 000000000..ad2429ff0
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.Common.Tests;
+using EventStore.Client;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+
+namespace CommunityToolkit.Aspire.KurrentDB.Tests;
+
+public class AspireKurrentDBClientExtensionsTest(KurrentDBContainerFixture containerFixture) : IClassFixture
+{
+ private const string DefaultConnectionName = "kurrentdb";
+
+ private string DefaultConnectionString =>
+ RequiresDockerAttribute.IsSupported ? containerFixture.GetConnectionString() : "esdb://localhost:2113?tls=false";
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ [RequiresDocker]
+ public async Task AddKurrentDBClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed)
+ {
+ var key = DefaultConnectionName;
+
+ var builder = CreateBuilder(DefaultConnectionString);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedKurrentDBClient(key, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+ else
+ {
+ builder.AddKurrentDBClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+
+ using var host = builder.Build();
+
+ var healthCheckService = host.Services.GetRequiredService();
+
+ var healthCheckReport = await healthCheckService.CheckHealthAsync();
+
+ var healthCheckName = useKeyed ? $"KurrentDB.Client_{key}" : "KurrentDB.Client";
+
+ Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName);
+ }
+
+ [Fact]
+ public void CanAddMultipleKeyedServices()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:kurrentdb1", "esdb://localhost:22113?tls=false"),
+ new KeyValuePair("ConnectionStrings:kurrentdb2", "esdb://localhost:22114?tls=false"),
+ new KeyValuePair("ConnectionStrings:kurrentdb3", "esdb://localhost:22115?tls=false"),
+ ]);
+
+ builder.AddKurrentDBClient("kurrentdb1");
+ builder.AddKeyedKurrentDBClient("kurrentdb2");
+ builder.AddKeyedKurrentDBClient("kurrentdb3");
+
+ using var host = builder.Build();
+
+ var client1 = host.Services.GetRequiredService();
+ var client2 = host.Services.GetRequiredKeyedService("kurrentdb2");
+ var client3 = host.Services.GetRequiredKeyedService("kurrentdb3");
+
+ Assert.NotSame(client1, client2);
+ Assert.NotSame(client1, client3);
+ Assert.NotSame(client2, client3);
+ }
+
+ private static HostApplicationBuilder CreateBuilder(string connectionString)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair($"ConnectionStrings:{DefaultConnectionName}", connectionString)
+ ]);
+ return builder;
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj
new file mode 100644
index 000000000..fabc6da45
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConfigurationTests.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConfigurationTests.cs
new file mode 100644
index 000000000..d68a78997
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConfigurationTests.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace CommunityToolkit.Aspire.KurrentDB.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void ConnectionStringIsNullByDefault() =>
+ Assert.Null(new KurrentDBSettings().ConnectionString);
+
+ [Fact]
+ public void HealthChecksEnabledByDefault() =>
+ Assert.False(new KurrentDBSettings().DisableHealthChecks);
+
+ [Fact]
+ public void HealthCheckTimeoutNullByDefault() =>
+ Assert.Null(new KurrentDBSettings().HealthCheckTimeout);
+
+ [Fact]
+ public void DisableTracingIsFalseByDefault() =>
+ Assert.False(new KurrentDBSettings().DisableTracing);
+}
diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs
new file mode 100644
index 000000000..634d2cb09
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.Common.Tests;
+using Aspire.Components.ConformanceTests;
+using EventStore.Client;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+
+namespace CommunityToolkit.Aspire.KurrentDB.Tests;
+
+public class ConformanceTests(KurrentDBContainerFixture containerFixture) : ConformanceTests, IClassFixture
+{
+ private readonly KurrentDBContainerFixture _containerFixture = containerFixture;
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+
+ protected override string ActivitySourceName => string.Empty;
+
+ protected override string[] RequiredLogCategories => [];
+
+ protected override bool CanConnectToServer => RequiresDockerAttribute.IsSupported;
+
+ protected override bool SupportsKeyedRegistrations => true;
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ {
+ var connectionString = RequiresDockerAttribute.IsSupported
+ ? $"{_containerFixture.GetConnectionString()}"
+ : "esdb://localhost:22113?tls=false";
+
+ configuration.AddInMemoryCollection(
+ [
+ new KeyValuePair($"Aspire:KurrentDB:Client:ConnectionString", $"{connectionString}"),
+ new KeyValuePair($"ConnectionStrings:{key}", $"{connectionString}")
+ ]);
+ }
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ if (key is null)
+ {
+ builder.AddKurrentDBClient("kurrentdb", configureSettings: configure);
+ }
+ else
+ {
+ builder.AddKeyedKurrentDBClient(key, configureSettings: configure);
+ }
+ }
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "KurrentDB": {
+ "Client": {
+ "ConnectionString": "esdb://localhost:22113?tls=false",
+ "DisableHealthChecks": "false"
+ }
+ }
+ }
+ }
+ """;
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"Aspire": { "KurrentDB":{ "Client": { "ConnectionString": 3 }}}}""", "Value is \"integer\" but should be \"string\"")
+ };
+
+ protected override void SetHealthCheck(KurrentDBSettings options, bool enabled)
+ {
+ options.DisableHealthChecks = !enabled;
+ }
+
+ protected override void SetMetrics(KurrentDBSettings options, bool enabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void SetTracing(KurrentDBSettings options, bool enabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void TriggerActivity(EventStoreClient service)
+ {
+ using var source = new CancellationTokenSource(100);
+
+ var readResult = service.ReadAllAsync(direction: Direction.Backwards, position: Position.End, maxCount: 1);
+
+ readResult.Messages.ToArrayAsync().GetAwaiter().GetResult();
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBClientPublicApiTests.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBClientPublicApiTests.cs
new file mode 100644
index 000000000..33327fc69
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBClientPublicApiTests.cs
@@ -0,0 +1,88 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace CommunityToolkit.Aspire.KurrentDB.Tests;
+
+public class KurrentDBClientPublicApiTests
+{
+ [Fact]
+ public void AddKurrentDBClientShouldThrowWhenBuilderIsNull()
+ {
+ IHostApplicationBuilder builder = null!;
+
+ var connectionName = "kurrentdb";
+
+ var action = () => builder.AddKurrentDBClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKurrentDBClientShouldThrowWhenNameIsNull()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string connectionName = null!;
+
+ var action = () => builder.AddKurrentDBClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(connectionName), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKurrentDBClientShouldThrowWhenNameIsEmpty()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string connectionName = "";
+
+ var action = () => builder.AddKurrentDBClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(connectionName), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedKurrentDBClientShouldThrowWhenBuilderIsNull()
+ {
+ IHostApplicationBuilder builder = null!;
+
+ var connectionName = "kurrentdb";
+
+ var action = () => builder.AddKeyedKurrentDBClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedKurrentDBClientShouldThrowWhenNameIsNull()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string name = null!;
+
+ var action = () => builder.AddKeyedKurrentDBClient(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedKurrentDBClientShouldThrowWhenNameIsEmpty()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string name = "";
+
+ var action = () => builder.AddKeyedKurrentDBClient(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs
new file mode 100644
index 000000000..b81f2cfc2
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/KurrentDBContainerFixture.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.Common.Tests;
+using CommunityToolkit.Aspire.Hosting.KurrentDB;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+
+namespace CommunityToolkit.Aspire.KurrentDB.Tests;
+
+public sealed class KurrentDBContainerFixture : IAsyncLifetime
+{
+ public IContainer? Container { get; private set; }
+
+ public string GetConnectionString()
+ {
+ if (Container is null)
+ {
+ throw new InvalidOperationException("The test container was not initialized.");
+ }
+ var endpoint = new UriBuilder("esdb", Container.Hostname, Container.GetMappedPublicPort(2113)).ToString();
+ return $"{endpoint}?tls=false";
+ }
+
+ public async Task InitializeAsync()
+ {
+ if (RequiresDockerAttribute.IsSupported)
+ {
+ Container = new ContainerBuilder()
+ .WithImage($"{KurrentDBContainerImageTags.Registry}/{KurrentDBContainerImageTags.Image}:{KurrentDBContainerImageTags.Tag}")
+ .WithPortBinding(2113, true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(2113)))
+ .WithEnvironment("EVENTSTORE_CLUSTER_SIZE", "1")
+ .WithEnvironment("EVENTSTORE_NODE_PORT", "2113")
+ .WithEnvironment("EVENTSTORE_INSECURE", "true")
+ .Build();
+
+ await Container.StartAsync();
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (Container is not null)
+ {
+ await Container.DisposeAsync();
+ }
+ }
+}
From 2d64e910cc97b54ec76f62de4431d9693968518b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 13 Oct 2025 01:27:00 +0000
Subject: [PATCH 04/12] Add KurrentDB examples and update test workflow
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
.github/workflows/tests.yaml | 2 +
.../Account.cs | 105 ++++++++++++++++
.../AccountEvents.cs | 7 ++
...Aspire.Hosting.KurrentDB.ApiService.csproj | 18 +++
.../KurrentDBExtensions.cs | 76 ++++++++++++
.../Program.cs | 81 ++++++++++++
.../Properties/launchSettings.json | 41 ++++++
.../appsettings.json | 9 ++
...it.Aspire.Hosting.KurrentDB.AppHost.csproj | 21 ++++
.../Program.cs | 11 ++
.../Properties/launchSettings.json | 29 +++++
.../appsettings.json | 9 ++
...e.Hosting.KurrentDB.ServiceDefaults.csproj | 21 ++++
.../Extensions.cs | 117 ++++++++++++++++++
14 files changed, 547 insertions(+)
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Account.cs
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/AccountEvents.cs
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService.csproj
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Properties/launchSettings.json
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/appsettings.json
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost.csproj
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Program.cs
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Properties/launchSettings.json
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/appsettings.json
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj
create mode 100644 examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/Extensions.cs
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index e178e4885..0d33b6bfd 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -33,6 +33,7 @@ jobs:
Hosting.Golang.Tests,
Hosting.Java.Tests,
Hosting.k6.Tests,
+ Hosting.KurrentDB.Tests,
Hosting.LavinMQ.Tests,
Hosting.MailPit.Tests,
Hosting.McpInspector.Tests,
@@ -60,6 +61,7 @@ jobs:
# Client integration tests
EventStore.Tests,
GoFeatureFlag.Tests,
+ KurrentDB.Tests,
MassTransit.RabbitMQ.Tests,
Meilisearch.Tests,
Microsoft.Data.Sqlite.Tests,
diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Account.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Account.cs
new file mode 100644
index 000000000..5c814a326
--- /dev/null
+++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Account.cs
@@ -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