diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index 56ce128a017..862ddf7fba1 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -263,9 +263,9 @@ public static IResourceBuilder RunAsEmulator(this IResou var blobEndpoint = storage.GetEndpoint("blob"); var tableEndpoint = storage.GetEndpoint("table"); - context.EnvironmentVariables.Add("ACCEPT_EULA", "Y"); - context.EnvironmentVariables.Add("BLOB_SERVER", $"{blobEndpoint.Resource.Name}:{blobEndpoint.TargetPort}"); - context.EnvironmentVariables.Add("METADATA_SERVER", $"{tableEndpoint.Resource.Name}:{tableEndpoint.TargetPort}"); + context.EnvironmentVariables["ACCEPT_EULA"] = "Y"; + context.EnvironmentVariables["BLOB_SERVER"] = $"{blobEndpoint.Resource.Name}:{blobEndpoint.TargetPort}"; + context.EnvironmentVariables["METADATA_SERVER"] = $"{tableEndpoint.Resource.Name}:{tableEndpoint.TargetPort}"; })); // RunAsEmulator() can be followed by custom model configuration so we need to delay the creation of the Config.json file diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs index b4453afb558..20117563d7b 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs @@ -383,9 +383,9 @@ public static IResourceBuilder RunAsEmulator(this IReso { var sqlEndpoint = sqlServerResource.Resource.GetEndpoint("tcp"); - context.EnvironmentVariables.Add("ACCEPT_EULA", "Y"); - context.EnvironmentVariables.Add("SQL_SERVER", $"{sqlEndpoint.Resource.Name}:{sqlEndpoint.TargetPort}"); - context.EnvironmentVariables.Add("MSSQL_SA_PASSWORD", passwordParameter); + context.EnvironmentVariables["ACCEPT_EULA"] = "Y"; + context.EnvironmentVariables["SQL_SERVER"] = $"{sqlEndpoint.Resource.Name}:{sqlEndpoint.TargetPort}"; + context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = passwordParameter; })); var lifetime = ContainerLifetime.Session; diff --git a/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs b/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs index 106badcb114..cbb91e49c5e 100644 --- a/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs +++ b/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs @@ -140,8 +140,8 @@ static void ConfigureKafkaUIContainer(EnvironmentCallbackContext context, Endpoi ? ReferenceExpression.Create($"{endpoint.Resource.Name}:{endpoint.Property(EndpointProperty.TargetPort)}") : ReferenceExpression.Create($"{endpoint.Property(EndpointProperty.HostAndPort)}"); - context.EnvironmentVariables.Add($"KAFKA_CLUSTERS_{index}_NAME", endpoint.Resource.Name); - context.EnvironmentVariables.Add($"KAFKA_CLUSTERS_{index}_BOOTSTRAPSERVERS", bootstrapServers); + context.EnvironmentVariables[$"KAFKA_CLUSTERS_{index}_NAME"] = endpoint.Resource.Name; + context.EnvironmentVariables[$"KAFKA_CLUSTERS_{index}_BOOTSTRAPSERVERS"] = bootstrapServers; } } @@ -203,9 +203,9 @@ private static void ConfigureKafkaContainer(EnvironmentCallbackContext context, // See https://github.com/confluentinc/kafka-images/blob/master/local/include/etc/confluent/docker/configureDefaults for more details. // Define the default listeners + an internal listener for the container to broker communication - context.EnvironmentVariables.Add($"KAFKA_LISTENERS", $"PLAINTEXT://localhost:29092,CONTROLLER://localhost:29093,PLAINTEXT_HOST://0.0.0.0:{KafkaBrokerPort},PLAINTEXT_INTERNAL://0.0.0.0:{KafkaInternalBrokerPort}"); + context.EnvironmentVariables[$"KAFKA_LISTENERS"] = $"PLAINTEXT://localhost:29092,CONTROLLER://localhost:29093,PLAINTEXT_HOST://0.0.0.0:{KafkaBrokerPort},PLAINTEXT_INTERNAL://0.0.0.0:{KafkaInternalBrokerPort}"; // Defaults default listeners security protocol map + the internal listener to be PLAINTEXT - context.EnvironmentVariables.Add("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT"); + context.EnvironmentVariables["KAFKA_LISTENER_SECURITY_PROTOCOL_MAP"] = "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT"; // primaryEndpoint is the endpoint that is exposed to the host machine var primaryEndpoint = resource.PrimaryEndpoint; diff --git a/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs b/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs index 3f4c029a343..4e86970b0d7 100644 --- a/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs +++ b/src/Aspire.Hosting.Milvus/MilvusBuilderExtensions.cs @@ -215,6 +215,6 @@ private static void ConfigureAttuContainer(EnvironmentCallbackContext context, M { // Attu assumes Milvus is being accessed over a default Aspire container network and hardcodes the resource address // This will need to be refactored once updated service discovery APIs are available - context.EnvironmentVariables.Add("MILVUS_URL", $"{resource.PrimaryEndpoint.Scheme}://{resource.Name}:{resource.PrimaryEndpoint.TargetPort}"); + context.EnvironmentVariables["MILVUS_URL"] = $"{resource.PrimaryEndpoint.Scheme}://{resource.Name}:{resource.PrimaryEndpoint.TargetPort}"; } } diff --git a/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs b/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs index ce45a9cafea..8e70359d35e 100644 --- a/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs +++ b/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs @@ -248,17 +248,17 @@ private static void ConfigureMongoExpressContainer(EnvironmentCallbackContext co { // Mongo Express assumes Mongo is being accessed over a default Aspire container network and hardcodes the resource address // This will need to be refactored once updated service discovery APIs are available - context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_SERVER", resource.Name); + context.EnvironmentVariables["ME_CONFIG_MONGODB_SERVER"] = resource.Name; var targetPort = resource.PrimaryEndpoint.TargetPort; if (targetPort is int targetPortValue) { - context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_PORT", targetPortValue.ToString(CultureInfo.InvariantCulture)); + context.EnvironmentVariables["ME_CONFIG_MONGODB_PORT"] = targetPortValue.ToString(CultureInfo.InvariantCulture); } - context.EnvironmentVariables.Add("ME_CONFIG_BASICAUTH", "false"); + context.EnvironmentVariables["ME_CONFIG_BASICAUTH"] = "false"; if (resource.PasswordParameter is not null) { - context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_ADMINUSERNAME", resource.UserNameReference); - context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_ADMINPASSWORD", resource.PasswordParameter); + context.EnvironmentVariables["ME_CONFIG_MONGODB_ADMINUSERNAME"] = resource.UserNameReference; + context.EnvironmentVariables["ME_CONFIG_MONGODB_ADMINPASSWORD"] = resource.PasswordParameter; } } } diff --git a/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs index 926379acdb4..e937faa797c 100644 --- a/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs @@ -251,9 +251,9 @@ public static IResourceBuilder WithPhpMyAdmin(this IResourceBuilder bui { // PhpMyAdmin assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address // This will need to be refactored once updated service discovery APIs are available - context.EnvironmentVariables.Add("PMA_HOST", $"{endpoint.Resource.Name}:{endpoint.TargetPort}"); - context.EnvironmentVariables.Add("PMA_USER", "root"); - context.EnvironmentVariables.Add("PMA_PASSWORD", singleInstance.PasswordParameter); + context.EnvironmentVariables["PMA_HOST"] = $"{endpoint.Resource.Name}:{endpoint.TargetPort}"; + context.EnvironmentVariables["PMA_USER"] = "root"; + context.EnvironmentVariables["PMA_PASSWORD"] = singleInstance.PasswordParameter; }); } else diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs index 780d65cc401..2b6c66ee64e 100644 --- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs @@ -340,12 +340,12 @@ public static IResourceBuilder WithPgWeb(this IResourceB private static void SetPgAdminEnvironmentVariables(EnvironmentCallbackContext context) { // Disables pgAdmin authentication. - context.EnvironmentVariables.Add("PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED", "False"); - context.EnvironmentVariables.Add("PGADMIN_CONFIG_SERVER_MODE", "False"); + context.EnvironmentVariables["PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED"] = "False"; + context.EnvironmentVariables["PGADMIN_CONFIG_SERVER_MODE"] = "False"; // You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables. - context.EnvironmentVariables.Add("PGADMIN_DEFAULT_EMAIL", "admin@domain.com"); - context.EnvironmentVariables.Add("PGADMIN_DEFAULT_PASSWORD", "admin"); + context.EnvironmentVariables["PGADMIN_DEFAULT_EMAIL"] = "admin@domain.com"; + context.EnvironmentVariables["PGADMIN_DEFAULT_PASSWORD"] = "admin"; // When running in the context of Codespaces we need to set some additional environment // variables so that PGAdmin will trust the forwarded headers that Codespaces port diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index e3666d40894..9df0e13c99f 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -238,12 +238,12 @@ public static IResourceBuilder WithRedisInsight(this IResourceBui foreach (var redisInstance in redisInstances) { // RedisInsight assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address - context.EnvironmentVariables.Add($"RI_REDIS_HOST{counter}", redisInstance.Name); - context.EnvironmentVariables.Add($"RI_REDIS_PORT{counter}", redisInstance.PrimaryEndpoint.TargetPort!.Value); - context.EnvironmentVariables.Add($"RI_REDIS_ALIAS{counter}", redisInstance.Name); + context.EnvironmentVariables[$"RI_REDIS_HOST{counter}"] = redisInstance.Name; + context.EnvironmentVariables[$"RI_REDIS_PORT{counter}"] = redisInstance.PrimaryEndpoint.TargetPort!.Value; + context.EnvironmentVariables[$"RI_REDIS_ALIAS{counter}"] = redisInstance.Name; if (redisInstance.PasswordParameter is not null) { - context.EnvironmentVariables.Add($"RI_REDIS_PASSWORD{counter}", redisInstance.PasswordParameter); + context.EnvironmentVariables[$"RI_REDIS_PASSWORD{counter}"] = redisInstance.PasswordParameter; } counter++; diff --git a/tests/Aspire.Hosting.Kafka.Tests/AddKafkaTests.cs b/tests/Aspire.Hosting.Kafka.Tests/AddKafkaTests.cs index 08f5b42e103..c6ddaa642c9 100644 --- a/tests/Aspire.Hosting.Kafka.Tests/AddKafkaTests.cs +++ b/tests/Aspire.Hosting.Kafka.Tests/AddKafkaTests.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; @@ -176,4 +177,55 @@ public void WithKafkaUIAddsAnUniqueContainerSetsItsNameAndInvokesConfigurationCa Assert.Equal(8080, kafkaUiEndpoint.TargetPort); Assert.Equal(port, kafkaUiEndpoint.Port); } + + [Fact] + public async Task KafkaEnvironmentCallbackIsIdempotent() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var kafka = appBuilder.AddKafka("kafka") + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)); + + // Call GetEnvironmentVariableValuesAsync multiple times to ensure callbacks are idempotent + var config1 = await kafka.Resource.GetEnvironmentVariableValuesAsync(); + var config2 = await kafka.Resource.GetEnvironmentVariableValuesAsync(); + + // Both calls should succeed and return the same values + Assert.Equal(config1.Count, config2.Count); + Assert.Contains(config1, kvp => kvp.Key == "KAFKA_LISTENERS"); + Assert.Contains(config2, kvp => kvp.Key == "KAFKA_LISTENERS"); + Assert.Equal( + config1.First(kvp => kvp.Key == "KAFKA_LISTENERS").Value, + config2.First(kvp => kvp.Key == "KAFKA_LISTENERS").Value); + } + + [Fact] + public async Task KafkaUIEnvironmentCallbackIsIdempotent() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var kafka = appBuilder.AddKafka("kafka1") + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)) + .WithKafkaUI(); + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + var kafkaUiResource = Assert.Single(appModel.Resources.OfType()); + + // Trigger the BeforeResourceStartedEvent to add environment callbacks + await appBuilder.Eventing.PublishAsync( + new BeforeResourceStartedEvent(kafkaUiResource, app.Services), + EventDispatchBehavior.BlockingSequential); + + // Call GetEnvironmentVariableValuesAsync multiple times to ensure callbacks are idempotent + var config1 = await kafkaUiResource.GetEnvironmentVariableValuesAsync(); + var config2 = await kafkaUiResource.GetEnvironmentVariableValuesAsync(); + + // Both calls should succeed and return the same values + Assert.Equal(config1.Count, config2.Count); + Assert.Contains(config1, kvp => kvp.Key == "KAFKA_CLUSTERS_0_NAME"); + Assert.Contains(config2, kvp => kvp.Key == "KAFKA_CLUSTERS_0_NAME"); + Assert.Equal("kafka1", config1.First(kvp => kvp.Key == "KAFKA_CLUSTERS_0_NAME").Value); + Assert.Equal("kafka1", config2.First(kvp => kvp.Key == "KAFKA_CLUSTERS_0_NAME").Value); + } } diff --git a/tests/Aspire.Hosting.MongoDB.Tests/AddMongoDBTests.cs b/tests/Aspire.Hosting.MongoDB.Tests/AddMongoDBTests.cs index 9e022563a03..48c995e8a1d 100644 --- a/tests/Aspire.Hosting.MongoDB.Tests/AddMongoDBTests.cs +++ b/tests/Aspire.Hosting.MongoDB.Tests/AddMongoDBTests.cs @@ -286,4 +286,30 @@ public void CanAddDatabasesWithTheSameNameOnMultipleServers() Assert.Equal("mongodb://admin:{mongo1-password.value}@{mongo1.bindings.tcp.host}:{mongo1.bindings.tcp.port}/imports?authSource=admin&authMechanism=SCRAM-SHA-256", db1.Resource.ConnectionStringExpression.ValueExpression); Assert.Equal("mongodb://admin:{mongo2-password.value}@{mongo2.bindings.tcp.host}:{mongo2.bindings.tcp.port}/imports?authSource=admin&authMechanism=SCRAM-SHA-256", db2.Resource.ConnectionStringExpression.ValueExpression); } + + [Fact] + public async Task MongoExpressEnvironmentCallbackIsIdempotent() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var mongo = appBuilder.AddMongoDB("mongo") + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)) + .WithMongoExpress(); + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + var mongoExpressResource = Assert.Single(appModel.Resources.OfType()); + + // Call GetEnvironmentVariableValuesAsync multiple times to ensure callbacks are idempotent + var config1 = await mongoExpressResource.GetEnvironmentVariableValuesAsync(); + var config2 = await mongoExpressResource.GetEnvironmentVariableValuesAsync(); + + // Both calls should succeed and return the same values + Assert.Equal(config1.Count, config2.Count); + Assert.Contains(config1, kvp => kvp.Key == "ME_CONFIG_MONGODB_SERVER"); + Assert.Contains(config2, kvp => kvp.Key == "ME_CONFIG_MONGODB_SERVER"); + Assert.Equal( + config1.First(kvp => kvp.Key == "ME_CONFIG_MONGODB_SERVER").Value, + config2.First(kvp => kvp.Key == "ME_CONFIG_MONGODB_SERVER").Value); + } } diff --git a/tests/Aspire.Hosting.MySql.Tests/AddMySqlTests.cs b/tests/Aspire.Hosting.MySql.Tests/AddMySqlTests.cs index 749db12093a..f23a2654735 100644 --- a/tests/Aspire.Hosting.MySql.Tests/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.MySql.Tests/AddMySqlTests.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using System.Text.RegularExpressions; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; @@ -365,4 +366,35 @@ public async Task VerifyMySqlServerResourceWithPassword() var connectionString = await connectionStringResource.ConnectionStringExpression.GetValueAsync(default); Assert.Equal("Server=localhost;Port=2000;User ID=root;Password=p@ssw0rd1", connectionString); } + + [Fact] + public async Task PhpMyAdminEnvironmentCallbackIsIdempotent() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var mysql = appBuilder.AddMySql("mysql") + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3306)) + .WithPhpMyAdmin(); + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + var phpMyAdminResource = Assert.Single(appModel.Resources.OfType()); + + // Trigger the BeforeResourceStartedEvent to add environment callbacks + await appBuilder.Eventing.PublishAsync( + new BeforeResourceStartedEvent(phpMyAdminResource, app.Services), + EventDispatchBehavior.BlockingSequential); + + // Call GetEnvironmentVariableValuesAsync multiple times to ensure callbacks are idempotent + var config1 = await phpMyAdminResource.GetEnvironmentVariableValuesAsync(); + var config2 = await phpMyAdminResource.GetEnvironmentVariableValuesAsync(); + + // Both calls should succeed and return the same values + Assert.Equal(config1.Count, config2.Count); + Assert.Contains(config1, kvp => kvp.Key == "PMA_HOST"); + Assert.Contains(config2, kvp => kvp.Key == "PMA_HOST"); + Assert.Equal( + config1.First(kvp => kvp.Key == "PMA_HOST").Value, + config2.First(kvp => kvp.Key == "PMA_HOST").Value); + } } diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs index e88c2c8c54c..f57004fd846 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs @@ -684,4 +684,26 @@ public async Task VerifyPostgresServerResourceWithUserName() Assert.Equal($"Host=localhost;Port=2000;Username=user1;Password={postgres.Resource.PasswordParameter.Value}", connectionString); #pragma warning restore CS0618 // Type or member is obsolete } + + [Fact] + public async Task PostgresEnvironmentCallbackIsIdempotent() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var postgres = appBuilder.AddPostgres("postgres") + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432)); + + // Call GetEnvironmentVariableValuesAsync multiple times to ensure callbacks are idempotent + var config1 = await postgres.Resource.GetEnvironmentVariableValuesAsync(); + var config2 = await postgres.Resource.GetEnvironmentVariableValuesAsync(); + + // Both calls should succeed and return the same values + Assert.Equal(config1.Count, config2.Count); + // Verify that environment variables are set consistently across multiple calls + Assert.All(config1, kvp => + { + Assert.True(config2.ContainsKey(kvp.Key), $"Key {kvp.Key} should exist in second call"); + Assert.Equal(kvp.Value, config2[kvp.Key]); + }); + } } diff --git a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs index e96145c2e63..6c930f9b9c9 100644 --- a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs @@ -701,4 +701,30 @@ public async Task AddRedisContainerWithPasswordAnnotationMetadata() Assert.Equal("{myRedis.bindings.tcp.host}:{myRedis.bindings.tcp.port},password={pass.value}", connectionStringResource.ConnectionStringExpression.ValueExpression); Assert.StartsWith($"localhost:5001,password={password}", connectionString); } + + [Fact] + public async Task RedisInsightEnvironmentCallbackIsIdempotent() + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var redis = appBuilder.AddRedis("redis") + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379)) + .WithRedisInsight(); + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + var redisInsightResource = Assert.Single(appModel.Resources.OfType()); + + // Call GetEnvironmentVariableValuesAsync multiple times to ensure callbacks are idempotent + var config1 = await redisInsightResource.GetEnvironmentVariableValuesAsync(); + var config2 = await redisInsightResource.GetEnvironmentVariableValuesAsync(); + + // Both calls should succeed and return the same values + Assert.Equal(config1.Count, config2.Count); + Assert.Contains(config1, kvp => kvp.Key == "RI_REDIS_HOST1"); + Assert.Contains(config2, kvp => kvp.Key == "RI_REDIS_HOST1"); + Assert.Equal( + config1.First(kvp => kvp.Key == "RI_REDIS_HOST1").Value, + config2.First(kvp => kvp.Key == "RI_REDIS_HOST1").Value); + } }