diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs index e635cb36d..09df82683 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs @@ -81,7 +81,7 @@ private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken var componentReferenceAnnotations = daprSidecar.Annotations.OfType(); - var secrets = new Dictionary(); + var secretValueProviders = new Dictionary(); var endpointEnvironmentVars = new Dictionary(); var hasValueProviders = false; @@ -97,17 +97,17 @@ private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken } } - // Check if there are any secrets that need to be added to the secret store + // Collect secret value providers to be resolved later when environment is set up if (componentReferenceAnnotation.Component.TryGetAnnotationsOfType(out var secretAnnotations)) { foreach (var secretAnnotation in secretAnnotations) { - secrets[secretAnnotation.Key] = (await secretAnnotation.Value.GetValueAsync(cancellationToken))!; + secretValueProviders[secretAnnotation.Key] = secretAnnotation.Value; } } // If we have any secrets or value providers, ensure the secret store path is added - if ((secrets.Count > 0 || hasValueProviders) && onDemandResourcesPaths.TryGetValue("secretstore", out var secretStorePath)) + if ((secretValueProviders.Count > 0 || hasValueProviders) && onDemandResourcesPaths.TryGetValue("secretstore", out var secretStorePath)) { string onDemandResourcesPathDirectory = Path.GetDirectoryName(secretStorePath)!; if (onDemandResourcesPathDirectory is not null) @@ -136,13 +136,15 @@ private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken } } - if (secrets.Count > 0 || endpointEnvironmentVars.Count > 0) + if (secretValueProviders.Count > 0 || endpointEnvironmentVars.Count > 0) { daprSidecar.Annotations.Add(new EnvironmentCallbackAnnotation(async context => { - foreach (var secret in secrets) + // Resolve secrets when environment is being set up (when parameter values are available) + foreach (var (secretKey, secretValueProvider) in secretValueProviders) { - context.EnvironmentVariables.TryAdd(secret.Key, secret.Value); + var secretValue = await secretValueProvider.GetValueAsync(context.CancellationToken); + context.EnvironmentVariables.TryAdd(secretKey, secretValue ?? string.Empty); } // Add value provider references diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/ResourceBuilderExtensionsTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/ResourceBuilderExtensionsTests.cs index a730ed153..948a1ef17 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/ResourceBuilderExtensionsTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/ResourceBuilderExtensionsTests.cs @@ -1,6 +1,7 @@ using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; using System.Runtime.CompilerServices; using Xunit; @@ -146,6 +147,61 @@ public void WithReferenceOnSidecarCorrectlyAttachesDaprComponents() Assert.DoesNotContain(project.Resource.Annotations, a => a is DaprComponentReferenceAnnotation); } + [Fact] + public void SecretParameterValueStoredAsProviderNotResolvedValue() + { + // Arrange + // When a secret parameter is defined without an explicit value, + // it should be stored as an IValueProvider, not resolved immediately + var builder = DistributedApplication.CreateBuilder(); + + // Create a parameter without an explicit value (simulating user input via dashboard) + var secretParam = builder.AddParameter("secretPassword", secret: true); + + // Add a Dapr component that uses the secret parameter + var pubsub = builder.AddDaprPubSub("pubsub") + .WithMetadata("password", secretParam.Resource); + + // Act - Get the component resource and verify annotations + var componentResource = Assert.Single(builder.Resources.OfType()); + + // Assert - Verify that secret annotation exists with the IValueProvider + Assert.True(componentResource.TryGetAnnotationsOfType(out var secretAnnotations)); + var secretAnnotation = Assert.Single(secretAnnotations); + + // The key point: the Value should be an IValueProvider (ParameterResource), not a resolved string + Assert.NotNull(secretAnnotation.Value); + Assert.IsAssignableFrom(secretAnnotation.Value); + + // Verify the key contains the parameter name + Assert.Equal("secretPassword", secretAnnotation.Key); + } + + [Fact] + public void ValueProviderStoredInComponentAnnotation() + { + // Arrange + // This test verifies that value providers (like endpoint references) + // are stored in annotations, not resolved immediately + var builder = DistributedApplication.CreateBuilder(); + + var redis = builder.AddRedis("redis"); + var pubsub = builder.AddDaprPubSub("pubsub") + .WithMetadata("redisHost", redis.GetEndpoint("tcp")); + + // Act - Get the component and verify it has the value provider annotation + var componentResource = Assert.Single(builder.Resources.OfType()); + + // Assert - Verify that the component has the value provider annotation with IValueProvider + Assert.True(componentResource.TryGetAnnotationsOfType(out var valueProviderAnnotations)); + var annotation = Assert.Single(valueProviderAnnotations); + + // Verify that the ValueProvider is stored, not a resolved value + Assert.NotNull(annotation.ValueProvider); + Assert.IsAssignableFrom(annotation.ValueProvider); + Assert.Equal("redisHost", annotation.MetadataName); + } + // Test helper class that implements IValueProvider private class TestValueProvider : global::Aspire.Hosting.ApplicationModel.IValueProvider {