From 88fc92508eff200d5b1bfeb5044b771ba280ef47 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 2 Jul 2025 21:56:22 -0700 Subject: [PATCH 01/12] Added support for prompting for parameter values - Handle prompting for parameters once they have all been processed. This also handles re-prompting until they have all been processed. - Introduced MissingParameterValueException to detect when parameters are have a missing value. - Moved all parameter processing logic into ParameterProcessor --- .../MissingParameterValueException.cs | 36 ++++ .../ApplicationModel/ParameterResource.cs | 16 +- .../DistributedApplicationBuilder.cs | 1 + .../Orchestrator/ApplicationOrchestrator.cs | 52 ++--- .../Orchestrator/ParameterProcessor.cs | 195 ++++++++++++++++++ .../ParameterResourceBuilderExtensions.cs | 4 +- .../ApplicationOrchestratorTests.cs | 19 +- 7 files changed, 278 insertions(+), 45 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/MissingParameterValueException.cs create mode 100644 src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs diff --git a/src/Aspire.Hosting/ApplicationModel/MissingParameterValueException.cs b/src/Aspire.Hosting/ApplicationModel/MissingParameterValueException.cs new file mode 100644 index 00000000000..a32cf737b0c --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/MissingParameterValueException.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// The exception that is thrown when a parameter resource cannot be initialized because its value is missing or cannot be resolved. +/// +/// +/// This exception is typically thrown when: +/// +/// A parameter value is not provided in configuration and has no default value +/// A parameter's value callback throws an exception during execution +/// A parameter's value cannot be retrieved from the configured source (e.g., user secrets, environment variables) +/// +/// +public class MissingParameterValueException : DistributedApplicationException +{ + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public MissingParameterValueException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public MissingParameterValueException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs b/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs index f02d09ff740..6ca4260b80f 100644 --- a/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs @@ -74,5 +74,19 @@ internal string ConfigurationKey set => _configurationKey = value; } - ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) => new(Value); + /// + /// A task completion source that can be used to wait for the value of the parameter to be set. + /// + internal TaskCompletionSource? WaitForValueTcs { get; set; } + + async ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) + { + if (WaitForValueTcs is not null) + { + // Wait for the value to be set if the task completion source is available. + return await WaitForValueTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + return Value; + } } diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 05486e9a500..6bba69b5df1 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -350,6 +350,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // Orchestrator _innerBuilder.Services.AddSingleton(); + _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddHostedService(); // DCP stuff diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 239db3e6dd9..153431f6b96 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -24,6 +24,7 @@ internal sealed class ApplicationOrchestrator private readonly IDistributedApplicationEventing _eventing; private readonly IServiceProvider _serviceProvider; private readonly DistributedApplicationExecutionContext _executionContext; + private readonly ParameterProcessor _parameterProcessor; private readonly CancellationTokenSource _shutdownCancellation = new(); public ApplicationOrchestrator(DistributedApplicationModel model, @@ -34,7 +35,8 @@ public ApplicationOrchestrator(DistributedApplicationModel model, ResourceLoggerService loggerService, IDistributedApplicationEventing eventing, IServiceProvider serviceProvider, - DistributedApplicationExecutionContext executionContext) + DistributedApplicationExecutionContext executionContext, + ParameterProcessor parameterProcessor) { _dcpExecutor = dcpExecutor; _model = model; @@ -45,6 +47,7 @@ public ApplicationOrchestrator(DistributedApplicationModel model, _eventing = eventing; _serviceProvider = serviceProvider; _executionContext = executionContext; + _parameterProcessor = parameterProcessor; dcpExecutorEvents.Subscribe(OnResourcesPrepared); dcpExecutorEvents.Subscribe(OnResourceChanged); @@ -269,51 +272,15 @@ private async Task OnResourceEndpointsAllocated(ResourceEndpointsAllocatedEvent await PublishResourceEndpointUrls(@event.Resource, cancellationToken).ConfigureAwait(false); } - private async Task OnResourceInitialized(InitializeResourceEvent @event, CancellationToken cancellationToken) + private Task OnResourceInitialized(InitializeResourceEvent @event, CancellationToken cancellationToken) { var resource = @event.Resource; - if (resource is ParameterResource parameterResource) - { - await InitializeParameter(parameterResource).ConfigureAwait(false); - } - else if (resource is ConnectionStringResource connectionStringResource) + if (resource is ConnectionStringResource connectionStringResource) { InitializeConnectionString(connectionStringResource); } - async Task InitializeParameter(ParameterResource parameterResource) - { - try - { - await _notificationService.PublishUpdateAsync(parameterResource, s => - { - return s with - { - Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, parameterResource.Value ?? "", parameterResource.Secret), - State = new(KnownResourceStates.Active, KnownResourceStateStyles.Info) - }; - }) - .ConfigureAwait(false); - } - catch (Exception ex) - { - await _notificationService.PublishUpdateAsync(parameterResource, s => - { - return s with - { - State = new("Value missing", KnownResourceStateStyles.Error), - Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, ex.Message), - IsHidden = false - }; - }) - .ConfigureAwait(false); - - _loggerService.GetLogger(parameterResource) - .LogError(ex, "Failed to initialize parameter resource {ResourceName}", parameterResource.Name); - } - } - void InitializeConnectionString(ConnectionStringResource connectionStringResource) { var logger = _loggerService.GetLogger(resource); @@ -349,7 +316,7 @@ void InitializeConnectionString(ConnectionStringResource connectionStringResourc tcs.SetResult(); return Task.CompletedTask; }); - + waitFor.Add(tcs.Task.WaitAsync(cancellationToken)); } } @@ -364,6 +331,8 @@ await _notificationService.PublishUpdateAsync(connectionStringResource, s => s w }).ConfigureAwait(false); }, cancellationToken); } + + return Task.CompletedTask; } private async Task OnResourceChanged(OnResourceChangedContext context) @@ -467,6 +436,9 @@ await SetChildResourceAsync(child, state, startTimeStamp, stopTimeStamp) private async Task PublishResourcesInitialStateAsync(CancellationToken cancellationToken) { + // Initialize all parameter resources up front + await _parameterProcessor.InitializeParametersAsync(_model.Resources.OfType()).ConfigureAwait(false); + // Publish the initial state of the resources that have a snapshot annotation. foreach (var resource in _model.Resources) { diff --git a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs new file mode 100644 index 00000000000..6b284be185d --- /dev/null +++ b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs @@ -0,0 +1,195 @@ +#pragma warning disable ASPIREINTERACTION001 + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Dashboard.Model; +using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.Logging; + +namespace Aspire.Hosting.Orchestrator; + +/// +/// Handles processing of parameter resources during application orchestration. +/// +internal sealed class ParameterProcessor( + ResourceNotificationService notificationService, + ResourceLoggerService loggerService, + IInteractionService interactionService, + ILogger logger) +{ + private readonly ResourceNotificationService _notificationService = notificationService; + private readonly ResourceLoggerService _loggerService = loggerService; + private readonly List _unresolvedParameters = []; + + public async Task InitializeParametersAsync(IEnumerable parameterResources) + { + // Initialize all parameter resources by setting their WaitForValueTcs. + // This allows them to be processed asynchronously later. + foreach (var parameterResource in parameterResources) + { + parameterResource.WaitForValueTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + + await ProcessParameterAsync(parameterResource).ConfigureAwait(false); + } + + // If interaction service is available, we can handle unresolved parameters. + // This will allow the user to provide values for parameters that could not be initialized. + if (interactionService.IsAvailable) + { + // All parameters have been processed, we can now handle unresolved parameters if any. + if (_unresolvedParameters.Count > 0) + { + // Start the loop that will allow the user to specify values for unresolved parameters. + _ = Task.Run(async () => + { + try + { + await HandleUnresolvedParametersAsync().ConfigureAwait(false); + + logger.LogDebug("All unresolved parameters have been handled successfully."); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to handle unresolved parameters"); + } + }); + } + } + } + + private async Task ProcessParameterAsync(ParameterResource parameterResource) + { + try + { + var value = parameterResource.Value ?? ""; + + await _notificationService.PublishUpdateAsync(parameterResource, s => + { + return s with + { + Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, value, parameterResource.Secret), + State = new(KnownResourceStates.Active, KnownResourceStateStyles.Success) + }; + }) + .ConfigureAwait(false); + + parameterResource.WaitForValueTcs?.SetResult(value); + } + catch (Exception ex) + { + // Missing parameter values throw a MissingParameterValueException. + if (interactionService.IsAvailable && ex is MissingParameterValueException) + { + // If interaction service is available, we can prompt the user to provide a value. + // Add the parameter to unresolved parameters list. + _unresolvedParameters.Add(parameterResource); + + _loggerService.GetLogger(parameterResource) + .LogWarning(ex, "Parameter resource {ResourceName} could not be initialized. Waiting for user input.", parameterResource.Name); + } + else + { + // If interaction service is not available, we log the error and set the state to error. + parameterResource.WaitForValueTcs?.SetException(ex); + + _loggerService.GetLogger(parameterResource) + .LogError(ex, "Failed to initialize parameter resource {ResourceName}", parameterResource.Name); + } + + var stateText = ex is MissingParameterValueException ? + "Value missing" : + "Error initializing parameter"; + + await _notificationService.PublishUpdateAsync(parameterResource, s => + { + return s with + { + State = new(stateText, KnownResourceStateStyles.Error), + Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, ex.Message), + IsHidden = false + }; + }) + .ConfigureAwait(false); + } + } + + public async Task HandleUnresolvedParametersAsync() + { + // This method will continue in a loop until all unresolved parameters are resolved. + while (_unresolvedParameters.Count > 0) + { + // First we show a notification that there are unresolved parameters. + var result = await interactionService.PromptMessageBarAsync( + "Unresolved Parameters", + "There are unresolved parameters that need to be set. Please provide values for them.", + new MessageBarInteractionOptions + { + Intent = MessageIntent.Warning, + PrimaryButtonText = "Enter Values" + }) + .ConfigureAwait(false); + + if (result.Data) + { + // Now we build up a new form base on the unresolved parameters. + var inputs = new Dictionary(); + + foreach (var parameter in _unresolvedParameters) + { + // Create an input for each unresolved parameter. + inputs.Add(parameter, new InteractionInput + { + InputType = parameter.Secret ? InputType.SecretText : InputType.Text, + Label = parameter.Name, + Placeholder = "Enter value for " + parameter.Name, + Required = true, + }); + } + + var valuesPrompt = await interactionService.PromptInputsAsync( + "Set Unresolved Parameters", + "Please provide values for the unresolved parameters.", + [.. inputs.Values], + new InputsDialogInteractionOptions + { + PrimaryButtonText = "Submit", + ShowDismiss = true + }) + .ConfigureAwait(false); + + if (!valuesPrompt.Canceled) + { + // Iterate through the unresolved parameters and set their values based on user input. + for (var i = _unresolvedParameters.Count - 1; i >= 0; i--) + { + var parameter = _unresolvedParameters[i]; + var inputValue = valuesPrompt.Data[i].Value; + + if (inputValue is null) + { + // If the input value is null, we skip this parameter. + continue; + } + + parameter.WaitForValueTcs?.TrySetResult(inputValue); + + // Update the parameter resource state to active with the provided value. + await _notificationService.PublishUpdateAsync(parameter, s => + { + return s with + { + Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, inputValue, parameter.Secret), + State = new(KnownResourceStates.Active, KnownResourceStateStyles.Success) + }; + }) + .ConfigureAwait(false); + + // Remove the parameter from unresolved parameters list. + _unresolvedParameters.RemoveAt(i); + } + } + } + } + } +} diff --git a/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs b/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs index 5aef83ac681..08fd709dc1c 100644 --- a/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs @@ -153,7 +153,7 @@ private static string GetParameterValue(ConfigurationManager configuration, stri configurationKey ??= $"Parameters:{name}"; return configuration[configurationKey] ?? parameterDefault?.GetDefaultValue() - ?? throw new DistributedApplicationException($"Parameter resource could not be used because configuration key '{configurationKey}' is missing and the Parameter has no default value."); + ?? throw new MissingParameterValueException($"Parameter resource could not be used because configuration key '{configurationKey}' is missing and the Parameter has no default value."); } internal static IResourceBuilder AddParameter(this IDistributedApplicationBuilder builder, T resource) @@ -191,7 +191,7 @@ public static IResourceBuilder AddConnectionStrin new ConnectionStringParameterResource( name, _ => builder.Configuration.GetConnectionString(name) ?? - throw new DistributedApplicationException($"Connection string parameter resource could not be used because connection string '{name}' is missing."), + throw new MissingParameterValueException($"Connection string parameter resource could not be used because connection string '{name}' is missing."), environmentVariableName) ); } diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs index cbac272013c..945c7da7fb4 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs @@ -9,6 +9,7 @@ using Aspire.Hosting.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Aspire.Hosting.Tests.Orchestrator; @@ -444,6 +445,7 @@ private static ApplicationOrchestrator CreateOrchestrator( ResourceLoggerService? resourceLoggerService = null) { var serviceProvider = new ServiceCollection().BuildServiceProvider(); + resourceLoggerService ??= new ResourceLoggerService(); return new ApplicationOrchestrator( distributedAppModel, @@ -451,14 +453,27 @@ private static ApplicationOrchestrator CreateOrchestrator( dcpEvents ?? new DcpExecutorEvents(), [], notificationService, - resourceLoggerService ?? new ResourceLoggerService(), + resourceLoggerService, applicationEventing ?? new DistributedApplicationEventing(), serviceProvider, new DistributedApplicationExecutionContext( - new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run) { ServiceProvider = serviceProvider }) + new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run) { ServiceProvider = serviceProvider }), + new ParameterProcessor( + notificationService, + resourceLoggerService, + CreateInteractionService(), + NullLogger.Instance) ); } + private static InteractionService CreateInteractionService(DistributedApplicationOptions? options = null) + { + return new InteractionService( + NullLogger.Instance, + options ?? new DistributedApplicationOptions(), + new ServiceCollection().BuildServiceProvider()); + } + private sealed class CustomResource(string name) : Resource(name); private sealed class CustomChildResource(string name, IResource parent) : Resource(name), IResourceWithParent From 985ca1c785a5817892a4b8fc671df2f7b9d098e3 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 2 Jul 2025 22:23:43 -0700 Subject: [PATCH 02/12] Refactor HandleUnresolvedParametersAsync to use List for inputs instead of Dictionary --- src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs index 6b284be185d..61e7b59010e 100644 --- a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs +++ b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs @@ -133,12 +133,12 @@ public async Task HandleUnresolvedParametersAsync() if (result.Data) { // Now we build up a new form base on the unresolved parameters. - var inputs = new Dictionary(); + var inputs = new List(); foreach (var parameter in _unresolvedParameters) { // Create an input for each unresolved parameter. - inputs.Add(parameter, new InteractionInput + inputs.Add(new InteractionInput { InputType = parameter.Secret ? InputType.SecretText : InputType.Text, Label = parameter.Name, @@ -150,7 +150,7 @@ public async Task HandleUnresolvedParametersAsync() var valuesPrompt = await interactionService.PromptInputsAsync( "Set Unresolved Parameters", "Please provide values for the unresolved parameters.", - [.. inputs.Values], + [.. inputs], new InputsDialogInteractionOptions { PrimaryButtonText = "Submit", From 42001b5e61c408e3876b3a26b52095137ee501d2 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 2 Jul 2025 22:29:38 -0700 Subject: [PATCH 03/12] Add unit tests for ParameterProcessor functionality --- .../Orchestrator/ParameterProcessorTests.cs | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs new file mode 100644 index 00000000000..4ab229bfda3 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs @@ -0,0 +1,210 @@ +// 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.Orchestrator; +using Aspire.Hosting.Tests.Utils; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace Aspire.Hosting.Tests.Orchestrator; + +public class ParameterProcessorTests +{ + [Fact] + public async Task InitializeParametersAsync_WithValidParameters_SetsActiveState() + { + // Arrange + var parameterProcessor = CreateParameterProcessor(); + var parameters = new[] + { + CreateParameterResource("param1", "value1"), + CreateParameterResource("param2", "value2") + }; + + // Act + await parameterProcessor.InitializeParametersAsync(parameters); + + // Assert + foreach (var param in parameters) + { + Assert.NotNull(param.WaitForValueTcs); + Assert.True(param.WaitForValueTcs.Task.IsCompletedSuccessfully); + Assert.Equal(param.Value, await param.WaitForValueTcs.Task); + } + } + + [Fact] + public async Task InitializeParametersAsync_WithSecretParameter_MarksAsSecret() + { + // Arrange + var notificationService = ResourceNotificationServiceTestHelpers.Create(); + var parameterProcessor = CreateParameterProcessor(notificationService: notificationService); + var secretParam = CreateParameterResource("secret", "secretValue", secret: true); + + var updates = new List<(IResource Resource, CustomResourceSnapshot Snapshot)>(); + var watchTask = Task.Run(async () => + { + await foreach (var resourceEvent in notificationService.WatchAsync().ConfigureAwait(false)) + { + updates.Add((resourceEvent.Resource, resourceEvent.Snapshot)); + break; // Only collect the first update + } + }); + + // Act + await parameterProcessor.InitializeParametersAsync([secretParam]); + + // Wait for the notification + await watchTask.WaitAsync(TimeSpan.FromSeconds(5)); + + // Assert + Assert.Single(updates); + var (resource, snapshot) = updates[0]; + Assert.Equal(secretParam, resource); + Assert.Equal(KnownResourceStates.Active, snapshot.State?.Text); + Assert.Equal(KnownResourceStateStyles.Success, snapshot.State?.Style); + } + + [Fact] + public async Task InitializeParametersAsync_WithMissingParameterValue_AddsToUnresolvedWhenInteractionAvailable() + { + // Arrange + var interactionService = CreateInteractionService(); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); + + // Act + await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]); + + // Assert + Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); + Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); + } + + [Fact] + public async Task InitializeParametersAsync_WithMissingParameterValue_SetsExceptionWhenInteractionNotAvailable() + { + // Arrange + var interactionService = CreateInteractionService(disableDashboard: true); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); + + // Act + await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]); + + // Assert + Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); + Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); + Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsFaulted); + Assert.IsType(parameterWithMissingValue.WaitForValueTcs.Task.Exception?.InnerException); + } + + [Fact] + public async Task InitializeParametersAsync_WithNonMissingParameterException_SetsException() + { + // Arrange + var parameterProcessor = CreateParameterProcessor(); + var parameterWithError = CreateParameterWithGenericError("errorParam"); + + // Act + await parameterProcessor.InitializeParametersAsync([parameterWithError]); + + // Assert + Assert.NotNull(parameterWithError.WaitForValueTcs); + Assert.True(parameterWithError.WaitForValueTcs.Task.IsCompleted); + Assert.True(parameterWithError.WaitForValueTcs.Task.IsFaulted); + Assert.IsType(parameterWithError.WaitForValueTcs.Task.Exception?.InnerException); + } + + [Fact] + public async Task HandleUnresolvedParametersAsync_WithInteractionService_DoesNotThrow() + { + // Arrange + var interactionService = CreateInteractionService(); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); + + // Act - Initialize parameters first to add to unresolved list + await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]); + + // Allow background task to start + await Task.Delay(50); + + // Assert - The background task should have started without throwing + Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); + // The parameter should remain unresolved since we don't complete the interaction + Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); + + // Verify there's an active interaction for the parameter + var interactions = interactionService.GetCurrentInteractions(); + Assert.NotEmpty(interactions); + } + + [Fact] + public async Task HandleUnresolvedParametersAsync_CallDirectly_DoesNotThrow() + { + // Arrange + var interactionService = CreateInteractionService(); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + + // Act & Assert - Should not throw even when called directly with no unresolved parameters + await parameterProcessor.HandleUnresolvedParametersAsync(); + } + + [Fact] + public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully() + { + // Arrange + var parameterProcessor = CreateParameterProcessor(); + + // Act & Assert - Should not throw + await parameterProcessor.InitializeParametersAsync(Array.Empty()); + } + + private static ParameterProcessor CreateParameterProcessor( + ResourceNotificationService? notificationService = null, + ResourceLoggerService? loggerService = null, + IInteractionService? interactionService = null, + ILogger? logger = null, + bool disableDashboard = true) + { + return new ParameterProcessor( + notificationService ?? ResourceNotificationServiceTestHelpers.Create(), + loggerService ?? new ResourceLoggerService(), + interactionService ?? CreateInteractionService(disableDashboard), + logger ?? new NullLogger() + ); + } + + private static InteractionService CreateInteractionService(bool disableDashboard = false) + { + return new InteractionService( + new NullLogger(), + new DistributedApplicationOptions { DisableDashboard = disableDashboard }, + new ServiceCollection().BuildServiceProvider()); + } + + private static ParameterResource CreateParameterResource(string name, string value, bool secret = false) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { [$"Parameters:{name}"] = value }) + .Build(); + + return new ParameterResource(name, _ => configuration[$"Parameters:{name}"] ?? throw new MissingParameterValueException($"Parameter '{name}' is missing"), secret); + } + + private static ParameterResource CreateParameterWithMissingValue(string name) + { + return new ParameterResource(name, _ => throw new MissingParameterValueException($"Parameter '{name}' is missing"), secret: false); + } + + private static ParameterResource CreateParameterWithGenericError(string name) + { + return new ParameterResource(name, _ => throw new InvalidOperationException($"Generic error for parameter '{name}'"), secret: false); + } +} From c7f95fc301882daf72f46d98822613b23e270e05 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 2 Jul 2025 23:03:08 -0700 Subject: [PATCH 04/12] Refactor HandleUnresolvedParametersAsync for improved testing and interaction handling --- .../Orchestrator/ParameterProcessor.cs | 19 ++- .../Orchestrator/ParameterProcessorTests.cs | 149 +++++++++++++++--- 2 files changed, 143 insertions(+), 25 deletions(-) diff --git a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs index 61e7b59010e..56ee4d55f27 100644 --- a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs +++ b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs @@ -114,10 +114,17 @@ await _notificationService.PublishUpdateAsync(parameterResource, s => } } - public async Task HandleUnresolvedParametersAsync() + // Internal for testing purposes. + private async Task HandleUnresolvedParametersAsync() + { + await HandleUnresolvedParametersAsync(_unresolvedParameters).ConfigureAwait(false); + } + + // Internal for testing purposes - allows passing specific parameters to test. + internal async Task HandleUnresolvedParametersAsync(IList unresolvedParameters) { // This method will continue in a loop until all unresolved parameters are resolved. - while (_unresolvedParameters.Count > 0) + while (unresolvedParameters.Count > 0) { // First we show a notification that there are unresolved parameters. var result = await interactionService.PromptMessageBarAsync( @@ -135,7 +142,7 @@ public async Task HandleUnresolvedParametersAsync() // Now we build up a new form base on the unresolved parameters. var inputs = new List(); - foreach (var parameter in _unresolvedParameters) + foreach (var parameter in unresolvedParameters) { // Create an input for each unresolved parameter. inputs.Add(new InteractionInput @@ -161,9 +168,9 @@ public async Task HandleUnresolvedParametersAsync() if (!valuesPrompt.Canceled) { // Iterate through the unresolved parameters and set their values based on user input. - for (var i = _unresolvedParameters.Count - 1; i >= 0; i--) + for (var i = unresolvedParameters.Count - 1; i >= 0; i--) { - var parameter = _unresolvedParameters[i]; + var parameter = unresolvedParameters[i]; var inputValue = valuesPrompt.Data[i].Value; if (inputValue is null) @@ -186,7 +193,7 @@ await _notificationService.PublishUpdateAsync(parameter, s => .ConfigureAwait(false); // Remove the parameter from unresolved parameters list. - _unresolvedParameters.RemoveAt(i); + unresolvedParameters.RemoveAt(i); } } } diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs index 4ab229bfda3..c8710602530 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs @@ -38,6 +38,30 @@ public async Task InitializeParametersAsync_WithValidParameters_SetsActiveState( } } + [Fact] + public async Task InitializeParametersAsync_WithValidParametersAndDashboardEnabled_SetsActiveState() + { + // Arrange + var interactionService = CreateInteractionService(disableDashboard: false); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService, disableDashboard: false); + var parameters = new[] + { + CreateParameterResource("param1", "value1"), + CreateParameterResource("param2", "value2") + }; + + // Act + await parameterProcessor.InitializeParametersAsync(parameters); + + // Assert + foreach (var param in parameters) + { + Assert.NotNull(param.WaitForValueTcs); + Assert.True(param.WaitForValueTcs.Task.IsCompletedSuccessfully); + Assert.Equal(param.Value, await param.WaitForValueTcs.Task); + } + } + [Fact] public async Task InitializeParametersAsync_WithSecretParameter_MarksAsSecret() { @@ -63,9 +87,8 @@ public async Task InitializeParametersAsync_WithSecretParameter_MarksAsSecret() await watchTask.WaitAsync(TimeSpan.FromSeconds(5)); // Assert - Assert.Single(updates); - var (resource, snapshot) = updates[0]; - Assert.Equal(secretParam, resource); + var (resource, snapshot) = Assert.Single(updates); + Assert.Same(secretParam, resource); Assert.Equal(KnownResourceStates.Active, snapshot.State?.Text); Assert.Equal(KnownResourceStateStyles.Success, snapshot.State?.Style); } @@ -104,6 +127,22 @@ public async Task InitializeParametersAsync_WithMissingParameterValue_SetsExcept Assert.IsType(parameterWithMissingValue.WaitForValueTcs.Task.Exception?.InnerException); } + [Fact] + public async Task InitializeParametersAsync_WithMissingParameterValueAndDashboardEnabled_LeavesUnresolved() + { + // Arrange + var interactionService = CreateInteractionService(disableDashboard: false); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService, disableDashboard: false); + var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); + + // Act + await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]); + + // Assert - Parameter should remain unresolved when dashboard is enabled + Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); + Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); + } + [Fact] public async Task InitializeParametersAsync_WithNonMissingParameterException_SetsException() { @@ -122,48 +161,120 @@ public async Task InitializeParametersAsync_WithNonMissingParameterException_Set } [Fact] - public async Task HandleUnresolvedParametersAsync_WithInteractionService_DoesNotThrow() + public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesInteractions() { // Arrange var interactionService = CreateInteractionService(); var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + var param1 = CreateParameterWithMissingValue("param1"); + var param2 = CreateParameterWithMissingValue("param2"); + var secretParam = CreateParameterWithMissingValue("secretParam"); + + // Act - Initialize parameters to add them to unresolved list + await parameterProcessor.HandleUnresolvedParametersAsync([param1, param2, secretParam]); + + // Assert - All parameters should be unresolved and interaction should be created + Assert.NotNull(param1.WaitForValueTcs); + Assert.NotNull(param2.WaitForValueTcs); + Assert.NotNull(secretParam.WaitForValueTcs); + Assert.False(param1.WaitForValueTcs.Task.IsCompleted); + Assert.False(param2.WaitForValueTcs.Task.IsCompleted); + Assert.False(secretParam.WaitForValueTcs.Task.IsCompleted); + + // Verify there's an active interaction for the parameters + var interactions = interactionService.GetCurrentInteractions(); + Assert.NotEmpty(interactions); + + // Verify the interaction has the expected title + Assert.Contains(interactions, i => i.Title == "Unresolved Parameters"); + } + + [Fact] + public async Task HandleUnresolvedParametersAsync_WithNoInteractionService_DoesNotCreateInteractions() + { + // Arrange - Use interaction service with dashboard disabled + var interactionService = CreateInteractionService(disableDashboard: true); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); - // Act - Initialize parameters first to add to unresolved list + // Act - Initialize parameters await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]); - // Allow background task to start - await Task.Delay(50); + // Allow background task time to run (if it would run) + await Task.Delay(100); - // Assert - The background task should have started without throwing + // Assert - Parameter should be in error state since interaction service is not available Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); - // The parameter should remain unresolved since we don't complete the interaction - Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); + Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); + Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsFaulted); - // Verify there's an active interaction for the parameter + // No interactions should be created var interactions = interactionService.GetCurrentInteractions(); - Assert.NotEmpty(interactions); + Assert.Empty(interactions); } [Fact] - public async Task HandleUnresolvedParametersAsync_CallDirectly_DoesNotThrow() + public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully() + { + // Arrange + var parameterProcessor = CreateParameterProcessor(); + + // Act & Assert - Should not throw + await parameterProcessor.InitializeParametersAsync(Array.Empty()); + } + + [Fact] + public void HandleUnresolvedParametersAsync_WithSingleUnresolvedParameter_CreatesInteraction() { // Arrange var interactionService = CreateInteractionService(); var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + var unresolvedParam = CreateParameterWithMissingValue("testParam"); + + // Initialize the parameter's WaitForValueTcs + unresolvedParam.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Act - Directly call HandleUnresolvedParametersAsync with the parameter list + var handleTask = parameterProcessor.HandleUnresolvedParametersAsync([unresolvedParam]); - // Act & Assert - Should not throw even when called directly with no unresolved parameters - await parameterProcessor.HandleUnresolvedParametersAsync(); + // Assert - Verify interaction was created (the method should start immediately and create interaction) + var interactions = interactionService.GetCurrentInteractions(); + Assert.NotEmpty(interactions); + Assert.Contains(interactions, i => i.Title == "Unresolved Parameters"); + + // Verify the parameter is still unresolved (waiting for user input) + Assert.NotNull(unresolvedParam.WaitForValueTcs); + Assert.False(unresolvedParam.WaitForValueTcs.Task.IsCompleted); } [Fact] - public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully() + public void HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesCorrectInteraction() { // Arrange - var parameterProcessor = CreateParameterProcessor(); + var interactionService = CreateInteractionService(); + var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + var unresolvedParam1 = CreateParameterWithMissingValue("param1"); + var unresolvedParam2 = CreateParameterWithMissingValue("param2"); - // Act & Assert - Should not throw - await parameterProcessor.InitializeParametersAsync(Array.Empty()); + // Initialize the parameters' WaitForValueTcs + unresolvedParam1.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + unresolvedParam2.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Act - Directly call HandleUnresolvedParametersAsync with the parameter list + var handleTask = parameterProcessor.HandleUnresolvedParametersAsync([unresolvedParam1, unresolvedParam2]); + + // Assert - Verify interaction was created for multiple parameters + var interactions = interactionService.GetCurrentInteractions(); + Assert.NotEmpty(interactions); + + var parameterInteraction = interactions.FirstOrDefault(i => i.Title == "Unresolved Parameters"); + Assert.NotNull(parameterInteraction); + + // Verify both parameters are still unresolved (waiting for user input) + Assert.NotNull(unresolvedParam1.WaitForValueTcs); + Assert.NotNull(unresolvedParam2.WaitForValueTcs); + Assert.False(unresolvedParam1.WaitForValueTcs.Task.IsCompleted); + Assert.False(unresolvedParam2.WaitForValueTcs.Task.IsCompleted); } private static ParameterProcessor CreateParameterProcessor( From 03e5755749bff2ab07ea981b2acb2828d601bf0d Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 2 Jul 2025 23:09:33 -0700 Subject: [PATCH 05/12] Refactor HandleUnresolvedParametersAsync test to use array for parameters and improve async handling --- .../Orchestrator/ParameterProcessorTests.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs index c8710602530..549e83f3b85 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs @@ -161,7 +161,7 @@ public async Task InitializeParametersAsync_WithNonMissingParameterException_Set } [Fact] - public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesInteractions() + public void HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesInteractions() { // Arrange var interactionService = CreateInteractionService(); @@ -170,8 +170,16 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete var param2 = CreateParameterWithMissingValue("param2"); var secretParam = CreateParameterWithMissingValue("secretParam"); + ParameterResource[] parameters = [param1, param2, secretParam]; + + foreach (var param in parameters) + { + // Initialize the parameters' WaitForValueTcs + param.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + // Act - Initialize parameters to add them to unresolved list - await parameterProcessor.HandleUnresolvedParametersAsync([param1, param2, secretParam]); + var task = parameterProcessor.HandleUnresolvedParametersAsync(parameters); // Assert - All parameters should be unresolved and interaction should be created Assert.NotNull(param1.WaitForValueTcs); From 4adeb7a896725ac5b309fa0dc74959784c2e2ff9 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 2 Jul 2025 23:53:41 -0700 Subject: [PATCH 06/12] Refactor HandleUnresolvedParametersAsync tests to improve async handling and interaction service usage --- .../Orchestrator/ParameterProcessorTests.cs | 191 ++++++++++-------- 1 file changed, 102 insertions(+), 89 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs index 549e83f3b85..60df3f87a44 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Channels; using Xunit; #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. @@ -161,16 +162,16 @@ public async Task InitializeParametersAsync_WithNonMissingParameterException_Set } [Fact] - public void HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesInteractions() + public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesInteractions() { // Arrange - var interactionService = CreateInteractionService(); - var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); + var testInteractionService = new TestInteractionService(); + var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService); var param1 = CreateParameterWithMissingValue("param1"); var param2 = CreateParameterWithMissingValue("param2"); var secretParam = CreateParameterWithMissingValue("secretParam"); - ParameterResource[] parameters = [param1, param2, secretParam]; + List parameters = [param1, param2, secretParam]; foreach (var param in parameters) { @@ -178,111 +179,80 @@ public void HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_Cre param.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } - // Act - Initialize parameters to add them to unresolved list - var task = parameterProcessor.HandleUnresolvedParametersAsync(parameters); - - // Assert - All parameters should be unresolved and interaction should be created - Assert.NotNull(param1.WaitForValueTcs); - Assert.NotNull(param2.WaitForValueTcs); - Assert.NotNull(secretParam.WaitForValueTcs); - Assert.False(param1.WaitForValueTcs.Task.IsCompleted); - Assert.False(param2.WaitForValueTcs.Task.IsCompleted); - Assert.False(secretParam.WaitForValueTcs.Task.IsCompleted); - - // Verify there's an active interaction for the parameters - var interactions = interactionService.GetCurrentInteractions(); - Assert.NotEmpty(interactions); - - // Verify the interaction has the expected title - Assert.Contains(interactions, i => i.Title == "Unresolved Parameters"); - } + // Act - Start handling unresolved parameters + var handleTask = parameterProcessor.HandleUnresolvedParametersAsync(parameters); - [Fact] - public async Task HandleUnresolvedParametersAsync_WithNoInteractionService_DoesNotCreateInteractions() - { - // Arrange - Use interaction service with dashboard disabled - var interactionService = CreateInteractionService(disableDashboard: true); - var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); - var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); + // Assert - Wait for the first interaction (message bar) + var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); + Assert.Equal("Unresolved Parameters", messageBarInteraction.Title); + Assert.Equal("There are unresolved parameters that need to be set. Please provide values for them.", messageBarInteraction.Message); - // Act - Initialize parameters - await parameterProcessor.InitializeParametersAsync([parameterWithMissingValue]); + // Complete the message bar interaction to proceed to inputs dialog + messageBarInteraction.CompletionTcs.SetResult(new InteractionResult(true, false)); // Data = true (user clicked Enter Values) - // Allow background task time to run (if it would run) - await Task.Delay(100); + // Wait for the inputs interaction + var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); + Assert.Equal("Set Unresolved Parameters", inputsInteraction.Title); + Assert.Equal("Please provide values for the unresolved parameters.", inputsInteraction.Message); - // Assert - Parameter should be in error state since interaction service is not available - Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); - Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); - Assert.True(parameterWithMissingValue.WaitForValueTcs.Task.IsFaulted); - - // No interactions should be created - var interactions = interactionService.GetCurrentInteractions(); - Assert.Empty(interactions); + // Complete the inputs interaction with values + var inputResults = new List + { + new() { InputType = InputType.Text, Label = "param1", Value = "value1" }, + new() { InputType = InputType.Text, Label = "param2", Value = "value2" }, + new() { InputType = InputType.SecretText, Label = "secretParam", Value = "secretValue" } + }; + inputsInteraction.CompletionTcs.SetResult(new InteractionResult>(inputResults, false)); + + // Wait for the handle task to complete + await handleTask; + + // Assert - All parameters should now be resolved + Assert.True(param1.WaitForValueTcs!.Task.IsCompletedSuccessfully); + Assert.True(param2.WaitForValueTcs!.Task.IsCompletedSuccessfully); + Assert.True(secretParam.WaitForValueTcs!.Task.IsCompletedSuccessfully); + Assert.Equal("value1", await param1.WaitForValueTcs.Task); + Assert.Equal("value2", await param2.WaitForValueTcs.Task); + Assert.Equal("secretValue", await secretParam.WaitForValueTcs.Task); } [Fact] - public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully() + public async Task HandleUnresolvedParametersAsync_WhenUserCancelsInteraction_ParametersRemainUnresolved() { // Arrange - var parameterProcessor = CreateParameterProcessor(); + var testInteractionService = new TestInteractionService(); + var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService); + var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); + + parameterWithMissingValue.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Act & Assert - Should not throw - await parameterProcessor.InitializeParametersAsync(Array.Empty()); - } + // Act - Start handling unresolved parameters + _ = parameterProcessor.HandleUnresolvedParametersAsync([parameterWithMissingValue]); - [Fact] - public void HandleUnresolvedParametersAsync_WithSingleUnresolvedParameter_CreatesInteraction() - { - // Arrange - var interactionService = CreateInteractionService(); - var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); - var unresolvedParam = CreateParameterWithMissingValue("testParam"); + // Wait for the message bar interaction + var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); + Assert.Equal("Unresolved Parameters", messageBarInteraction.Title); - // Initialize the parameter's WaitForValueTcs - unresolvedParam.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + // Complete the message bar interaction with false (user chose not to enter values) + messageBarInteraction.CompletionTcs.SetResult(new InteractionResult(false, false)); // Data = false (user dismissed/cancelled) - // Act - Directly call HandleUnresolvedParametersAsync with the parameter list - var handleTask = parameterProcessor.HandleUnresolvedParametersAsync([unresolvedParam]); + // Assert that the message bar will show up again if there are still unresolved parameters + var nextMessageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); + Assert.Equal("Unresolved Parameters", nextMessageBarInteraction.Title); - // Assert - Verify interaction was created (the method should start immediately and create interaction) - var interactions = interactionService.GetCurrentInteractions(); - Assert.NotEmpty(interactions); - Assert.Contains(interactions, i => i.Title == "Unresolved Parameters"); - - // Verify the parameter is still unresolved (waiting for user input) - Assert.NotNull(unresolvedParam.WaitForValueTcs); - Assert.False(unresolvedParam.WaitForValueTcs.Task.IsCompleted); + // Assert - Parameter should remain unresolved since user cancelled + Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); + Assert.False(parameterWithMissingValue.WaitForValueTcs.Task.IsCompleted); } [Fact] - public void HandleUnresolvedParametersAsync_WithMultipleUnresolvedParameters_CreatesCorrectInteraction() + public async Task InitializeParametersAsync_WithEmptyParameterList_CompletesSuccessfully() { // Arrange - var interactionService = CreateInteractionService(); - var parameterProcessor = CreateParameterProcessor(interactionService: interactionService); - var unresolvedParam1 = CreateParameterWithMissingValue("param1"); - var unresolvedParam2 = CreateParameterWithMissingValue("param2"); - - // Initialize the parameters' WaitForValueTcs - unresolvedParam1.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - unresolvedParam2.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - // Act - Directly call HandleUnresolvedParametersAsync with the parameter list - var handleTask = parameterProcessor.HandleUnresolvedParametersAsync([unresolvedParam1, unresolvedParam2]); + var parameterProcessor = CreateParameterProcessor(); - // Assert - Verify interaction was created for multiple parameters - var interactions = interactionService.GetCurrentInteractions(); - Assert.NotEmpty(interactions); - - var parameterInteraction = interactions.FirstOrDefault(i => i.Title == "Unresolved Parameters"); - Assert.NotNull(parameterInteraction); - - // Verify both parameters are still unresolved (waiting for user input) - Assert.NotNull(unresolvedParam1.WaitForValueTcs); - Assert.NotNull(unresolvedParam2.WaitForValueTcs); - Assert.False(unresolvedParam1.WaitForValueTcs.Task.IsCompleted); - Assert.False(unresolvedParam2.WaitForValueTcs.Task.IsCompleted); + // Act & Assert - Should not throw + await parameterProcessor.InitializeParametersAsync([]); } private static ParameterProcessor CreateParameterProcessor( @@ -326,4 +296,47 @@ private static ParameterResource CreateParameterWithGenericError(string name) { return new ParameterResource(name, _ => throw new InvalidOperationException($"Generic error for parameter '{name}'"), secret: false); } + + private sealed record InteractionData(string Title, string? Message, InteractionOptions? Options, TaskCompletionSource CompletionTcs); + + private sealed class TestInteractionService : IInteractionService + { + public Channel Interactions { get; } = Channel.CreateUnbounded(); + + public bool IsAvailable { get; set; } = true; + + public Task> PromptConfirmationAsync(string title, string message, MessageBoxInteractionOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task> PromptInputAsync(string title, string? message, string inputLabel, string placeHolder, InputsDialogInteractionOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task> PromptInputAsync(string title, string? message, InteractionInput input, InputsDialogInteractionOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public async Task>> PromptInputsAsync(string title, string? message, IReadOnlyList inputs, InputsDialogInteractionOptions? options = null, CancellationToken cancellationToken = default) + { + var data = new InteractionData(title, message, options, new TaskCompletionSource()); + Interactions.Writer.TryWrite(data); + return (InteractionResult>)await data.CompletionTcs.Task; + } + + public async Task> PromptMessageBarAsync(string title, string message, MessageBarInteractionOptions? options = null, CancellationToken cancellationToken = default) + { + var data = new InteractionData(title, message, options, new TaskCompletionSource()); + Interactions.Writer.TryWrite(data); + return (InteractionResult)await data.CompletionTcs.Task; + } + + public Task> PromptMessageBoxAsync(string title, string message, MessageBoxInteractionOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } } From cfdc3860150c54b3ab2a0863605b1fc7532ad34a Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 3 Jul 2025 00:08:58 -0700 Subject: [PATCH 07/12] Refactor HandleUnresolvedParametersAsync test to enhance interaction handling and parameter processing --- .../Orchestrator/ParameterProcessorTests.cs | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs index 60df3f87a44..368e0cfa31a 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Dashboard.Model; using Aspire.Hosting.Orchestrator; using Aspire.Hosting.Tests.Utils; using Microsoft.Extensions.Configuration; @@ -166,10 +167,11 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete { // Arrange var testInteractionService = new TestInteractionService(); - var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService); + var notificationService = ResourceNotificationServiceTestHelpers.Create(); + var parameterProcessor = CreateParameterProcessor(notificationService: notificationService, interactionService: testInteractionService); var param1 = CreateParameterWithMissingValue("param1"); var param2 = CreateParameterWithMissingValue("param2"); - var secretParam = CreateParameterWithMissingValue("secretParam"); + var secretParam = CreateParameterWithMissingValue("secretParam", secret: true); List parameters = [param1, param2, secretParam]; @@ -179,6 +181,8 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete param.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } + var updates = notificationService.WatchAsync().GetAsyncEnumerator(); + // Act - Start handling unresolved parameters var handleTask = parameterProcessor.HandleUnresolvedParametersAsync(parameters); @@ -195,14 +199,28 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete Assert.Equal("Set Unresolved Parameters", inputsInteraction.Title); Assert.Equal("Please provide values for the unresolved parameters.", inputsInteraction.Message); - // Complete the inputs interaction with values - var inputResults = new List - { - new() { InputType = InputType.Text, Label = "param1", Value = "value1" }, - new() { InputType = InputType.Text, Label = "param2", Value = "value2" }, - new() { InputType = InputType.SecretText, Label = "secretParam", Value = "secretValue" } - }; - inputsInteraction.CompletionTcs.SetResult(new InteractionResult>(inputResults, false)); + Assert.Collection(inputsInteraction.Inputs, + input => + { + Assert.Equal("param1", input.Label); + Assert.Equal(InputType.Text, input.InputType); + }, + input => + { + Assert.Equal("param2", input.Label); + Assert.Equal(InputType.Text, input.InputType); + }, + input => + { + Assert.Equal("secretParam", input.Label); + Assert.Equal(InputType.SecretText, input.InputType); + }); + + inputsInteraction.Inputs[0].SetValue("value1"); + inputsInteraction.Inputs[1].SetValue("value2"); + inputsInteraction.Inputs[2].SetValue("secretValue"); + + inputsInteraction.CompletionTcs.SetResult(new InteractionResult>(inputsInteraction.Inputs, false)); // Wait for the handle task to complete await handleTask; @@ -214,6 +232,21 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete Assert.Equal("value1", await param1.WaitForValueTcs.Task); Assert.Equal("value2", await param2.WaitForValueTcs.Task); Assert.Equal("secretValue", await secretParam.WaitForValueTcs.Task); + + // Notification service should have received updates for each parameter + // Marking them as Active with the provided values + await updates.MoveNextAsync(); + Assert.Equal(KnownResourceStates.Active, updates.Current.Snapshot.State?.Text); + Assert.Equal("value1", updates.Current.Snapshot.Properties.FirstOrDefault(p => p.Name == KnownProperties.Parameter.Value)?.Value); + + await updates.MoveNextAsync(); + Assert.Equal(KnownResourceStates.Active, updates.Current.Snapshot.State?.Text); + Assert.Equal("value2", updates.Current.Snapshot.Properties.FirstOrDefault(p => p.Name == KnownProperties.Parameter.Value)?.Value); + + await updates.MoveNextAsync(); + Assert.Equal(KnownResourceStates.Active, updates.Current.Snapshot.State?.Text); + Assert.Equal("secretValue", updates.Current.Snapshot.Properties.FirstOrDefault(p => p.Name == KnownProperties.Parameter.Value)?.Value); + Assert.True(updates.Current.Snapshot.Properties.FirstOrDefault(p => p.Name == KnownProperties.Parameter.Value)?.IsSensitive ?? false); } [Fact] @@ -223,7 +256,7 @@ public async Task HandleUnresolvedParametersAsync_WhenUserCancelsInteraction_Par var testInteractionService = new TestInteractionService(); var parameterProcessor = CreateParameterProcessor(interactionService: testInteractionService); var parameterWithMissingValue = CreateParameterWithMissingValue("missingParam"); - + parameterWithMissingValue.WaitForValueTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); // Act - Start handling unresolved parameters @@ -287,9 +320,9 @@ private static ParameterResource CreateParameterResource(string name, string val return new ParameterResource(name, _ => configuration[$"Parameters:{name}"] ?? throw new MissingParameterValueException($"Parameter '{name}' is missing"), secret); } - private static ParameterResource CreateParameterWithMissingValue(string name) + private static ParameterResource CreateParameterWithMissingValue(string name, bool secret = false) { - return new ParameterResource(name, _ => throw new MissingParameterValueException($"Parameter '{name}' is missing"), secret: false); + return new ParameterResource(name, _ => throw new MissingParameterValueException($"Parameter '{name}' is missing"), secret: secret); } private static ParameterResource CreateParameterWithGenericError(string name) @@ -297,7 +330,7 @@ private static ParameterResource CreateParameterWithGenericError(string name) return new ParameterResource(name, _ => throw new InvalidOperationException($"Generic error for parameter '{name}'"), secret: false); } - private sealed record InteractionData(string Title, string? Message, InteractionOptions? Options, TaskCompletionSource CompletionTcs); + private sealed record InteractionData(string Title, string? Message, IReadOnlyList Inputs, InteractionOptions? Options, TaskCompletionSource CompletionTcs); private sealed class TestInteractionService : IInteractionService { @@ -322,14 +355,14 @@ public Task> PromptInputAsync(string title, public async Task>> PromptInputsAsync(string title, string? message, IReadOnlyList inputs, InputsDialogInteractionOptions? options = null, CancellationToken cancellationToken = default) { - var data = new InteractionData(title, message, options, new TaskCompletionSource()); + var data = new InteractionData(title, message, inputs, options, new TaskCompletionSource()); Interactions.Writer.TryWrite(data); return (InteractionResult>)await data.CompletionTcs.Task; } public async Task> PromptMessageBarAsync(string title, string message, MessageBarInteractionOptions? options = null, CancellationToken cancellationToken = default) { - var data = new InteractionData(title, message, options, new TaskCompletionSource()); + var data = new InteractionData(title, message, [], options, new TaskCompletionSource()); Interactions.Writer.TryWrite(data); return (InteractionResult)await data.CompletionTcs.Task; } From a44dfae2cd738e2bcf50bc70be4d27e6e37dbfcc Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 3 Jul 2025 00:13:19 -0700 Subject: [PATCH 08/12] Refactor exception handling in environment variable tests to use MissingParameterValueException --- tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs | 2 +- tests/Aspire.Hosting.Tests/WithReferenceTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index 659f8c41df0..00ed812bb34 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -161,7 +161,7 @@ public async Task EnvironmentCallbackThrowsWhenParameterValueMissingInDcpMode() var projectA = builder.AddProject("projectA") .WithEnvironment("MY_PARAMETER", parameter); - var exception = await Assert.ThrowsAsync(async () => await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync( + var exception = await Assert.ThrowsAsync(async () => await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync( projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance diff --git a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs index 7de1122c3e6..9d24cd3f647 100644 --- a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs @@ -191,7 +191,7 @@ public async Task ParameterAsConnectionStringResourceThrowsWhenConnectionStringS .WithReference(missingResource); // Call environment variable callbacks. - var exception = await Assert.ThrowsAsync(async () => + var exception = await Assert.ThrowsAsync(async () => { var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); }).DefaultTimeout(); From 8954f937d0f239f58e00927ec754390dbe1ec856 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 3 Jul 2025 00:23:07 -0700 Subject: [PATCH 09/12] Add MissingParameterValueException for handling missing parameter values --- .../{ApplicationModel => }/MissingParameterValueException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Aspire.Hosting/{ApplicationModel => }/MissingParameterValueException.cs (97%) diff --git a/src/Aspire.Hosting/ApplicationModel/MissingParameterValueException.cs b/src/Aspire.Hosting/MissingParameterValueException.cs similarity index 97% rename from src/Aspire.Hosting/ApplicationModel/MissingParameterValueException.cs rename to src/Aspire.Hosting/MissingParameterValueException.cs index a32cf737b0c..2f02f9d9cce 100644 --- a/src/Aspire.Hosting/ApplicationModel/MissingParameterValueException.cs +++ b/src/Aspire.Hosting/MissingParameterValueException.cs @@ -1,7 +1,7 @@ // 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; +namespace Aspire.Hosting; /// /// The exception that is thrown when a parameter resource cannot be initialized because its value is missing or cannot be resolved. From a6d7c4c89e6df7876afffb8eb901bd62a9dfe34e Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 3 Jul 2025 00:54:24 -0700 Subject: [PATCH 10/12] PR feedback --- .../Orchestrator/ParameterProcessor.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs index 56ee4d55f27..220d9fdc30c 100644 --- a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs +++ b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs @@ -18,8 +18,6 @@ internal sealed class ParameterProcessor( IInteractionService interactionService, ILogger logger) { - private readonly ResourceNotificationService _notificationService = notificationService; - private readonly ResourceLoggerService _loggerService = loggerService; private readonly List _unresolvedParameters = []; public async Task InitializeParametersAsync(IEnumerable parameterResources) @@ -51,7 +49,7 @@ public async Task InitializeParametersAsync(IEnumerable param } catch (Exception ex) { - logger.LogError(ex, "Failed to handle unresolved parameters"); + logger.LogError(ex, "Failed to handle unresolved parameters."); } }); } @@ -64,7 +62,7 @@ private async Task ProcessParameterAsync(ParameterResource parameterResource) { var value = parameterResource.Value ?? ""; - await _notificationService.PublishUpdateAsync(parameterResource, s => + await notificationService.PublishUpdateAsync(parameterResource, s => { return s with { @@ -74,7 +72,7 @@ await _notificationService.PublishUpdateAsync(parameterResource, s => }) .ConfigureAwait(false); - parameterResource.WaitForValueTcs?.SetResult(value); + parameterResource.WaitForValueTcs?.TrySetResult(value); } catch (Exception ex) { @@ -85,23 +83,23 @@ await _notificationService.PublishUpdateAsync(parameterResource, s => // Add the parameter to unresolved parameters list. _unresolvedParameters.Add(parameterResource); - _loggerService.GetLogger(parameterResource) + loggerService.GetLogger(parameterResource) .LogWarning(ex, "Parameter resource {ResourceName} could not be initialized. Waiting for user input.", parameterResource.Name); } else { // If interaction service is not available, we log the error and set the state to error. - parameterResource.WaitForValueTcs?.SetException(ex); + parameterResource.WaitForValueTcs?.TrySetException(ex); - _loggerService.GetLogger(parameterResource) - .LogError(ex, "Failed to initialize parameter resource {ResourceName}", parameterResource.Name); + loggerService.GetLogger(parameterResource) + .LogError(ex, "Failed to initialize parameter resource {ResourceName}.", parameterResource.Name); } var stateText = ex is MissingParameterValueException ? "Value missing" : "Error initializing parameter"; - await _notificationService.PublishUpdateAsync(parameterResource, s => + await notificationService.PublishUpdateAsync(parameterResource, s => { return s with { @@ -128,12 +126,12 @@ internal async Task HandleUnresolvedParametersAsync(IList unr { // First we show a notification that there are unresolved parameters. var result = await interactionService.PromptMessageBarAsync( - "Unresolved Parameters", + "Unresolved parameters", "There are unresolved parameters that need to be set. Please provide values for them.", new MessageBarInteractionOptions { Intent = MessageIntent.Warning, - PrimaryButtonText = "Enter Values" + PrimaryButtonText = "Enter values" }) .ConfigureAwait(false); @@ -155,9 +153,9 @@ internal async Task HandleUnresolvedParametersAsync(IList unr } var valuesPrompt = await interactionService.PromptInputsAsync( - "Set Unresolved Parameters", + "Set unresolved parameters", "Please provide values for the unresolved parameters.", - [.. inputs], + inputs, new InputsDialogInteractionOptions { PrimaryButtonText = "Submit", @@ -182,7 +180,7 @@ internal async Task HandleUnresolvedParametersAsync(IList unr parameter.WaitForValueTcs?.TrySetResult(inputValue); // Update the parameter resource state to active with the provided value. - await _notificationService.PublishUpdateAsync(parameter, s => + await notificationService.PublishUpdateAsync(parameter, s => { return s with { From 19e90b67e3e8a95cc2eaeb29c4bf697192fba210 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 3 Jul 2025 01:03:20 -0700 Subject: [PATCH 11/12] Fix capitalization in interaction titles for unresolved parameters --- .../Orchestrator/ParameterProcessorTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs index 368e0cfa31a..38a07b88cb5 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs @@ -188,7 +188,7 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete // Assert - Wait for the first interaction (message bar) var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); - Assert.Equal("Unresolved Parameters", messageBarInteraction.Title); + Assert.Equal("Unresolved parameters", messageBarInteraction.Title); Assert.Equal("There are unresolved parameters that need to be set. Please provide values for them.", messageBarInteraction.Message); // Complete the message bar interaction to proceed to inputs dialog @@ -196,7 +196,7 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete // Wait for the inputs interaction var inputsInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); - Assert.Equal("Set Unresolved Parameters", inputsInteraction.Title); + Assert.Equal("Set unresolved parameters", inputsInteraction.Title); Assert.Equal("Please provide values for the unresolved parameters.", inputsInteraction.Message); Assert.Collection(inputsInteraction.Inputs, @@ -264,14 +264,14 @@ public async Task HandleUnresolvedParametersAsync_WhenUserCancelsInteraction_Par // Wait for the message bar interaction var messageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); - Assert.Equal("Unresolved Parameters", messageBarInteraction.Title); + Assert.Equal("Unresolved parameters", messageBarInteraction.Title); // Complete the message bar interaction with false (user chose not to enter values) messageBarInteraction.CompletionTcs.SetResult(new InteractionResult(false, false)); // Data = false (user dismissed/cancelled) // Assert that the message bar will show up again if there are still unresolved parameters var nextMessageBarInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); - Assert.Equal("Unresolved Parameters", nextMessageBarInteraction.Title); + Assert.Equal("Unresolved parameters", nextMessageBarInteraction.Title); // Assert - Parameter should remain unresolved since user cancelled Assert.NotNull(parameterWithMissingValue.WaitForValueTcs); From 9ad2e0ddf70f6ce9281b5ffdde551cfc466c67c6 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 3 Jul 2025 02:00:04 -0700 Subject: [PATCH 12/12] Update HandleUnresolvedParametersAsync to set Required property to false and change PrimaryButtonText to "Save" --- src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs | 5 ++--- .../Orchestrator/ParameterProcessorTests.cs | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs index 220d9fdc30c..f44b1d65fc0 100644 --- a/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs +++ b/src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs @@ -148,7 +148,6 @@ internal async Task HandleUnresolvedParametersAsync(IList unr InputType = parameter.Secret ? InputType.SecretText : InputType.Text, Label = parameter.Name, Placeholder = "Enter value for " + parameter.Name, - Required = true, }); } @@ -158,7 +157,7 @@ internal async Task HandleUnresolvedParametersAsync(IList unr inputs, new InputsDialogInteractionOptions { - PrimaryButtonText = "Submit", + PrimaryButtonText = "Save", ShowDismiss = true }) .ConfigureAwait(false); @@ -171,7 +170,7 @@ internal async Task HandleUnresolvedParametersAsync(IList unr var parameter = unresolvedParameters[i]; var inputValue = valuesPrompt.Data[i].Value; - if (inputValue is null) + if (string.IsNullOrEmpty(inputValue)) { // If the input value is null, we skip this parameter. continue; diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs index 38a07b88cb5..a6a5e651f50 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ParameterProcessorTests.cs @@ -204,16 +204,19 @@ public async Task HandleUnresolvedParametersAsync_WithMultipleUnresolvedParamete { Assert.Equal("param1", input.Label); Assert.Equal(InputType.Text, input.InputType); + Assert.False(input.Required); }, input => { Assert.Equal("param2", input.Label); Assert.Equal(InputType.Text, input.InputType); + Assert.False(input.Required); }, input => { Assert.Equal("secretParam", input.Label); Assert.Equal(InputType.SecretText, input.InputType); + Assert.False(input.Required); }); inputsInteraction.Inputs[0].SetValue("value1");