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 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; + } +} diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/AccountEvents.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/AccountEvents.cs new file mode 100644 index 000000000..5ae87805c --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/AccountEvents.cs @@ -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); diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService.csproj b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService.csproj new file mode 100644 index 000000000..50174618e --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService.csproj @@ -0,0 +1,18 @@ + + + + enable + enable + + + + + + + + + + + + + diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs new file mode 100644 index 000000000..030abaa1b --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs @@ -0,0 +1,76 @@ +using EventStore.Client; +using System.Text.Json; +using System.Text; + +namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService; + +public static class KurrentDBExtensions +{ + public static async Task GetAccount(this EventStoreClient 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 AppendAcountEvents(this EventStoreClient 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 ? StreamRevision.None : StreamRevision.FromInt64(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 + { + { "EventClrTypeName", @event.GetType().AssemblyQualifiedName! } + })) + ); + } +} diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs new file mode 100644 index 000000000..8234544b1 --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs @@ -0,0 +1,81 @@ +using CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService; +using EventStore.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 (EventStoreClient eventStore, CancellationToken cancellationToken) => +{ + var account = Account.Create(Guid.NewGuid(), "John Doe"); + + account.Deposit(100); + + await eventStore.AppendAcountEvents(account, cancellationToken); + + return Results.Created($"/account/{account.Id}", account); +}); + +app.MapGet("/account/{id:guid}", async (Guid id, EventStoreClient 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, EventStoreClient eventStore, CancellationToken cancellationToken) => +{ + var account = await eventStore.GetAccount(id, cancellationToken); + if (account is null) + { + return Results.NotFound(); + } + + account.Deposit(request.Amount); + + await eventStore.AppendAcountEvents(account, cancellationToken); + + return Results.Ok(); +}); + +app.MapPost("/account/{id:guid}/withdraw", async (Guid id, WithdrawRequest request, EventStoreClient eventStore, CancellationToken cancellationToken) => +{ + var account = await eventStore.GetAccount(id, cancellationToken); + if (account is null) + { + return Results.NotFound(); + } + + account.Withdraw(request.Amount); + + await eventStore.AppendAcountEvents(account, cancellationToken); + + return Results.Ok(); +}); + +app.Run(); + +public record DepositRequest(decimal Amount); +public record WithdrawRequest(decimal Amount); diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Properties/launchSettings.json b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Properties/launchSettings.json new file mode 100644 index 000000000..08fc43745 --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38959", + "sslPort": 44303 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5279", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7015;http://localhost:5279", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/appsettings.json b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost.csproj b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost.csproj new file mode 100644 index 000000000..f11be1abf --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + Exe + enable + enable + true + 9ea31b5e-317f-4692-8a61-e60ac7ec0d0a + + + + + + + + + + + + diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Program.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Program.cs new file mode 100644 index 000000000..67f71f309 --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Program.cs @@ -0,0 +1,11 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var kurrentdb = builder.AddKurrentDB("kurrentdb", 22113); + +builder.AddProject("apiservice") + .WithReference(kurrentdb) + .WaitFor(kurrentdb); + +builder.Build().Run(); diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Properties/launchSettings.json b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..f996ed794 --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17038;http://localhost:15090", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21125", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22133" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15090", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19068", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20178" + } + } + } +} diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/appsettings.json b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj new file mode 100644 index 000000000..caa6344dc --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/Extensions.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..1081a52f3 --- /dev/null +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ServiceDefaults/Extensions.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} From a69bcbd8e71efaffe73ae7e7a3e245560250fc34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 02:57:55 +0000 Subject: [PATCH 05/12] Revert API file changes and remove manually created KurrentDB API files Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../api/CommunityToolkit.Aspire.EventStore.cs | 2 -- ...munityToolkit.Aspire.Hosting.EventStore.cs | 2 -- ...mmunityToolkit.Aspire.Hosting.KurrentDB.cs | 31 ------------------- .../api/CommunityToolkit.Aspire.KurrentDB.cs | 31 ------------------- 4 files changed, 66 deletions(-) delete mode 100644 src/CommunityToolkit.Aspire.Hosting.KurrentDB/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs delete mode 100644 src/CommunityToolkit.Aspire.KurrentDB/api/CommunityToolkit.Aspire.KurrentDB.cs diff --git a/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs b/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs index f0c7b4a53..877984e0b 100644 --- a/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs +++ b/src/CommunityToolkit.Aspire.EventStore/api/CommunityToolkit.Aspire.EventStore.cs @@ -8,7 +8,6 @@ //------------------------------------------------------------------------------ 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 { } } @@ -23,7 +22,6 @@ 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/api/CommunityToolkit.Aspire.Hosting.EventStore.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/api/CommunityToolkit.Aspire.Hosting.EventStore.cs index 78d52fbbe..d3ff14047 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,7 +8,6 @@ //------------------------------------------------------------------------------ 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; } @@ -21,7 +20,6 @@ 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/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs deleted file mode 100644 index 05b3bd0a1..000000000 --- a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/api/CommunityToolkit.Aspire.Hosting.KurrentDB.cs +++ /dev/null @@ -1,31 +0,0 @@ -//------------------------------------------------------------------------------ -// -// 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/api/CommunityToolkit.Aspire.KurrentDB.cs b/src/CommunityToolkit.Aspire.KurrentDB/api/CommunityToolkit.Aspire.KurrentDB.cs deleted file mode 100644 index 5fae44601..000000000 --- a/src/CommunityToolkit.Aspire.KurrentDB/api/CommunityToolkit.Aspire.KurrentDB.cs +++ /dev/null @@ -1,31 +0,0 @@ -//------------------------------------------------------------------------------ -// -// 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 b3706a4baacf93b2c8af7d46e63bb7ff0c3c74e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 01:28:28 +0000 Subject: [PATCH 06/12] Update README to use correct KurrentDB type names and GitHub links Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- src/CommunityToolkit.Aspire.KurrentDB/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.KurrentDB/README.md b/src/CommunityToolkit.Aspire.KurrentDB/README.md index 4202fab23..d7f770dfa 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/README.md +++ b/src/CommunityToolkit.Aspire.KurrentDB/README.md @@ -1,6 +1,6 @@ # CommunityToolkit.Aspire.KurrentDB -Registers an [EventStoreClient](https://github.com/EventStore/EventStore-Client-Dotnet) in the DI container for connecting to KurrentDB. +Registers an [KurrentDBClient](https://github.com/kurrent-io/KurrentDB-Client-Dotnet) in the DI container for connecting to KurrentDB. ## Getting started @@ -18,7 +18,7 @@ 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. +In the _Program.cs_ file of your project, call the `AddKurrentDBClient` extension method to register an `KurrentDBClient` for use via the dependency injection container. The method takes a connection name parameter. ```csharp builder.AddKurrentDBClient("kurrentdb"); @@ -94,10 +94,10 @@ The `WithReference` method configures a connection in the `MyService` project na builder.AddKurrentDBClient("kurrentdb"); ``` -Then, in your service, inject `EventStoreClient` and use it to interact with the KurrentDB API: +Then, in your service, inject `KurrentDBClient` and use it to interact with the KurrentDB API: ```csharp -public class MyService(EventStoreClient eventStoreClient) +public class MyService(KurrentDBClient eventStoreClient) { // ... } @@ -105,7 +105,7 @@ public class MyService(EventStoreClient eventStoreClient) ## Additional documentation -- https://github.com/EventStore/EventStore-Client-Dotnet +- https://github.com/kurrent-io/KurrentDB-Client-Dotnet - https://www.kurrent.io - https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-kurrentdb From dd8c8b97e32f75e565e496e1c2490bd47de5555c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 02:24:01 +0000 Subject: [PATCH 07/12] Remove EventStore test projects from repository and CI workflow Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .github/workflows/tests.yaml | 1 - .../AspireEventStoreClientExtensionsTest.cs | 89 ------ ...nityToolkit.Aspire.EventStore.Tests.csproj | 12 - .../ConfigurationTests.cs | 23 -- .../ConformanceTests.cs | 92 ------ .../EventStoreClientPublicApiTests.cs | 88 ------ .../EventStoreContainerFixture.cs | 49 ---- .../AddEventStoreTests.cs | 90 ------ .../AppHostTests.cs | 111 -------- ...kit.Aspire.Hosting.EventStore.Tests.csproj | 10 - .../EventStoreFunctionalTests.cs | 261 ------------------ .../EventStorePublicApiTests.cs | 140 ---------- 12 files changed, 966 deletions(-) delete mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs delete mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj delete mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs delete mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs delete mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreClientPublicApiTests.cs delete mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs delete mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs delete mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs delete mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj delete mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs delete mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0d33b6bfd..ca1f7f1c3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -59,7 +59,6 @@ jobs: Hosting.SurrealDb.Tests, # Client integration tests - EventStore.Tests, GoFeatureFlag.Tests, KurrentDB.Tests, MassTransit.RabbitMQ.Tests, diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs deleted file mode 100644 index adf1307bf..000000000 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs +++ /dev/null @@ -1,89 +0,0 @@ -// 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.EventStore.Tests; - -public class AspireEventStoreClientExtensionsTest(EventStoreContainerFixture containerFixture) : IClassFixture -{ - private const string DefaultConnectionName = "eventstore"; - - private string DefaultConnectionString => - RequiresDockerAttribute.IsSupported ? containerFixture.GetConnectionString() : "esdb://localhost:2113?tls=false"; - - [Theory] - [InlineData(true)] - [InlineData(false)] - [RequiresDocker] - public async Task AddEventStoreClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed) - { - var key = DefaultConnectionName; - - var builder = CreateBuilder(DefaultConnectionString); - - if (useKeyed) - { - builder.AddKeyedEventStoreClient(key, settings => - { - settings.DisableHealthChecks = false; - }); - } - else - { - builder.AddEventStoreClient(DefaultConnectionName, settings => - { - settings.DisableHealthChecks = false; - }); - } - - using var host = builder.Build(); - - var healthCheckService = host.Services.GetRequiredService(); - - var healthCheckReport = await healthCheckService.CheckHealthAsync(); - - var healthCheckName = useKeyed ? $"EventStore.Client_{key}" : "EventStore.Client"; - - Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName); - } - - [Fact] - public void CanAddMultipleKeyedServices() - { - var builder = Host.CreateEmptyApplicationBuilder(null); - builder.Configuration.AddInMemoryCollection([ - new KeyValuePair("ConnectionStrings:eventstore1", "esdb://localhost:22113?tls=false"), - new KeyValuePair("ConnectionStrings:eventstore2", "esdb://localhost:22114?tls=false"), - new KeyValuePair("ConnectionStrings:eventstore3", "esdb://localhost:22115?tls=false"), - ]); - - builder.AddEventStoreClient("eventstore1"); - builder.AddKeyedEventStoreClient("eventstore2"); - builder.AddKeyedEventStoreClient("eventstore3"); - - using var host = builder.Build(); - - var client1 = host.Services.GetRequiredService(); - var client2 = host.Services.GetRequiredKeyedService("eventstore2"); - var client3 = host.Services.GetRequiredKeyedService("eventstore3"); - - 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.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj b/tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj deleted file mode 100644 index fd0b3c3b2..000000000 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs deleted file mode 100644 index e82a370ee..000000000 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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.EventStore.Tests; - -public class ConfigurationTests -{ - [Fact] - public void ConnectionStringIsNullByDefault() => - Assert.Null(new EventStoreSettings().ConnectionString); - - [Fact] - public void HealthChecksEnabledByDefault() => - Assert.False(new EventStoreSettings().DisableHealthChecks); - - [Fact] - public void HealthCheckTimeoutNullByDefault() => - Assert.Null(new EventStoreSettings().HealthCheckTimeout); - - [Fact] - public void DisableTracingIsFalseByDefault() => - Assert.False(new EventStoreSettings().DisableTracing); -} diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs deleted file mode 100644 index ec2637221..000000000 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// 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.EventStore.Tests; - -public class ConformanceTests(EventStoreContainerFixture containerFixture) : ConformanceTests, IClassFixture -{ - private readonly EventStoreContainerFixture _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:EventStore: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.AddEventStoreClient("eventstore", configureSettings: configure); - } - else - { - builder.AddKeyedEventStoreClient(key, configureSettings: configure); - } - } - - protected override string ValidJsonConfig => """ - { - "Aspire": { - "EventStore": { - "Client": { - "ConnectionString": "esdb://localhost:22113?tls=false", - "DisableHealthChecks": "false" - } - } - } - } - """; - - protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] - { - ("""{"Aspire": { "EventStore":{ "Client": { "ConnectionString": 3 }}}}""", "Value is \"integer\" but should be \"string\"") - }; - - protected override void SetHealthCheck(EventStoreSettings options, bool enabled) - { - options.DisableHealthChecks = !enabled; - } - - protected override void SetMetrics(EventStoreSettings options, bool enabled) - { - throw new NotImplementedException(); - } - - protected override void SetTracing(EventStoreSettings 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.EventStore.Tests/EventStoreClientPublicApiTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreClientPublicApiTests.cs deleted file mode 100644 index 19fe11f0b..000000000 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreClientPublicApiTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -// 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.EventStore.Tests; - -public class EventStoreClientPublicApiTests -{ - [Fact] - public void AddEventStoreClientShouldThrowWhenBuilderIsNull() - { - IHostApplicationBuilder builder = null!; - - var connectionName = "eventstore"; - - var action = () => builder.AddEventStoreClient(connectionName); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(builder), exception.ParamName); - } - - [Fact] - public void AddEventStoreClientShouldThrowWhenNameIsNull() - { - var builder = Host.CreateEmptyApplicationBuilder(null); - - string connectionName = null!; - - var action = () => builder.AddEventStoreClient(connectionName); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(connectionName), exception.ParamName); - } - - [Fact] - public void AddEventStoreClientShouldThrowWhenNameIsEmpty() - { - var builder = Host.CreateEmptyApplicationBuilder(null); - - string connectionName = ""; - - var action = () => builder.AddEventStoreClient(connectionName); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(connectionName), exception.ParamName); - } - - [Fact] - public void AddKeyedEventStoreClientShouldThrowWhenBuilderIsNull() - { - IHostApplicationBuilder builder = null!; - - var connectionName = "eventstore"; - - var action = () => builder.AddKeyedEventStoreClient(connectionName); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(builder), exception.ParamName); - } - - [Fact] - public void AddKeyedEventStoreClientShouldThrowWhenNameIsNull() - { - var builder = Host.CreateEmptyApplicationBuilder(null); - - string name = null!; - - var action = () => builder.AddKeyedEventStoreClient(name); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(name), exception.ParamName); - } - - [Fact] - public void AddKeyedEventStoreClientShouldThrowWhenNameIsEmpty() - { - var builder = Host.CreateEmptyApplicationBuilder(null); - - string name = ""; - - var action = () => builder.AddKeyedEventStoreClient(name); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(name), exception.ParamName); - } -} diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs deleted file mode 100644 index e8f07a8d6..000000000 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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.EventStore; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; - -namespace CommunityToolkit.Aspire.EventStore.Tests; - -public sealed class EventStoreContainerFixture : 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($"{EventStoreContainerImageTags.Registry}/{EventStoreContainerImageTags.Image}:{EventStoreContainerImageTags.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(); - } - } -} diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs deleted file mode 100644 index 1707a2fcd..000000000 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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.EventStore.Tests; - -public class AddEventStoreTests -{ - [Fact] - public async Task AddEventStoreContainerWithDefaultsAddsAnnotationMetadata() - { - var appBuilder = DistributedApplication.CreateBuilder(); - - var eventstore = appBuilder.AddEventStore("eventstore"); - - using var app = appBuilder.Build(); - - var appModel = app.Services.GetRequiredService(); - - var containerResource = Assert.Single(appModel.Resources.OfType()); - Assert.Equal("eventstore", containerResource.Name); - - var endpoints = containerResource.Annotations.OfType(); - Assert.Single(endpoints); - - var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http"); - Assert.Equal(EventStoreResource.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(EventStoreContainerImageTags.Tag, containerAnnotation.Tag); - Assert.Equal(EventStoreContainerImageTags.Image, containerAnnotation.Image); - Assert.Equal(EventStoreContainerImageTags.Registry, containerAnnotation.Registry); - - var config = await eventstore.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($"{EventStoreResource.DefaultHttpPort}", env.Value); - }, - ext => - { - Assert.Equal("EVENTSTORE_INSECURE", ext.Key); - Assert.Equal("true", ext.Value); - }); - } - - [Fact] - public async Task EventStoreCreatesConnectionString() - { - var appBuilder = DistributedApplication.CreateBuilder(); - var eventstore = appBuilder - .AddEventStore("eventstore") - .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://{eventstore.bindings.http.host}:{eventstore.bindings.http.port}?tls=false", connectionStringResource.ConnectionStringExpression.ValueExpression); - } -} diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs deleted file mode 100644 index cf24e1669..000000000 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -// 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.Testing; -using Projects; -using System.Net.Http.Json; - -namespace CommunityToolkit.Aspire.Hosting.EventStore.Tests; - -[RequiresDocker] -public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> -{ - [Fact] - public async Task ResourceStartsAndRespondsOk() - { - var resourceName = "eventstore"; - await fixture.ResourceNotificationService - .WaitForResourceHealthyAsync(resourceName) - .WaitAsync(TimeSpan.FromMinutes(1)); - - var httpClient = fixture.CreateHttpClient(resourceName); - - var response = await httpClient.GetAsync("/"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task ApiServiceCreateAccount() - { - var resourceName = "apiservice"; - await fixture.ResourceNotificationService - .WaitForResourceHealthyAsync("eventstore") - .WaitAsync(TimeSpan.FromMinutes(1)); - await fixture.ResourceNotificationService - .WaitForResourceHealthyAsync(resourceName) - .WaitAsync(TimeSpan.FromMinutes(1)); - - var httpClient = fixture.CreateHttpClient(resourceName); - - var createResponse = await httpClient.PostAsJsonAsync("/account/create", new { }); - Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); - - var location = createResponse.Headers.Location; - - var getResponse = await httpClient.GetAsync(location); - Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); - - var account = await getResponse.Content.ReadFromJsonAsync(); - Assert.NotNull(account); - Assert.Equal("John Doe", account.Name); - Assert.Equal(100, account.Balance); - } - - [Fact] - public async Task ApiServiceCreateAccountAndDeposit() - { - var resourceName = "apiservice"; - await fixture.ResourceNotificationService - .WaitForResourceHealthyAsync(resourceName) - .WaitAsync(TimeSpan.FromMinutes(1)); - - var httpClient = fixture.CreateHttpClient(resourceName); - - var createResponse = await httpClient.PostAsJsonAsync("/account/create", new { }); - Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); - - var location = createResponse.Headers.Location; - - var depositResponse = await httpClient.PostAsJsonAsync($"{location!}/deposit", new { Amount = 50 }); - Assert.Equal(HttpStatusCode.OK, depositResponse.StatusCode); - - var getResponse = await httpClient.GetAsync(location); - Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); - - var account = await getResponse.Content.ReadFromJsonAsync(); - Assert.NotNull(account); - Assert.Equal("John Doe", account.Name); - Assert.Equal(150, account.Balance); - } - - [Fact] - public async Task ApiServiceCreateAccountAndWithdraw() - { - var resourceName = "apiservice"; - await fixture.ResourceNotificationService - .WaitForResourceHealthyAsync(resourceName) - .WaitAsync(TimeSpan.FromMinutes(1)); - - var httpClient = fixture.CreateHttpClient(resourceName); - - var createResponse = await httpClient.PostAsJsonAsync("/account/create", new { }); - Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); - - var location = createResponse.Headers.Location; - - var depositResponse = await httpClient.PostAsJsonAsync($"{location!}/withdraw", new { Amount = 90 }); - Assert.Equal(HttpStatusCode.OK, depositResponse.StatusCode); - - var getResponse = await httpClient.GetAsync(location); - Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); - - var account = await getResponse.Content.ReadFromJsonAsync(); - Assert.NotNull(account); - Assert.Equal("John Doe", account.Name); - Assert.Equal(10, account.Balance); - } - - public record AccountDto(Guid Id, string Name, decimal Balance); -} diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj deleted file mode 100644 index 60df07d69..000000000 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs deleted file mode 100644 index f8a1bbea9..000000000 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ /dev/null @@ -1,261 +0,0 @@ -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.EventStore.Tests; - -[RequiresDocker] -public class EventStoreFunctionalTests(ITestOutputHelper testOutputHelper) -{ - public const string TestStreamNamePrefix = "account-"; - public const string TestAccountName = "John Doe"; - - [Fact] - public async Task VerifyEventStoreResource() - { - using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); - - var eventstore = builder.AddEventStore("eventstore"); - - using var app = builder.Build(); - - await app.StartAsync(); - - var rns = app.Services.GetRequiredService(); - - await rns.WaitForResourceHealthyAsync(eventstore.Resource.Name, default); - - var hostBuilder = Host.CreateApplicationBuilder(); - - hostBuilder.Configuration[$"ConnectionStrings:{eventstore.Resource.Name}"] = await eventstore.Resource.ConnectionStringExpression.GetValueAsync(default); - - hostBuilder.AddEventStoreClient(eventstore.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 eventstore1 = builder1.AddEventStore("eventstore"); - - if (useVolume) - { - // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails - volumeName = VolumeNameGenerator.Generate(eventstore1, nameof(WithDataShouldPersistStateBetweenUsages)); - - // if the volume already exists (because of a crashing previous run), delete it - DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); - eventstore1.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); - } - - eventstore1.WithDataBindMount(bindMountPath); - } - - using (var app = builder1.Build()) - { - await app.StartAsync(); - - var rns = app.Services.GetRequiredService(); - - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); - - try - { - var hostBuilder = Host.CreateApplicationBuilder(); - - hostBuilder.Configuration[$"ConnectionStrings:{eventstore1.Resource.Name}"] = await eventstore1.Resource.ConnectionStringExpression.GetValueAsync(default); - - hostBuilder.AddEventStoreClient(eventstore1.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 eventstore2 = builder2.AddEventStore("eventstore"); - - if (useVolume) - { - eventstore2.WithDataVolume(volumeName); - } - else - { - eventstore2.WithDataBindMount(bindMountPath!); - } - - using (var app = builder2.Build()) - { - await app.StartAsync(); - - var rns = app.Services.GetRequiredService(); - - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); - - try - { - var hostBuilder = Host.CreateApplicationBuilder(); - - hostBuilder.Configuration[$"ConnectionStrings:{eventstore2.Resource.Name}"] = await eventstore2.Resource.ConnectionStringExpression.GetValueAsync(default); - - hostBuilder.AddEventStoreClient(eventstore2.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 VerifyWaitForEventStoreBlocksDependentResources() - { - 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.AddEventStore("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.EventStore.Tests/EventStorePublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs deleted file mode 100644 index 4546d29dc..000000000 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// 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.EventStore.Tests; - -public class EventStorePublicApiTests -{ - [Fact] - public void AddEventStoreShouldThrowWhenBuilderIsNull() - { - IDistributedApplicationBuilder builder = null!; - const string name = "eventstore"; - - var action = () => builder.AddEventStore(name); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(builder), exception.ParamName); - } - - [Fact] - public void AddEventStoreShouldThrowWhenNameIsNull() - { - var builder = new DistributedApplicationBuilder([]); - string name = null!; - - var action = () => builder.AddEventStore(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 eventstore = builder.AddEventStore("eventstore"); - - string source = null!; - - var action = () => eventstore.WithDataBindMount(source); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(source), exception.ParamName); - } - - [Fact] - public void WithDataVolumeShouldAddMountAnnotation() - { - var builder = new DistributedApplicationBuilder([]); - var eventstore = builder.AddEventStore("eventstore") - .WithDataVolume(name: null); - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = appModel.Resources.OfType().SingleOrDefault(); - - Assert.NotNull(resource); - Assert.Equal("eventstore", resource.Name); - - Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); - Assert.EndsWith("-data", mountAnnotation.Source); - Assert.Equal("/var/lib/eventstore", mountAnnotation.Target); - } - - [Fact] - public void WithNamedDataVolumeShouldAddMountAnnotation() - { - var builder = new DistributedApplicationBuilder([]); - var eventstore = builder.AddEventStore("eventstore") - .WithDataVolume("mydata"); - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = appModel.Resources.OfType().SingleOrDefault(); - - Assert.NotNull(resource); - Assert.Equal("eventstore", resource.Name); - - Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); - Assert.Equal("mydata", mountAnnotation.Source); - Assert.Equal("/var/lib/eventstore", mountAnnotation.Target); - } - - [Fact] - public void WithDataBindMountShouldAddMountAnnotation() - { - var builder = new DistributedApplicationBuilder([]); - var eventstore = builder.AddEventStore("eventstore") - .WithDataBindMount("./mydata"); - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = appModel.Resources.OfType().SingleOrDefault(); - - Assert.NotNull(resource); - Assert.Equal("eventstore", resource.Name); - - Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); - Assert.EndsWith("mydata", mountAnnotation.Source); - Assert.Equal("/var/lib/eventstore", mountAnnotation.Target); - } - - [Fact] - public void EventStoreResourceCtorShouldThrowWhenNameIsNull() - { - var builder = new DistributedApplicationBuilder([]); - const string name = null!; - - var action = () => new EventStoreResource(name); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(name), exception.ParamName); - } -} From d969f4f570a36f344e29bc2bf1090b95b05b4dc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 03:55:44 +0000 Subject: [PATCH 08/12] Update solution file to include KurrentDB projects and remove EventStore test projects Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- CommunityToolkit.Aspire.slnx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index b27af11f5..b6e91e7a5 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -35,6 +35,11 @@ + + + + + @@ -170,6 +175,7 @@ + @@ -193,6 +199,7 @@ + @@ -208,7 +215,6 @@ - @@ -216,11 +222,11 @@ - + @@ -244,6 +250,7 @@ + From ab8cb36344a98bcf4d78c623bc56fe14b7684d6d Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 22 Oct 2025 00:35:15 +0000 Subject: [PATCH 09/12] Removing test from the test list --- .github/workflows/tests.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a2507816e..9dcad744a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -28,7 +28,6 @@ jobs: Hosting.Dapr.Tests, Hosting.DbGate.Tests, Hosting.Deno.Tests, - Hosting.EventStore.Tests, Hosting.Flagd.Tests, Hosting.GoFeatureFlag.Tests, Hosting.Golang.Tests, From cf7b6b037d2cb7c4fcab8d6cd353da663c090d30 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 22 Oct 2025 12:01:53 +1100 Subject: [PATCH 10/12] Registering the right client --- Directory.Packages.props | 1 + .../AspireKurrentDBExtensions.cs | 11 ++++++----- .../CommunityToolkit.Aspire.KurrentDB.csproj | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 98fea59f7..0c8897238 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,6 +18,7 @@ + diff --git a/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs index c1e109fbe..8344bafb8 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs +++ b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs @@ -6,6 +6,7 @@ using EventStore.Client; using EventStore.Client.Extensions.OpenTelemetry; using HealthChecks.EventStore.gRPC; +using KurrentDB.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -31,7 +32,7 @@ public static void AddKurrentDBClient( Action? configureSettings = null) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNullOrEmpty(connectionName); + ArgumentException.ThrowIfNullOrEmpty(connectionName); AddKurrentDBClient(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null); } @@ -47,7 +48,7 @@ public static void AddKeyedKurrentDBClient( Action? configureSettings = null) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNullOrEmpty(name); + ArgumentException.ThrowIfNullOrEmpty(name); AddKurrentDBClient(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name); } @@ -97,12 +98,12 @@ private static void AddKurrentDBClient( timeout: settings.HealthCheckTimeout)); } - EventStoreClient ConfigureKurrentDBClient(IServiceProvider serviceProvider) + KurrentDBClient ConfigureKurrentDBClient(IServiceProvider serviceProvider) { if (settings.ConnectionString is not null) { - var eventStoreClientSettings = EventStoreClientSettings.Create(settings.ConnectionString!); - return new EventStoreClient(eventStoreClientSettings); + var eventStoreClientSettings = KurrentDBClientSettings.Create(settings.ConnectionString!); + return new KurrentDBClient(eventStoreClientSettings); } else { diff --git a/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj b/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj index 8555cbe4b..c02b1d924 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj +++ b/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj @@ -9,6 +9,7 @@ + From afdf0097682e87222545010b0c465fea9df39288 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 22 Oct 2025 01:33:40 +0000 Subject: [PATCH 11/12] Fixing a bunch of places that were still using EventStoreClient --- .../KurrentDBExtensions.cs | 10 +++---- .../Program.cs | 16 ++++++------ .../KurrentDBContainerImageTags.cs | 6 ++--- .../AspireKurrentDBExtensions.cs | 8 +++--- .../KurrentDBSettings.cs | 4 ++- .../README.md | 2 +- .../KurrentDBFunctionalTests.cs | 26 +++++++++---------- .../AspireKurrentDBClientExtensionsTest.cs | 8 +++--- .../ConformanceTests.cs | 6 ++--- 9 files changed, 44 insertions(+), 42 deletions(-) diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs index 030abaa1b..71160a21d 100644 --- a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/KurrentDBExtensions.cs @@ -1,12 +1,12 @@ -using EventStore.Client; -using System.Text.Json; +using System.Text.Json; using System.Text; +using KurrentDB.Client; namespace CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService; public static class KurrentDBExtensions { - public static async Task GetAccount(this EventStoreClient eventStore, Guid id, CancellationToken cancellationToken) + public static async Task GetAccount(this KurrentDBClient eventStore, Guid id, CancellationToken cancellationToken) { var readResult = eventStore.ReadStreamAsync( Direction.Forwards, @@ -33,7 +33,7 @@ public static class KurrentDBExtensions return account; } - public static async Task AppendAcountEvents(this EventStoreClient eventStore, Account account, CancellationToken cancellationToken) + public static async Task AppendAccountEvents(this KurrentDBClient eventStore, Account account, CancellationToken cancellationToken) { var events = account.DequeueUncommittedEvents(); @@ -43,7 +43,7 @@ public static async Task AppendAcountEvents(this EventStoreClient eventStore, Ac var expectedVersion = account.Version - events.Length; await eventStore.AppendToStreamAsync( $"account-{account.Id:N}", - expectedVersion == 0 ? StreamRevision.None : StreamRevision.FromInt64(expectedVersion), + expectedVersion == 0 ? StreamState.NoStream : StreamState.StreamRevision((ulong)expectedVersion), eventsToAppend, cancellationToken: cancellationToken ); diff --git a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs index 8234544b1..0b102a3e5 100644 --- a/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs +++ b/examples/kurrentdb/CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService/Program.cs @@ -1,5 +1,5 @@ using CommunityToolkit.Aspire.Hosting.KurrentDB.ApiService; -using EventStore.Client; +using KurrentDB.Client; var builder = WebApplication.CreateBuilder(args); @@ -23,18 +23,18 @@ app.UseSwaggerUI(); } -app.MapPost("/account/create", async (EventStoreClient eventStore, CancellationToken cancellationToken) => +app.MapPost("/account/create", async (KurrentDBClient eventStore, CancellationToken cancellationToken) => { var account = Account.Create(Guid.NewGuid(), "John Doe"); account.Deposit(100); - await eventStore.AppendAcountEvents(account, cancellationToken); + await eventStore.AppendAccountEvents(account, cancellationToken); return Results.Created($"/account/{account.Id}", account); }); -app.MapGet("/account/{id:guid}", async (Guid id, EventStoreClient eventStore, CancellationToken cancellationToken) => +app.MapGet("/account/{id:guid}", async (Guid id, KurrentDBClient eventStore, CancellationToken cancellationToken) => { var account = await eventStore.GetAccount(id, cancellationToken); if (account is null) @@ -45,7 +45,7 @@ return TypedResults.Ok(account); }); -app.MapPost("/account/{id:guid}/deposit", async (Guid id, DepositRequest request, EventStoreClient eventStore, CancellationToken cancellationToken) => +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) @@ -55,12 +55,12 @@ account.Deposit(request.Amount); - await eventStore.AppendAcountEvents(account, cancellationToken); + await eventStore.AppendAccountEvents(account, cancellationToken); return Results.Ok(); }); -app.MapPost("/account/{id:guid}/withdraw", async (Guid id, WithdrawRequest request, EventStoreClient eventStore, CancellationToken cancellationToken) => +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) @@ -70,7 +70,7 @@ account.Withdraw(request.Amount); - await eventStore.AppendAcountEvents(account, cancellationToken); + await eventStore.AppendAccountEvents(account, cancellationToken); return Results.Ok(); }); diff --git a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs index d0088533b..516a62364 100644 --- a/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.KurrentDB/KurrentDBContainerImageTags.cs @@ -5,7 +5,7 @@ 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"; + public const string Registry = "docker.kurrent.io"; + public const string Image = "kurrent-latest/kurrentdb"; + public const string Tag = "25.1"; } diff --git a/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs index 8344bafb8..e7d836803 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs +++ b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs @@ -21,7 +21,7 @@ public static class AspireKurrentDBExtensions private const string DefaultConfigSectionName = "Aspire:KurrentDB:Client"; /// - /// Registers as a singleton in the services provided by the . + /// 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. @@ -37,7 +37,7 @@ public static void AddKurrentDBClient( } /// - /// Registers as a keyed singleton for the given in the services provided by the . + /// 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. @@ -102,8 +102,8 @@ KurrentDBClient ConfigureKurrentDBClient(IServiceProvider serviceProvider) { if (settings.ConnectionString is not null) { - var eventStoreClientSettings = KurrentDBClientSettings.Create(settings.ConnectionString!); - return new KurrentDBClient(eventStoreClientSettings); + var clientSettings = KurrentDBClientSettings.Create(settings.ConnectionString!); + return new KurrentDBClient(clientSettings); } else { diff --git a/src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs b/src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs index 950e2167f..3c78b0e4e 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs +++ b/src/CommunityToolkit.Aspire.KurrentDB/KurrentDBSettings.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using KurrentDB.Client; + namespace CommunityToolkit.Aspire.KurrentDB; /// -/// Provides the client configuration settings for connecting to a KurrentDB server using EventStoreClient. +/// Provides the client configuration settings for connecting to a KurrentDB server using . /// public sealed class KurrentDBSettings { diff --git a/src/CommunityToolkit.Aspire.KurrentDB/README.md b/src/CommunityToolkit.Aspire.KurrentDB/README.md index d7f770dfa..076512110 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/README.md +++ b/src/CommunityToolkit.Aspire.KurrentDB/README.md @@ -97,7 +97,7 @@ builder.AddKurrentDBClient("kurrentdb"); Then, in your service, inject `KurrentDBClient` and use it to interact with the KurrentDB API: ```csharp -public class MyService(KurrentDBClient eventStoreClient) +public class MyService(KurrentDBClient client) { // ... } diff --git a/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs index 8a386876b..3b1b431f8 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.KurrentDB.Tests/KurrentDBFunctionalTests.cs @@ -1,7 +1,7 @@ using Aspire.Components.Common.Tests; using Aspire.Hosting; using Aspire.Hosting.Utils; -using EventStore.Client; +using KurrentDB.Client; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; using System.Text; @@ -41,10 +41,10 @@ public async Task VerifyKurrentDBResource() await host.StartAsync(); - var eventStoreClient = host.Services.GetRequiredService(); + var kurrentDBClient = host.Services.GetRequiredService(); - var id = await CreateTestDataAsync(eventStoreClient); - await VerifyTestDataAsync(eventStoreClient, id); + var id = await CreateTestDataAsync(kurrentDBClient); + await VerifyTestDataAsync(kurrentDBClient, id); } [Theory] @@ -108,9 +108,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) { await host.StartAsync(); - var eventStoreClient = host.Services.GetRequiredService(); - id = await CreateTestDataAsync(eventStoreClient); - await VerifyTestDataAsync(eventStoreClient, id.Value); + var kurrentDBClient = host.Services.GetRequiredService(); + id = await CreateTestDataAsync(kurrentDBClient); + await VerifyTestDataAsync(kurrentDBClient, id.Value); } } finally @@ -151,9 +151,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using (var host = hostBuilder.Build()) { await host.StartAsync(); - var eventStoreClient = host.Services.GetRequiredService(); + var kurrentDBClient = host.Services.GetRequiredService(); - await VerifyTestDataAsync(eventStoreClient, id.Value); + await VerifyTestDataAsync(kurrentDBClient, id.Value); } } finally @@ -224,7 +224,7 @@ public async Task VerifyWaitForKurrentDBBlocksDependentResources() await app.StopAsync(); } - private static async Task CreateTestDataAsync(EventStoreClient eventStoreClient) + private static async Task CreateTestDataAsync(KurrentDBClient kurrentDBClient) { var id = Guid.NewGuid(); var accountCreated = new AccountCreated(id, TestAccountName); @@ -232,17 +232,17 @@ private static async Task CreateTestDataAsync(EventStoreClient eventStoreC var eventData = new EventData(Uuid.NewUuid(), nameof(AccountCreated), data); var streamName = $"{TestStreamNamePrefix}{id}"; - var writeResult = await eventStoreClient.AppendToStreamAsync(streamName, StreamRevision.None, [eventData]); + var writeResult = await kurrentDBClient.AppendToStreamAsync(streamName, StreamState.NoStream, [eventData]); Assert.NotNull(writeResult); return id; } - private static async Task VerifyTestDataAsync(EventStoreClient eventStoreClient, Guid id) + private static async Task VerifyTestDataAsync(KurrentDBClient kurrentDBClient, Guid id) { var streamName = $"{TestStreamNamePrefix}{id}"; - var readResult = eventStoreClient.ReadStreamAsync(Direction.Forwards, streamName, StreamPosition.Start); + var readResult = kurrentDBClient.ReadStreamAsync(Direction.Forwards, streamName, StreamPosition.Start); Assert.NotNull(readResult); var readState = await readResult.ReadState; diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs index ad2429ff0..357252acc 100644 --- a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs +++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/AspireKurrentDBClientExtensionsTest.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Components.Common.Tests; -using EventStore.Client; +using KurrentDB.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; @@ -68,9 +68,9 @@ public void CanAddMultipleKeyedServices() using var host = builder.Build(); - var client1 = host.Services.GetRequiredService(); - var client2 = host.Services.GetRequiredKeyedService("kurrentdb2"); - var client3 = host.Services.GetRequiredKeyedService("kurrentdb3"); + 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); diff --git a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs index 634d2cb09..41391480f 100644 --- a/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs +++ b/tests/CommunityToolkit.Aspire.KurrentDB.Tests/ConformanceTests.cs @@ -3,13 +3,13 @@ using Aspire.Components.Common.Tests; using Aspire.Components.ConformanceTests; -using EventStore.Client; +using KurrentDB.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; namespace CommunityToolkit.Aspire.KurrentDB.Tests; -public class ConformanceTests(KurrentDBContainerFixture containerFixture) : ConformanceTests, IClassFixture +public class ConformanceTests(KurrentDBContainerFixture containerFixture) : ConformanceTests, IClassFixture { private readonly KurrentDBContainerFixture _containerFixture = containerFixture; @@ -81,7 +81,7 @@ protected override void SetTracing(KurrentDBSettings options, bool enabled) throw new NotImplementedException(); } - protected override void TriggerActivity(EventStoreClient service) + protected override void TriggerActivity(KurrentDBClient service) { using var source = new CancellationTokenSource(100); From 9218939a7846f44c96ff616ce311331889ce4420 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Sun, 26 Oct 2025 09:45:15 +1000 Subject: [PATCH 12/12] Fix KurrentDB tracing --- .../AspireKurrentDBExtensions.cs | 5 ++--- .../CommunityToolkit.Aspire.KurrentDB.csproj | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs index e7d836803..57efb06a6 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs +++ b/src/CommunityToolkit.Aspire.KurrentDB/AspireKurrentDBExtensions.cs @@ -3,10 +3,9 @@ using Aspire; using CommunityToolkit.Aspire.KurrentDB; -using EventStore.Client; -using EventStore.Client.Extensions.OpenTelemetry; using HealthChecks.EventStore.gRPC; using KurrentDB.Client; +using KurrentDB.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -83,7 +82,7 @@ private static void AddKurrentDBClient( if (!settings.DisableTracing) { builder.Services.AddOpenTelemetry() - .WithTracing(traceBuilder => traceBuilder.AddEventStoreClientInstrumentation()); + .WithTracing(traceBuilder => traceBuilder.AddKurrentDBClientInstrumentation()); } if (!settings.DisableHealthChecks) diff --git a/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj b/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj index c02b1d924..d4f5dfcb5 100644 --- a/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj +++ b/src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj @@ -7,8 +7,6 @@ - -