From 85e3eb9e3881698b2b4135d8e338ceb47f81dc28 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 08:54:27 -0700 Subject: [PATCH 01/20] Expose IInteractionService to polyglot app hosts Add an ATS-first polyglot surface for the interaction service so app hosts in TypeScript, Python, Go, Rust, and Java can prompt users from command callbacks. - Expose ExecuteCommandContext.ServiceProvider (serviceProvider()) so command callbacks can resolve services, including the new getInteractionService(). - Register IInteractionService as an ATS handle and add InteractionExports with prompt methods, input factories (as IInteractionService extensions so the scanner keeps the input name and emits them as service methods), an InteractionInputBuilder handle, and a curated InteractionInputLoadContext for dynamic loading that never exposes the raw IServiceProvider. - Model inputs via a builder/handle instead of a DTO because input options can carry non-serializable callbacks (dynamic loading). - Add a 'pick-zone' withCommand test bench (dynamically loaded choice input) to the polyglot apphosts and regenerate the codegen snapshots for all languages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ResourceCommandAnnotation.cs | 5 +- src/Aspire.Hosting/Ats/AtsTypeMappings.cs | 2 + src/Aspire.Hosting/Ats/InteractionExports.cs | 673 ++++++++++ src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 73 ++ ...TwoPassScanningGeneratedAspire.verified.go | 826 ++++++++++++ ...oPassScanningGeneratedAspire.verified.java | 1165 +++++++++++++++-- ...TwoPassScanningGeneratedAspire.verified.py | 340 +++++ ...TwoPassScanningGeneratedAspire.verified.rs | 604 +++++++++ ...TwoPassScanningGeneratedAspire.verified.ts | 950 ++++++++++++++ .../Aspire.Hosting/Go/apphost.go | 36 + .../Aspire.Hosting/Java/AppHost.java | 35 + .../Aspire.Hosting/Python/apphost.py | 31 + .../Aspire.Hosting/TypeScript/apphost.mts | 34 + 13 files changed, 4693 insertions(+), 81 deletions(-) create mode 100644 src/Aspire.Hosting/Ats/InteractionExports.cs diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs index 3560a24daff..52f7f7b985a 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs @@ -432,7 +432,10 @@ public sealed class ExecuteCommandContext /// /// The service provider. /// - [AspireExportIgnore(Reason = "IServiceProvider is not usable from polyglot command callbacks.")] + /// + /// Polyglot command callbacks use this handle to resolve app host services, such as the interaction service via + /// serviceProvider().getInteractionService(), so they can prompt the user while the command executes. + /// public required IServiceProvider ServiceProvider { get; init; } /// diff --git a/src/Aspire.Hosting/Ats/AtsTypeMappings.cs b/src/Aspire.Hosting/Ats/AtsTypeMappings.cs index 00882748bfb..4d75a392f07 100644 --- a/src/Aspire.Hosting/Ats/AtsTypeMappings.cs +++ b/src/Aspire.Hosting/Ats/AtsTypeMappings.cs @@ -1,4 +1,5 @@ #pragma warning disable ASPIREPIPELINES001 +#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. @@ -56,6 +57,7 @@ [assembly: AspireExport(typeof(ResourceNotificationService))] [assembly: AspireExport(typeof(ResourceLoggerService))] [assembly: AspireExport(typeof(ResourceCommandService))] +[assembly: AspireExport(typeof(IInteractionService))] // Additional framework and hosting types we reference [assembly: AspireExport(typeof(IConfiguration))] diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs new file mode 100644 index 00000000000..7c01dc2c737 --- /dev/null +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -0,0 +1,673 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting.Ats; + +#pragma warning disable ASPIREINTERACTION001 // IInteractionService and related types are experimental. + +/// +/// ATS exports for the interaction service. +/// +/// +/// +/// The interaction service surface is tailored for polyglot app hosts rather than exposed directly. The shipped +/// .NET API models inputs with delegate-bearing option types (for example ) +/// that cannot be serialized as ATS DTOs. Instead, polyglot callers build inputs through factory capabilities that +/// return the opaque handle, attach behavior such as dynamic loading via +/// callbacks on that handle, and then pass the handles to the prompt capabilities. +/// +/// +internal static class InteractionExports +{ + /// + /// Gets the interaction service from the service provider. + /// + /// The service provider handle. + /// An interaction service handle. + [AspireExport] + public static IInteractionService GetInteractionService(this IServiceProvider serviceProvider) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + + return serviceProvider.GetRequiredService(); + } + + /// + /// Gets a value indicating whether the interaction service is available to prompt the user. + /// + /// The interaction service handle. + /// when the service can prompt the user; otherwise . + [AspireExport] + public static bool IsAvailable(this IInteractionService interactionService) + { + ArgumentNullException.ThrowIfNull(interactionService); + + return interactionService.IsAvailable; + } + + /// + /// Prompts the user for confirmation with an OK/Cancel dialog. + /// + [AspireExport] + public static async Task PromptConfirmation( + this IInteractionService interactionService, + string title, + string message, + InteractionMessageBoxOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(interactionService); + + var result = await interactionService.PromptConfirmationAsync(title, message, options?.ToOptions(), cancellationToken).ConfigureAwait(false); + return BoolInteractionResult.From(result); + } + + /// + /// Prompts the user with a message box dialog. + /// + [AspireExport] + public static async Task PromptMessageBox( + this IInteractionService interactionService, + string title, + string message, + InteractionMessageBoxOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(interactionService); + + var result = await interactionService.PromptMessageBoxAsync(title, message, options?.ToOptions(), cancellationToken).ConfigureAwait(false); + return BoolInteractionResult.From(result); + } + + /// + /// Prompts the user with a notification. + /// + [AspireExport] + public static async Task PromptNotification( + this IInteractionService interactionService, + string title, + string message, + InteractionNotificationOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(interactionService); + + var result = await interactionService.PromptNotificationAsync(title, message, options?.ToOptions(), cancellationToken).ConfigureAwait(false); + return BoolInteractionResult.From(result); + } + + /// + /// Prompts the user for a single input. + /// + // Prompts can invoke dynamic-loading callbacks that re-enter the remote host through ATS, so the synchronous + // invocation path must run on a background thread to keep the JSON-RPC loop processing nested callbacks. + [AspireExport(RunSyncOnBackgroundThread = true)] + public static async Task PromptInput( + this IInteractionService interactionService, + string title, + string? message, + InteractionInputBuilder input, + InteractionInputsDialogOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(interactionService); + ArgumentNullException.ThrowIfNull(input); + + var result = await interactionService.PromptInputAsync(title, message, input.Input, options?.ToOptions(), cancellationToken).ConfigureAwait(false); + return InputInteractionResult.From(result); + } + + /// + /// Prompts the user for multiple inputs. + /// + [AspireExport(RunSyncOnBackgroundThread = true)] + public static async Task PromptInputs( + this IInteractionService interactionService, + string title, + string? message, + InteractionInputBuilder[] inputs, + InteractionInputsDialogOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(interactionService); + ArgumentNullException.ThrowIfNull(inputs); + + var interactionInputs = new InteractionInput[inputs.Length]; + for (var i = 0; i < inputs.Length; i++) + { + interactionInputs[i] = inputs[i].Input; + } + + var result = await interactionService.PromptInputsAsync(title, message, interactionInputs, options?.ToOptions(), cancellationToken).ConfigureAwait(false); + return InputsInteractionResult.From(result); + } + + // The input factories hang off IInteractionService so the ATS scanner treats the service handle as the + // receiver (polyglot: interactionService.createTextInput(...)). The receiver itself is unused because inputs + // are independent of the service, so suppress the unused-parameter analyzer for the factory block. +#pragma warning disable IDE0060 // Remove unused parameter + /// + /// Creates a single-line text input. + /// + [AspireExport] + public static InteractionInputBuilder CreateTextInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null) + { + return InteractionInputBuilder.Create(name, InputType.Text, options); + } + + /// + /// Creates a secret (masked) text input. + /// + [AspireExport] + public static InteractionInputBuilder CreateSecretInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null) + { + return InteractionInputBuilder.Create(name, InputType.SecretText, options); + } + + /// + /// Creates a boolean (checkbox) input. + /// + [AspireExport] + public static InteractionInputBuilder CreateBooleanInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null) + { + return InteractionInputBuilder.Create(name, InputType.Boolean, options); + } + + /// + /// Creates a numeric input. + /// + [AspireExport] + public static InteractionInputBuilder CreateNumberInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null) + { + return InteractionInputBuilder.Create(name, InputType.Number, options); + } + + /// + /// Creates a choice input that selects from a list of options. + /// + /// The interaction service. + /// The name of the input. + /// The available choices, keyed by submitted value. + /// Optional configuration for the input. + [AspireExport] + public static InteractionInputBuilder CreateChoiceInput(this IInteractionService interactionService, string name, IReadOnlyDictionary? choices = null, CreateInteractionInputOptions? options = null) + { + var builder = InteractionInputBuilder.Create(name, InputType.Choice, options); + if (choices is { Count: > 0 }) + { + builder.Input.Options = ToOptionList(choices); + } + + return builder; + } +#pragma warning restore IDE0060 // Remove unused parameter + + internal static IReadOnlyList> ToOptionList(IReadOnlyDictionary choices) + { + var list = new List>(choices.Count); + foreach (var choice in choices) + { + list.Add(KeyValuePair.Create(choice.Key, choice.Value)); + } + + return list; + } +} + +/// +/// An opaque, server-side builder for an used by polyglot app hosts. +/// +/// +/// The builder owns the live instance. Dynamic-loading callbacks mutate this same +/// instance through , which is why the input is modeled as a handle here +/// instead of the by-value InteractionInput DTO. +/// +[AspireExport] +internal sealed class InteractionInputBuilder +{ + private InteractionInputBuilder(InteractionInput input) + { + Input = input; + } + + internal InteractionInput Input { get; } + + internal static InteractionInputBuilder Create(string name, InputType inputType, CreateInteractionInputOptions? options) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + var input = new InteractionInput + { + Name = name, + InputType = inputType, + Label = options?.Label, + Description = options?.Description, + EnableDescriptionMarkdown = options?.EnableDescriptionMarkdown ?? false, + Required = options?.Required ?? false, + Placeholder = options?.Placeholder, + Value = options?.Value, + AllowCustomChoice = options?.AllowCustomChoice ?? false, + Disabled = options?.Disabled ?? false, + MaxLength = options?.MaxLength, + }; + + return new InteractionInputBuilder(input); + } + + /// + /// Sets the choice options for the input. + /// + /// The available choices, keyed by submitted value. + /// The same builder handle. + [AspireExport] + public InteractionInputBuilder WithChoiceOptions(IReadOnlyDictionary choices) + { + ArgumentNullException.ThrowIfNull(choices); + + Input.Options = InteractionExports.ToOptionList(choices); + return this; + } + + /// + /// Sets the value of the input. + /// + /// The value to assign. + /// The same builder handle. + [AspireExport] + public InteractionInputBuilder WithValue(string? value) + { + Input.Value = value; + return this; + } + + /// + /// Attaches a callback that dynamically loads or updates the input after the prompt starts. + /// + /// The callback invoked to load the input. Use the supplied context to read other inputs and update this input. + /// Optional configuration that controls when the callback runs. + /// The same builder handle. + [AspireExport] + public InteractionInputBuilder WithDynamicLoading(Func callback, DynamicLoadingOptions? options = null) + { + ArgumentNullException.ThrowIfNull(callback); + + // Bridge the engine's LoadInputContext to the curated polyglot context so callbacks never see the raw + // IServiceProvider and can only mutate the live input through guarded setters. + Input.SetDynamicLoading(new InputLoadOptions + { + LoadCallback = loadContext => callback(new InteractionInputLoadContext(loadContext)), + AlwaysLoadOnStart = options?.AlwaysLoadOnStart ?? false, + DependsOnInputs = options?.DependsOnInputs, + }); + + return this; + } +} + +/// +/// The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the +/// prompt, and provides guarded setters to update the loading input. +/// +[AspireExport] +internal sealed class InteractionInputLoadContext +{ + private readonly LoadInputContext _inner; + + internal InteractionInputLoadContext(LoadInputContext inner) + { + _inner = inner; + } + + /// + /// Gets the name of the input that is loading. + /// + /// The input name. + [AspireExport] + public string GetInputName() + { + return _inner.Input.Name; + } + + /// + /// Gets the current value of an input in the prompt by name. + /// + /// The name of the input to read. + /// The input value, or when the input has no value or does not exist. + [AspireExport] + public string? GetInputValue(string inputName) + { + ArgumentNullException.ThrowIfNull(inputName); + + return _inner.AllInputs.TryGetByName(inputName, out var input) ? input.Value : null; + } + + /// + /// Sets the choice options for the loading input. + /// + /// The available choices, keyed by submitted value. + [AspireExport] + public void SetChoiceOptions(IReadOnlyDictionary choices) + { + ArgumentNullException.ThrowIfNull(choices); + + // Honor cancellation so a stale load that was superseded by a newer one does not overwrite the input. + _inner.CancellationToken.ThrowIfCancellationRequested(); + _inner.Input.Options = InteractionExports.ToOptionList(choices); + } + + /// + /// Sets the value of the loading input. + /// + /// The value to assign. + [AspireExport] + public void SetValue(string? value) + { + _inner.CancellationToken.ThrowIfCancellationRequested(); + _inner.Input.Value = value; + } +} + +/// +/// Optional configuration shared by interaction input factory capabilities. +/// +[AspireDto] +internal sealed class CreateInteractionInputOptions +{ + /// + /// Gets or sets the label for the input. Defaults to the input name when not specified. + /// + public string? Label { get; set; } + + /// + /// Gets or sets the description for the input. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether the description is rendered as Markdown. + /// + public bool? EnableDescriptionMarkdown { get; set; } + + /// + /// Gets or sets a value indicating whether the input is required. + /// + public bool? Required { get; set; } + + /// + /// Gets or sets the placeholder text for the input. + /// + public string? Placeholder { get; set; } + + /// + /// Gets or sets the initial value of the input. + /// + public string? Value { get; set; } + + /// + /// Gets or sets a value indicating whether a custom choice is allowed. Only used by choice inputs. + /// + public bool? AllowCustomChoice { get; set; } + + /// + /// Gets or sets a value indicating whether the input is disabled. + /// + public bool? Disabled { get; set; } + + /// + /// Gets or sets the maximum length for text inputs. + /// + public int? MaxLength { get; set; } +} + +/// +/// Options controlling when a dynamic-loading callback runs. +/// +[AspireDto] +internal sealed class DynamicLoadingOptions +{ + /// + /// Gets or sets a value indicating whether the callback always runs at the start of the prompt. + /// + public bool? AlwaysLoadOnStart { get; set; } + + /// + /// Gets or sets the names of inputs this input depends on. The callback runs when any of them change. + /// + public IReadOnlyList? DependsOnInputs { get; set; } +} + +/// +/// Options for message box and confirmation prompts. +/// +[AspireDto] +internal sealed class InteractionMessageBoxOptions +{ + /// + /// Gets or sets the primary button text. + /// + public string? PrimaryButtonText { get; set; } + + /// + /// Gets or sets the secondary button text. + /// + public string? SecondaryButtonText { get; set; } + + /// + /// Gets or sets a value indicating whether the secondary button is shown. + /// + public bool? ShowSecondaryButton { get; set; } + + /// + /// Gets or sets a value indicating whether the dismiss button is shown. + /// + public bool? ShowDismiss { get; set; } + + /// + /// Gets or sets a value indicating whether Markdown in the message is rendered. + /// + public bool? EnableMessageMarkdown { get; set; } + + /// + /// Gets or sets the intent of the message box. + /// + public MessageIntent? Intent { get; set; } + + internal MessageBoxInteractionOptions ToOptions() + { + return new MessageBoxInteractionOptions + { + PrimaryButtonText = PrimaryButtonText, + SecondaryButtonText = SecondaryButtonText, + ShowSecondaryButton = ShowSecondaryButton, + ShowDismiss = ShowDismiss, + EnableMessageMarkdown = EnableMessageMarkdown, + Intent = Intent, + }; + } +} + +/// +/// Options for notification prompts. +/// +[AspireDto] +internal sealed class InteractionNotificationOptions +{ + /// + /// Gets or sets the primary button text. + /// + public string? PrimaryButtonText { get; set; } + + /// + /// Gets or sets the secondary button text. + /// + public string? SecondaryButtonText { get; set; } + + /// + /// Gets or sets a value indicating whether the secondary button is shown. + /// + public bool? ShowSecondaryButton { get; set; } + + /// + /// Gets or sets a value indicating whether the dismiss button is shown. + /// + public bool? ShowDismiss { get; set; } + + /// + /// Gets or sets a value indicating whether Markdown in the message is rendered. + /// + public bool? EnableMessageMarkdown { get; set; } + + /// + /// Gets or sets the intent of the notification. + /// + public MessageIntent? Intent { get; set; } + + /// + /// Gets or sets the text for a link in the notification. + /// + public string? LinkText { get; set; } + + /// + /// Gets or sets the URL for the link in the notification. + /// + public string? LinkUrl { get; set; } + + internal NotificationInteractionOptions ToOptions() + { + return new NotificationInteractionOptions + { + PrimaryButtonText = PrimaryButtonText, + SecondaryButtonText = SecondaryButtonText, + ShowSecondaryButton = ShowSecondaryButton, + ShowDismiss = ShowDismiss, + EnableMessageMarkdown = EnableMessageMarkdown, + Intent = Intent, + LinkText = LinkText, + LinkUrl = LinkUrl, + }; + } +} + +/// +/// Options for inputs dialog prompts. +/// +[AspireDto] +internal sealed class InteractionInputsDialogOptions +{ + /// + /// Gets or sets the primary button text. + /// + public string? PrimaryButtonText { get; set; } + + /// + /// Gets or sets the secondary button text. + /// + public string? SecondaryButtonText { get; set; } + + /// + /// Gets or sets a value indicating whether the secondary button is shown. + /// + public bool? ShowSecondaryButton { get; set; } + + /// + /// Gets or sets a value indicating whether the dismiss button is shown. + /// + public bool? ShowDismiss { get; set; } + + /// + /// Gets or sets a value indicating whether Markdown in the message is rendered. + /// + public bool? EnableMessageMarkdown { get; set; } + + internal InputsDialogInteractionOptions ToOptions() + { + return new InputsDialogInteractionOptions + { + PrimaryButtonText = PrimaryButtonText, + SecondaryButtonText = SecondaryButtonText, + ShowSecondaryButton = ShowSecondaryButton, + ShowDismiss = ShowDismiss, + EnableMessageMarkdown = EnableMessageMarkdown, + }; + } +} + +/// +/// The result of a boolean interaction prompt. +/// +[AspireDto] +internal sealed class BoolInteractionResult +{ + /// + /// Gets a value indicating whether the interaction was canceled by the user. + /// + public required bool Canceled { get; init; } + + /// + /// Gets the value returned from the interaction. Not meaningful when is . + /// + public bool Value { get; init; } + + internal static BoolInteractionResult From(InteractionResult result) + { + return new BoolInteractionResult + { + Canceled = result.Canceled, + Value = !result.Canceled && result.Data, + }; + } +} + +/// +/// The result of a single-input interaction prompt. +/// +[AspireDto] +internal sealed class InputInteractionResult +{ + /// + /// Gets a value indicating whether the interaction was canceled by the user. + /// + public required bool Canceled { get; init; } + + /// + /// Gets the input returned from the interaction. Not present when is . + /// + public InteractionInput? Input { get; init; } + + internal static InputInteractionResult From(InteractionResult result) + { + return new InputInteractionResult + { + Canceled = result.Canceled, + Input = result.Canceled ? null : result.Data, + }; + } +} + +/// +/// The result of a multi-input interaction prompt. +/// +[AspireDto] +internal sealed class InputsInteractionResult +{ + /// + /// Gets a value indicating whether the interaction was canceled by the user. + /// + public required bool Canceled { get; init; } + + /// + /// Gets the inputs returned from the interaction. Empty when is . + /// + public IReadOnlyList Inputs { get; init; } = []; + + internal static InputsInteractionResult From(InteractionResult result) + { + return new InputsInteractionResult + { + Canceled = result.Canceled, + Inputs = result.Canceled || result.Data is null ? [] : result.Data.ToArray(), + }; + } +} diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index dd82a3d3772..6eed1088944 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -57,6 +57,8 @@ Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceUrlsCallbackContext Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceUrlsEditor Aspire.Hosting/Aspire.Hosting.ApplicationModel.UpdateCommandStateContext [ExposeProperties] Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext [ExposeProperties] +Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder +Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext Aspire.Hosting/Aspire.Hosting.DistributedApplication Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext [ExposeProperties] Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContextOptions @@ -67,6 +69,7 @@ Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEventing [interfac Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationResourceEvent [interface] Aspire.Hosting/Aspire.Hosting.ExternalServiceResource Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder [interface] +Aspire.Hosting/Aspire.Hosting.IInteractionService [interface] Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext [ExposeProperties] Aspire.Hosting/Aspire.Hosting.InteractionInputCollection Aspire.Hosting/Aspire.Hosting.IResourceWithContainerFiles [interface] @@ -182,10 +185,17 @@ Aspire.Hosting/Aspire.Hosting.ApplicationModel.UpdateCommandStateResourceSnapsho Aspire.Hosting/Aspire.Hosting.Ats.AddContainerOptions # Options for configuring a container image in polyglot apphosts. Image: string # The container image name. Tag?: string # The container image tag. +Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult # The result of a boolean interaction prompt. + Canceled: boolean # Gets a value indicating whether the interaction was canceled by the user. + Value?: boolean # Gets the value returned from the interaction. Not meaningful when `Canceled` is `true`. Aspire.Hosting/Aspire.Hosting.Ats.CertificateTrustExecutionConfigurationExportData # ATS-friendly certificate trust data returned from an execution-configuration result. CertificateSubjects: string[] # The certificate subjects included in the trust configuration. CustomBundlePaths: string[] # The relative custom bundle paths. Scope: enum:Aspire.Hosting.ApplicationModel.CertificateTrustScope # The certificate trust scope. +Aspire.Hosting/Aspire.Hosting.Ats.ContainerFilesOptions # Options for creating or updating files and directories in a container from polyglot apphosts. + DefaultGroup?: number # The default group ID for the created or updated file system entries. Defaults to 0 for root if not set. + DefaultOwner?: number # The default owner UID for the created or updated file system entries. Defaults to 0 for root if not set. + Umask?: number # The Unix umask to apply to files or directories without explicit permissions. Use octal literals in JavaScript or TypeScript, for example `0o022`. Aspire.Hosting/Aspire.Hosting.Ats.CreateBuilderOptions # Options for creating a distributed application builder from polyglot apphosts. AllowUnsecuredTransport: boolean # Allows the use of HTTP urls for the AppHost resource endpoint. AppHostFilePath: string # The full path to the AppHost file (e.g., apphost.ts, apphost.py). Used for consistent socket path computation across CLI and AppHost. @@ -195,6 +205,19 @@ Aspire.Hosting/Aspire.Hosting.Ats.CreateBuilderOptions # Options for creating a DisableDashboard: boolean # Determines whether the dashboard is disabled. EnableResourceLogging: boolean # Enables resource logging. ProjectDirectory: string # The directory containing the AppHost project file. +Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions # Optional configuration shared by interaction input factory capabilities. + AllowCustomChoice?: boolean # Gets or sets a value indicating whether a custom choice is allowed. Only used by choice inputs. + Description: string # Gets or sets the description for the input. + Disabled?: boolean # Gets or sets a value indicating whether the input is disabled. + EnableDescriptionMarkdown?: boolean # Gets or sets a value indicating whether the description is rendered as Markdown. + Label: string # Gets or sets the label for the input. Defaults to the input name when not specified. + MaxLength?: number # Gets or sets the maximum length for text inputs. + Placeholder: string # Gets or sets the placeholder text for the input. + Required?: boolean # Gets or sets a value indicating whether the input is required. + Value: string # Gets or sets the initial value of the input. +Aspire.Hosting/Aspire.Hosting.Ats.DynamicLoadingOptions # Options controlling when a dynamic-loading callback runs. + AlwaysLoadOnStart?: boolean # Gets or sets a value indicating whether the callback always runs at the start of the prompt. + DependsOnInputs: string[] # Gets or sets the names of inputs this input depends on. The callback runs when any of them change. Aspire.Hosting/Aspire.Hosting.Ats.HttpsCertificateExecutionConfigurationExportData # ATS-friendly HTTPS certificate data returned from an execution-configuration result. IsKeyPathReferenced: boolean # Indicates whether the key path was referenced. IsPfxPathReferenced: boolean # Indicates whether the PFX path was referenced. @@ -207,6 +230,34 @@ Aspire.Hosting/Aspire.Hosting.Ats.HttpsCertificateInfo # ATS-friendly certificat Issuer: string # The certificate issuer. Subject: string # The certificate subject. Thumbprint?: string # The certificate thumbprint. +Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult # The result of a single-input interaction prompt. + Canceled: boolean # Gets a value indicating whether the interaction was canceled by the user. + Input?: Aspire.Hosting/Aspire.Hosting.InteractionInput # Gets the input returned from the interaction. Not present when `Canceled` is `true`. +Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult # The result of a multi-input interaction prompt. + Canceled: boolean # Gets a value indicating whether the interaction was canceled by the user. + Inputs?: Aspire.Hosting/Aspire.Hosting.InteractionInput[] # Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. +Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions # Options for inputs dialog prompts. + EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. + PrimaryButtonText: string # Gets or sets the primary button text. + SecondaryButtonText: string # Gets or sets the secondary button text. + ShowDismiss?: boolean # Gets or sets a value indicating whether the dismiss button is shown. + ShowSecondaryButton?: boolean # Gets or sets a value indicating whether the secondary button is shown. +Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions # Options for message box and confirmation prompts. + EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. + Intent?: enum:Aspire.Hosting.MessageIntent # Gets or sets the intent of the message box. + PrimaryButtonText: string # Gets or sets the primary button text. + SecondaryButtonText: string # Gets or sets the secondary button text. + ShowDismiss?: boolean # Gets or sets a value indicating whether the dismiss button is shown. + ShowSecondaryButton?: boolean # Gets or sets a value indicating whether the secondary button is shown. +Aspire.Hosting/Aspire.Hosting.Ats.InteractionNotificationOptions # Options for notification prompts. + EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. + Intent?: enum:Aspire.Hosting.MessageIntent # Gets or sets the intent of the notification. + LinkText: string # Gets or sets the text for a link in the notification. + LinkUrl: string # Gets or sets the URL for the link in the notification. + PrimaryButtonText: string # Gets or sets the primary button text. + SecondaryButtonText: string # Gets or sets the secondary button text. + ShowDismiss?: boolean # Gets or sets a value indicating whether the dismiss button is shown. + ShowSecondaryButton?: boolean # Gets or sets a value indicating whether the secondary button is shown. Aspire.Hosting/Aspire.Hosting.Ats.ParameterCustomInputOptions # Options for customizing parameter inputs from polyglot app hosts. AllowCustomChoice?: boolean # Gets or sets whether custom choices are allowed. Description: string # Gets or sets the description for the input. @@ -261,6 +312,7 @@ enum:Aspire.Hosting.ApplicationModel.UrlDisplayLocation = SummaryAndDetails | De enum:Aspire.Hosting.ApplicationModel.WaitBehavior = WaitOnResourceUnavailable | StopOnResourceUnavailable enum:Aspire.Hosting.DistributedApplicationOperation = Run | Publish enum:Aspire.Hosting.InputType = Text | SecretText | Choice | Boolean | Number +enum:Aspire.Hosting.MessageIntent = None | Success | Warning | Error | Information | Confirmation enum:Aspire.Hosting.OtlpProtocol = Grpc | HttpProtobuf | HttpJson enum:Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus = Unhealthy | Degraded | Healthy enum:System.Net.Sockets.ProtocolType = IP | IPv6HopByHopOptions | Unspecified | Icmp | Igmp | Ggp | IPv4 | Tcp | Pup | Udp | Idp | IPv6 | IPv6RoutingHeader | IPv6FragmentHeader | IPSecEncapsulatingSecurityPayload | IPSecAuthenticationHeader | IcmpV6 | IPv6NoNextHeader | IPv6DestinationOptions | ND | Raw | Ipx | Spx | SpxII | Unknown @@ -376,6 +428,7 @@ Aspire.Hosting.ApplicationModel/ExecuteCommandContext.arguments(context: Aspire. Aspire.Hosting.ApplicationModel/ExecuteCommandContext.cancellationToken(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecuteCommandContext) -> cancellationToken Aspire.Hosting.ApplicationModel/ExecuteCommandContext.logger(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecuteCommandContext) -> Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger Aspire.Hosting.ApplicationModel/ExecuteCommandContext.resourceName(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecuteCommandContext) -> string +Aspire.Hosting.ApplicationModel/ExecuteCommandContext.serviceProvider(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecuteCommandContext) -> System.ComponentModel/System.IServiceProvider Aspire.Hosting.ApplicationModel/getEndpoint(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceUrlsCallbackContext, name: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference Aspire.Hosting.ApplicationModel/getValueAsync(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression, cancellationToken: cancellationToken) -> string Aspire.Hosting.ApplicationModel/IAspireStore.basePath(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IAspireStore) -> string @@ -404,6 +457,13 @@ Aspire.Hosting.ApplicationModel/UpdateCommandStateContext.resourceSnapshot(conte Aspire.Hosting.ApplicationModel/warning(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade, message: string) -> void Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.cancellationToken(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> cancellationToken Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.executionContext(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext +Aspire.Hosting.Ats/getInputName(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext) -> string +Aspire.Hosting.Ats/getInputValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, inputName: string) -> string +Aspire.Hosting.Ats/setChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, choices: Aspire.Hosting/Dict) -> void +Aspire.Hosting.Ats/setValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, value: string) -> void +Aspire.Hosting.Ats/withChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, choices: Aspire.Hosting/Dict) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder +Aspire.Hosting.Ats/withDynamicLoading(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, callback: callback, options?: Aspire.Hosting/Aspire.Hosting.Ats.DynamicLoadingOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder +Aspire.Hosting.Ats/withValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, value: string) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting.Eventing/IDistributedApplicationEventing.unsubscribe(context: Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEventing, subscription: Aspire.Hosting/Aspire.Hosting.Eventing.DistributedApplicationEventSubscription) -> void Aspire.Hosting.Pipelines/addTag(context: Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineStep, tag: string) -> void Aspire.Hosting.Pipelines/dependsOn(context: Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineStep, stepName: string) -> void @@ -470,11 +530,16 @@ Aspire.Hosting/completeStepMarkdown(markdownString: string, completionState?: st Aspire.Hosting/completeTask(completionMessage?: string, completionState?: string, cancellationToken?: cancellationToken) -> void Aspire.Hosting/completeTaskMarkdown(markdownString: string, completionState?: string, cancellationToken?: cancellationToken) -> void Aspire.Hosting/configure(callback: callback) -> void +Aspire.Hosting/createBooleanInput(name: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/createBuilder() -> Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder +Aspire.Hosting/createChoiceInput(name: string, choices?: Aspire.Hosting/Dict, options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/createExecutionConfiguration() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExecutionConfigurationBuilder Aspire.Hosting/createLogger(categoryName: string) -> Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger Aspire.Hosting/createMarkdownTask(markdownString: string, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingTask +Aspire.Hosting/createNumberInput(name: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder +Aspire.Hosting/createSecretInput(name: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/createTask(statusText: string, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingTask +Aspire.Hosting/createTextInput(name: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/Dict.clear() -> void Aspire.Hosting/Dict.count() -> number Aspire.Hosting/Dict.get(key: any) -> any @@ -525,6 +590,7 @@ Aspire.Hosting/getEndpoint(name: string) -> Aspire.Hosting/Aspire.Hosting.Applic Aspire.Hosting/getEventing() -> Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEventing Aspire.Hosting/getFileNameWithContent(filenameTemplate: string, sourceFilename: string) -> string Aspire.Hosting/getHttpsCertificateData() -> Aspire.Hosting/Aspire.Hosting.Ats.HttpsCertificateExecutionConfigurationExportData +Aspire.Hosting/getInteractionService() -> Aspire.Hosting/Aspire.Hosting.IInteractionService Aspire.Hosting/getLoggerFactory() -> Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILoggerFactory Aspire.Hosting/getOrSetSecret(resourceBuilder: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, name: string, value: string) -> void Aspire.Hosting/getResourceCommandService() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceCommandService @@ -544,6 +610,7 @@ Aspire.Hosting/InputsDialogValidationContext.addValidationError(context: Aspire. Aspire.Hosting/InputsDialogValidationContext.cancellationToken(context: Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext) -> cancellationToken Aspire.Hosting/InputsDialogValidationContext.inputs(context: Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext) -> Aspire.Hosting/Aspire.Hosting.InteractionInputCollection Aspire.Hosting/InteractionInputCollection.toArray(context: Aspire.Hosting/Aspire.Hosting.InteractionInputCollection) -> Aspire.Hosting/Aspire.Hosting.InteractionInput[] +Aspire.Hosting/isAvailable() -> boolean Aspire.Hosting/isDevelopment() -> boolean Aspire.Hosting/isEnvironment(environmentName: string) -> boolean Aspire.Hosting/isProduction() -> boolean @@ -580,6 +647,11 @@ Aspire.Hosting/ProjectResourceOptions.launchProfileName(context: Aspire.Hosting/ Aspire.Hosting/ProjectResourceOptions.setExcludeKestrelEndpoints(context: Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions, value: boolean) -> Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions Aspire.Hosting/ProjectResourceOptions.setExcludeLaunchProfile(context: Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions, value: boolean) -> Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions Aspire.Hosting/ProjectResourceOptions.setLaunchProfileName(context: Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions, value: string) -> Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions +Aspire.Hosting/promptConfirmation(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult +Aspire.Hosting/promptInput(title: string, message: string, input: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult +Aspire.Hosting/promptInputs(title: string, message: string, inputs: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder[], options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult +Aspire.Hosting/promptMessageBox(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult +Aspire.Hosting/promptNotification(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionNotificationOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult Aspire.Hosting/publishAsConnectionString() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/publishAsContainer() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/publishAsDockerFile(configure: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecutableResource @@ -619,6 +691,7 @@ Aspire.Hosting/withCommand(name: string, displayName: string, executeCommand: ca Aspire.Hosting/withComputeEnvironment(computeEnvironmentResource: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IComputeEnvironmentResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IComputeResource Aspire.Hosting/withConnectionProperty(name: string, value: string|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithConnectionString Aspire.Hosting/withContainerCertificatePaths(customCertificatesDestination?: string, defaultCertificateBundlePaths?: string[], defaultCertificateDirectoryPaths?: string[]) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource +Aspire.Hosting/withContainerFiles(destinationPath: string, sourcePath: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.ContainerFilesOptions) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withContainerFilesSource(sourcePath: string) -> Aspire.Hosting/Aspire.Hosting.IResourceWithContainerFiles Aspire.Hosting/withContainerName(name: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withContainerNetworkAlias(alias: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index a6f434a1801..05ae188883d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -157,6 +157,18 @@ const ( InputTypeNumber InputType = "Number" ) +// MessageIntent represents MessageIntent. +type MessageIntent string + +const ( + MessageIntentNone MessageIntent = "None" + MessageIntentSuccess MessageIntent = "Success" + MessageIntentWarning MessageIntent = "Warning" + MessageIntentError MessageIntent = "Error" + MessageIntentInformation MessageIntent = "Information" + MessageIntentConfirmation MessageIntent = "Confirmation" +) + // ResourceCommandVisibility represents ResourceCommandVisibility. type ResourceCommandVisibility string @@ -382,6 +394,158 @@ func (d *HttpsCertificateExecutionConfigurationExportData) ToMap() map[string]an return m } +// CreateInteractionInputOptions represents CreateInteractionInputOptions. +type CreateInteractionInputOptions struct { + Label string `json:"Label,omitempty"` + Description string `json:"Description,omitempty"` + EnableDescriptionMarkdown *bool `json:"EnableDescriptionMarkdown,omitempty"` + Required *bool `json:"Required,omitempty"` + Placeholder string `json:"Placeholder,omitempty"` + Value string `json:"Value,omitempty"` + AllowCustomChoice *bool `json:"AllowCustomChoice,omitempty"` + Disabled *bool `json:"Disabled,omitempty"` + MaxLength *float64 `json:"MaxLength,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *CreateInteractionInputOptions) ToMap() map[string]any { + m := map[string]any{} + m["Label"] = serializeValue(d.Label) + m["Description"] = serializeValue(d.Description) + if d.EnableDescriptionMarkdown != nil { m["EnableDescriptionMarkdown"] = serializeValue(d.EnableDescriptionMarkdown) } + if d.Required != nil { m["Required"] = serializeValue(d.Required) } + m["Placeholder"] = serializeValue(d.Placeholder) + m["Value"] = serializeValue(d.Value) + if d.AllowCustomChoice != nil { m["AllowCustomChoice"] = serializeValue(d.AllowCustomChoice) } + if d.Disabled != nil { m["Disabled"] = serializeValue(d.Disabled) } + if d.MaxLength != nil { m["MaxLength"] = serializeValue(d.MaxLength) } + return m +} + +// DynamicLoadingOptions represents DynamicLoadingOptions. +type DynamicLoadingOptions struct { + AlwaysLoadOnStart *bool `json:"AlwaysLoadOnStart,omitempty"` + DependsOnInputs []string `json:"DependsOnInputs,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *DynamicLoadingOptions) ToMap() map[string]any { + m := map[string]any{} + if d.AlwaysLoadOnStart != nil { m["AlwaysLoadOnStart"] = serializeValue(d.AlwaysLoadOnStart) } + if d.DependsOnInputs != nil { m["DependsOnInputs"] = serializeValue(d.DependsOnInputs) } + return m +} + +// InteractionMessageBoxOptions represents InteractionMessageBoxOptions. +type InteractionMessageBoxOptions struct { + PrimaryButtonText string `json:"PrimaryButtonText,omitempty"` + SecondaryButtonText string `json:"SecondaryButtonText,omitempty"` + ShowSecondaryButton *bool `json:"ShowSecondaryButton,omitempty"` + ShowDismiss *bool `json:"ShowDismiss,omitempty"` + EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"` + Intent *MessageIntent `json:"Intent,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *InteractionMessageBoxOptions) ToMap() map[string]any { + m := map[string]any{} + m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) + m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) + if d.ShowSecondaryButton != nil { m["ShowSecondaryButton"] = serializeValue(d.ShowSecondaryButton) } + if d.ShowDismiss != nil { m["ShowDismiss"] = serializeValue(d.ShowDismiss) } + if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) } + if d.Intent != nil { m["Intent"] = serializeValue(d.Intent) } + return m +} + +// InteractionNotificationOptions represents InteractionNotificationOptions. +type InteractionNotificationOptions struct { + PrimaryButtonText string `json:"PrimaryButtonText,omitempty"` + SecondaryButtonText string `json:"SecondaryButtonText,omitempty"` + ShowSecondaryButton *bool `json:"ShowSecondaryButton,omitempty"` + ShowDismiss *bool `json:"ShowDismiss,omitempty"` + EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"` + Intent *MessageIntent `json:"Intent,omitempty"` + LinkText string `json:"LinkText,omitempty"` + LinkUrl string `json:"LinkUrl,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *InteractionNotificationOptions) ToMap() map[string]any { + m := map[string]any{} + m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) + m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) + if d.ShowSecondaryButton != nil { m["ShowSecondaryButton"] = serializeValue(d.ShowSecondaryButton) } + if d.ShowDismiss != nil { m["ShowDismiss"] = serializeValue(d.ShowDismiss) } + if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) } + if d.Intent != nil { m["Intent"] = serializeValue(d.Intent) } + m["LinkText"] = serializeValue(d.LinkText) + m["LinkUrl"] = serializeValue(d.LinkUrl) + return m +} + +// InteractionInputsDialogOptions represents InteractionInputsDialogOptions. +type InteractionInputsDialogOptions struct { + PrimaryButtonText string `json:"PrimaryButtonText,omitempty"` + SecondaryButtonText string `json:"SecondaryButtonText,omitempty"` + ShowSecondaryButton *bool `json:"ShowSecondaryButton,omitempty"` + ShowDismiss *bool `json:"ShowDismiss,omitempty"` + EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *InteractionInputsDialogOptions) ToMap() map[string]any { + m := map[string]any{} + m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) + m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) + if d.ShowSecondaryButton != nil { m["ShowSecondaryButton"] = serializeValue(d.ShowSecondaryButton) } + if d.ShowDismiss != nil { m["ShowDismiss"] = serializeValue(d.ShowDismiss) } + if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) } + return m +} + +// BoolInteractionResult represents BoolInteractionResult. +type BoolInteractionResult struct { + Canceled bool `json:"Canceled,omitempty"` + Value *bool `json:"Value,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *BoolInteractionResult) ToMap() map[string]any { + m := map[string]any{} + m["Canceled"] = serializeValue(d.Canceled) + if d.Value != nil { m["Value"] = serializeValue(d.Value) } + return m +} + +// InputInteractionResult represents InputInteractionResult. +type InputInteractionResult struct { + Canceled bool `json:"Canceled,omitempty"` + Input *InteractionInput `json:"Input,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *InputInteractionResult) ToMap() map[string]any { + m := map[string]any{} + m["Canceled"] = serializeValue(d.Canceled) + if d.Input != nil { m["Input"] = serializeValue(d.Input) } + return m +} + +// InputsInteractionResult represents InputsInteractionResult. +type InputsInteractionResult struct { + Canceled bool `json:"Canceled,omitempty"` + Inputs []*InteractionInput `json:"Inputs,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *InputsInteractionResult) ToMap() map[string]any { + m := map[string]any{} + m["Canceled"] = serializeValue(d.Canceled) + if d.Inputs != nil { m["Inputs"] = serializeValue(d.Inputs) } + return m +} + // ResourceEventDto represents ResourceEventDto. type ResourceEventDto struct { ResourceName string `json:"ResourceName,omitempty"` @@ -14224,6 +14388,7 @@ type ExecuteCommandContext interface { CancellationToken() (*CancellationToken, error) Logger() Logger ResourceName() (string, error) + ServiceProvider() ServiceProvider Err() error } @@ -14305,6 +14470,25 @@ func (s *executeCommandContext) ResourceName() (string, error) { return decodeAs[string](result) } +// ServiceProvider the service provider. +func (s *executeCommandContext) ServiceProvider() ServiceProvider { + if s.err != nil { return &serviceProvider{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.ApplicationModel/ExecuteCommandContext.serviceProvider", reqArgs) + if err != nil { + return &serviceProvider{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting.ApplicationModel/ExecuteCommandContext.serviceProvider returned unexpected type %T", result) + return &serviceProvider{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &serviceProvider{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + // ExecutionConfigurationBuilder is the public interface for handle type ExecutionConfigurationBuilder. type ExecutionConfigurationBuilder interface { handleReference @@ -15788,6 +15972,75 @@ func (s *inputsDialogValidationContext) Inputs() InteractionInputCollection { return &interactionInputCollection{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} } +// InteractionInputBuilder is the public interface for handle type InteractionInputBuilder. +type InteractionInputBuilder interface { + handleReference + WithChoiceOptions(choices map[string]string) InteractionInputBuilder + WithDynamicLoading(callback func(arg InteractionInputLoadContext), options ...*WithDynamicLoadingOptions) InteractionInputBuilder + WithValue(value string) InteractionInputBuilder + Err() error +} + +// interactionInputBuilder is the unexported impl of InteractionInputBuilder. +type interactionInputBuilder struct { + *resourceBuilderBase +} + +// newInteractionInputBuilderFromHandle wraps an existing handle as InteractionInputBuilder. +func newInteractionInputBuilderFromHandle(h *handle, c *client) InteractionInputBuilder { + return &interactionInputBuilder{resourceBuilderBase: newResourceBuilderBase(h, c)} +} + +// WithChoiceOptions sets the choice options for the input. +func (s *interactionInputBuilder) WithChoiceOptions(choices map[string]string) InteractionInputBuilder { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + if choices != nil { reqArgs["choices"] = serializeValue(choices) } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/withChoiceOptions", reqArgs); err != nil { s.setErr(err) } + return s +} + +// WithDynamicLoading attaches a callback that dynamically loads or updates the input after the prompt starts. +func (s *interactionInputBuilder) WithDynamicLoading(callback func(arg InteractionInputLoadContext), options ...*WithDynamicLoadingOptions) InteractionInputBuilder { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + if callback != nil { + cb := callback + shim := func(args ...any) any { + cb(callbackArg[InteractionInputLoadContext](args, 0)) + return nil + } + reqArgs["callback"] = s.client.registerCallback(shim) + } + if len(options) > 0 { + merged := &WithDynamicLoadingOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/withDynamicLoading", reqArgs); err != nil { s.setErr(err) } + return s +} + +// WithValue sets the value of the input. +func (s *interactionInputBuilder) WithValue(value string) InteractionInputBuilder { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + reqArgs["value"] = serializeValue(value) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/withValue", reqArgs); err != nil { s.setErr(err) } + return s +} + // InteractionInputCollection is the public interface for handle type InteractionInputCollection. type InteractionInputCollection interface { handleReference @@ -15820,6 +16073,411 @@ func (s *interactionInputCollection) ToArray() ([]*InteractionInput, error) { return decodeAs[[]*InteractionInput](result) } +// InteractionInputLoadContext is the public interface for handle type InteractionInputLoadContext. +type InteractionInputLoadContext interface { + handleReference + GetInputName() (string, error) + GetInputValue(inputName string) (string, error) + SetChoiceOptions(choices map[string]string) error + SetValue(value string) error + Err() error +} + +// interactionInputLoadContext is the unexported impl of InteractionInputLoadContext. +type interactionInputLoadContext struct { + *resourceBuilderBase +} + +// newInteractionInputLoadContextFromHandle wraps an existing handle as InteractionInputLoadContext. +func newInteractionInputLoadContextFromHandle(h *handle, c *client) InteractionInputLoadContext { + return &interactionInputLoadContext{resourceBuilderBase: newResourceBuilderBase(h, c)} +} + +// GetInputName gets the name of the input that is loading. +func (s *interactionInputLoadContext) GetInputName() (string, error) { + if s.err != nil { var zero string; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/getInputName", reqArgs) + if err != nil { + var zero string + return zero, err + } + return decodeAs[string](result) +} + +// GetInputValue gets the current value of an input in the prompt by name. +func (s *interactionInputLoadContext) GetInputValue(inputName string) (string, error) { + if s.err != nil { var zero string; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + reqArgs["inputName"] = serializeValue(inputName) + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/getInputValue", reqArgs) + if err != nil { + var zero string + return zero, err + } + return decodeAs[string](result) +} + +// SetChoiceOptions sets the choice options for the loading input. +func (s *interactionInputLoadContext) SetChoiceOptions(choices map[string]string) error { + if s.err != nil { return s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + if choices != nil { reqArgs["choices"] = serializeValue(choices) } + _, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/setChoiceOptions", reqArgs) + return err +} + +// SetValue sets the value of the loading input. +func (s *interactionInputLoadContext) SetValue(value string) error { + if s.err != nil { return s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + reqArgs["value"] = serializeValue(value) + _, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/setValue", reqArgs) + return err +} + +// InteractionService is the public interface for handle type InteractionService. +type InteractionService interface { + handleReference + CreateBooleanInput(name string, options ...*CreateBooleanInputOptions) InteractionInputBuilder + CreateChoiceInput(name string, options ...*CreateChoiceInputOptions) InteractionInputBuilder + CreateNumberInput(name string, options ...*CreateNumberInputOptions) InteractionInputBuilder + CreateSecretInput(name string, options ...*CreateSecretInputOptions) InteractionInputBuilder + CreateTextInput(name string, options ...*CreateTextInputOptions) InteractionInputBuilder + IsAvailable() (bool, error) + PromptConfirmation(title string, message string, options ...*PromptConfirmationOptions) (*BoolInteractionResult, error) + PromptInput(title string, message string, input InteractionInputBuilder, options ...*PromptInputOptions) (*InputInteractionResult, error) + PromptInputs(title string, message string, inputs []InteractionInputBuilder, options ...*PromptInputsOptions) (*InputsInteractionResult, error) + PromptMessageBox(title string, message string, options ...*PromptMessageBoxOptions) (*BoolInteractionResult, error) + PromptNotification(title string, message string, options ...*PromptNotificationOptions) (*BoolInteractionResult, error) + Err() error +} + +// interactionService is the unexported impl of InteractionService. +type interactionService struct { + *resourceBuilderBase +} + +// newInteractionServiceFromHandle wraps an existing handle as InteractionService. +func newInteractionServiceFromHandle(h *handle, c *client) InteractionService { + return &interactionService{resourceBuilderBase: newResourceBuilderBase(h, c)} +} + +// CreateBooleanInput creates a boolean (checkbox) input. +func (s *interactionService) CreateBooleanInput(name string, options ...*CreateBooleanInputOptions) InteractionInputBuilder { + if s.err != nil { return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["name"] = serializeValue(name) + if len(options) > 0 { + merged := &CreateBooleanInputOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/createBooleanInput", reqArgs) + if err != nil { + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting/createBooleanInput returned unexpected type %T", result) + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionInputBuilder{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + +// CreateChoiceInput creates a choice input that selects from a list of options. +func (s *interactionService) CreateChoiceInput(name string, options ...*CreateChoiceInputOptions) InteractionInputBuilder { + if s.err != nil { return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["name"] = serializeValue(name) + if len(options) > 0 { + merged := &CreateChoiceInputOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/createChoiceInput", reqArgs) + if err != nil { + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting/createChoiceInput returned unexpected type %T", result) + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionInputBuilder{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + +// CreateNumberInput creates a numeric input. +func (s *interactionService) CreateNumberInput(name string, options ...*CreateNumberInputOptions) InteractionInputBuilder { + if s.err != nil { return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["name"] = serializeValue(name) + if len(options) > 0 { + merged := &CreateNumberInputOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/createNumberInput", reqArgs) + if err != nil { + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting/createNumberInput returned unexpected type %T", result) + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionInputBuilder{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + +// CreateSecretInput creates a secret (masked) text input. +func (s *interactionService) CreateSecretInput(name string, options ...*CreateSecretInputOptions) InteractionInputBuilder { + if s.err != nil { return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["name"] = serializeValue(name) + if len(options) > 0 { + merged := &CreateSecretInputOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/createSecretInput", reqArgs) + if err != nil { + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting/createSecretInput returned unexpected type %T", result) + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionInputBuilder{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + +// CreateTextInput creates a single-line text input. +func (s *interactionService) CreateTextInput(name string, options ...*CreateTextInputOptions) InteractionInputBuilder { + if s.err != nil { return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["name"] = serializeValue(name) + if len(options) > 0 { + merged := &CreateTextInputOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/createTextInput", reqArgs) + if err != nil { + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting/createTextInput returned unexpected type %T", result) + return &interactionInputBuilder{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionInputBuilder{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + +// IsAvailable gets a value indicating whether the interaction service is available to prompt the user. +func (s *interactionService) IsAvailable() (bool, error) { + if s.err != nil { var zero bool; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/isAvailable", reqArgs) + if err != nil { + var zero bool + return zero, err + } + return decodeAs[bool](result) +} + +// PromptConfirmation prompts the user for confirmation with an OK/Cancel dialog. +func (s *interactionService) PromptConfirmation(title string, message string, options ...*PromptConfirmationOptions) (*BoolInteractionResult, error) { + if s.err != nil { var zero *BoolInteractionResult; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["title"] = serializeValue(title) + reqArgs["message"] = serializeValue(message) + if len(options) > 0 { + merged := &PromptConfirmationOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + if merged.CancellationToken != nil { + ctx = merged.CancellationToken.Context() + if id := s.client.registerCancellation(merged.CancellationToken); id != "" { + reqArgs["cancellationToken"] = id + } + } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/promptConfirmation", reqArgs) + if err != nil { + var zero *BoolInteractionResult + return zero, err + } + return decodeAs[*BoolInteractionResult](result) +} + +// PromptInput prompts the user for a single input. +func (s *interactionService) PromptInput(title string, message string, input InteractionInputBuilder, options ...*PromptInputOptions) (*InputInteractionResult, error) { + if s.err != nil { var zero *InputInteractionResult; return zero, s.err } + if input != nil { if err := input.Err(); err != nil { var zero *InputInteractionResult; return zero, err } } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["title"] = serializeValue(title) + reqArgs["message"] = serializeValue(message) + reqArgs["input"] = serializeValue(input) + if len(options) > 0 { + merged := &PromptInputOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + if merged.CancellationToken != nil { + ctx = merged.CancellationToken.Context() + if id := s.client.registerCancellation(merged.CancellationToken); id != "" { + reqArgs["cancellationToken"] = id + } + } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/promptInput", reqArgs) + if err != nil { + var zero *InputInteractionResult + return zero, err + } + return decodeAs[*InputInteractionResult](result) +} + +// PromptInputs prompts the user for multiple inputs. +func (s *interactionService) PromptInputs(title string, message string, inputs []InteractionInputBuilder, options ...*PromptInputsOptions) (*InputsInteractionResult, error) { + if s.err != nil { var zero *InputsInteractionResult; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["title"] = serializeValue(title) + reqArgs["message"] = serializeValue(message) + if inputs != nil { reqArgs["inputs"] = serializeValue(inputs) } + if len(options) > 0 { + merged := &PromptInputsOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + if merged.CancellationToken != nil { + ctx = merged.CancellationToken.Context() + if id := s.client.registerCancellation(merged.CancellationToken); id != "" { + reqArgs["cancellationToken"] = id + } + } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/promptInputs", reqArgs) + if err != nil { + var zero *InputsInteractionResult + return zero, err + } + return decodeAs[*InputsInteractionResult](result) +} + +// PromptMessageBox prompts the user with a message box dialog. +func (s *interactionService) PromptMessageBox(title string, message string, options ...*PromptMessageBoxOptions) (*BoolInteractionResult, error) { + if s.err != nil { var zero *BoolInteractionResult; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["title"] = serializeValue(title) + reqArgs["message"] = serializeValue(message) + if len(options) > 0 { + merged := &PromptMessageBoxOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + if merged.CancellationToken != nil { + ctx = merged.CancellationToken.Context() + if id := s.client.registerCancellation(merged.CancellationToken); id != "" { + reqArgs["cancellationToken"] = id + } + } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/promptMessageBox", reqArgs) + if err != nil { + var zero *BoolInteractionResult + return zero, err + } + return decodeAs[*BoolInteractionResult](result) +} + +// PromptNotification prompts the user with a notification. +func (s *interactionService) PromptNotification(title string, message string, options ...*PromptNotificationOptions) (*BoolInteractionResult, error) { + if s.err != nil { var zero *BoolInteractionResult; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "interactionService": s.handle.ToJSON(), + } + reqArgs["title"] = serializeValue(title) + reqArgs["message"] = serializeValue(message) + if len(options) > 0 { + merged := &PromptNotificationOptions{} + for _, opt := range options { + if opt != nil { merged = deepUpdate(merged, opt) } + } + for k, v := range merged.ToMap() { reqArgs[k] = v } + if merged.CancellationToken != nil { + ctx = merged.CancellationToken.Context() + if id := s.client.registerCancellation(merged.CancellationToken); id != "" { + reqArgs["cancellationToken"] = id + } + } + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/promptNotification", reqArgs) + if err != nil { + var zero *BoolInteractionResult + return zero, err + } + return decodeAs[*BoolInteractionResult](result) +} + // LogFacade is the public interface for handle type LogFacade. type LogFacade interface { handleReference @@ -20387,6 +21045,7 @@ type ServiceProvider interface { GetAspireStore() AspireStore GetDistributedApplicationModel() DistributedApplicationModel GetEventing() DistributedApplicationEventing + GetInteractionService() InteractionService GetLoggerFactory() LoggerFactory GetResourceCommandService() ResourceCommandService GetResourceLoggerService() ResourceLoggerService @@ -20462,6 +21121,25 @@ func (s *serviceProvider) GetEventing() DistributedApplicationEventing { return &distributedApplicationEventing{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} } +// GetInteractionService gets the interaction service from the service provider. +func (s *serviceProvider) GetInteractionService() InteractionService { + if s.err != nil { return &interactionService{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "serviceProvider": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/getInteractionService", reqArgs) + if err != nil { + return &interactionService{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting/getInteractionService returned unexpected type %T", result) + return &interactionService{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionService{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + // GetLoggerFactory gets the logger factory from the service provider. func (s *serviceProvider) GetLoggerFactory() LoggerFactory { if s.err != nil { return &loggerFactory{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } @@ -25986,6 +26664,145 @@ func (o *BuildOptions) ToMap() map[string]any { return m } +// PromptConfirmationOptions carries optional parameters for PromptConfirmation. +type PromptConfirmationOptions struct { + Options *InteractionMessageBoxOptions `json:"options,omitempty"` + CancellationToken *CancellationToken `json:"-"` +} + +func (o *PromptConfirmationOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// PromptMessageBoxOptions carries optional parameters for PromptMessageBox. +type PromptMessageBoxOptions struct { + Options *InteractionMessageBoxOptions `json:"options,omitempty"` + CancellationToken *CancellationToken `json:"-"` +} + +func (o *PromptMessageBoxOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// PromptNotificationOptions carries optional parameters for PromptNotification. +type PromptNotificationOptions struct { + Options *InteractionNotificationOptions `json:"options,omitempty"` + CancellationToken *CancellationToken `json:"-"` +} + +func (o *PromptNotificationOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// PromptInputOptions carries optional parameters for PromptInput. +type PromptInputOptions struct { + Options *InteractionInputsDialogOptions `json:"options,omitempty"` + CancellationToken *CancellationToken `json:"-"` +} + +func (o *PromptInputOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// PromptInputsOptions carries optional parameters for PromptInputs. +type PromptInputsOptions struct { + Options *InteractionInputsDialogOptions `json:"options,omitempty"` + CancellationToken *CancellationToken `json:"-"` +} + +func (o *PromptInputsOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// CreateTextInputOptions carries optional parameters for CreateTextInput. +type CreateTextInputOptions struct { + Options *CreateInteractionInputOptions `json:"options,omitempty"` +} + +func (o *CreateTextInputOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// CreateSecretInputOptions carries optional parameters for CreateSecretInput. +type CreateSecretInputOptions struct { + Options *CreateInteractionInputOptions `json:"options,omitempty"` +} + +func (o *CreateSecretInputOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// CreateBooleanInputOptions carries optional parameters for CreateBooleanInput. +type CreateBooleanInputOptions struct { + Options *CreateInteractionInputOptions `json:"options,omitempty"` +} + +func (o *CreateBooleanInputOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// CreateNumberInputOptions carries optional parameters for CreateNumberInput. +type CreateNumberInputOptions struct { + Options *CreateInteractionInputOptions `json:"options,omitempty"` +} + +func (o *CreateNumberInputOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// CreateChoiceInputOptions carries optional parameters for CreateChoiceInput. +type CreateChoiceInputOptions struct { + Choices map[string]string `json:"choices,omitempty"` + Options *CreateInteractionInputOptions `json:"options,omitempty"` +} + +func (o *CreateChoiceInputOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Choices != nil { m["choices"] = serializeValue(o.Choices) } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + +// WithDynamicLoadingOptions carries optional parameters for WithDynamicLoading. +type WithDynamicLoadingOptions struct { + Options *DynamicLoadingOptions `json:"options,omitempty"` +} + +func (o *WithDynamicLoadingOptions) ToMap() map[string]any { + m := map[string]any{} + if o == nil { return m } + if o.Options != nil { m["options"] = serializeValue(o.Options) } + return m +} + // WaitForResourceStateOptions carries optional parameters for WaitForResourceState. type WaitForResourceStateOptions struct { TargetState *string `json:"targetState,omitempty"` @@ -26490,9 +27307,18 @@ func registerWrappers(c *client) { c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext", func(h *handle, c *client) any { return newInputsDialogValidationContextFromHandle(h, c) }) + c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", func(h *handle, c *client) any { + return newInteractionInputBuilderFromHandle(h, c) + }) c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.InteractionInputCollection", func(h *handle, c *client) any { return newInteractionInputCollectionFromHandle(h, c) }) + c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", func(h *handle, c *client) any { + return newInteractionInputLoadContextFromHandle(h, c) + }) + c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.IInteractionService", func(h *handle, c *client) any { + return newInteractionServiceFromHandle(h, c) + }) c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade", func(h *handle, c *client) any { return newLogFacadeFromHandle(h, c) }) diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index ab45143efc6..895c3d4c3c9 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1343,6 +1343,7 @@ public class AspireRegistrations { AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceNotificationService", (h, c) -> new ResourceNotificationService(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceLoggerService", (h, c) -> new ResourceLoggerService(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceCommandService", (h, c) -> new ResourceCommandService(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.IInteractionService", (h, c) -> new IInteractionService(h, c)); AspireClient.registerHandleWrapper("Microsoft.Extensions.Configuration.Abstractions/Microsoft.Extensions.Configuration.IConfiguration", (h, c) -> new IConfiguration(h, c)); AspireClient.registerHandleWrapper("Microsoft.Extensions.Configuration.Abstractions/Microsoft.Extensions.Configuration.IConfigurationSection", (h, c) -> new IConfigurationSection(h, c)); AspireClient.registerHandleWrapper("Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.IHostEnvironment", (h, c) -> new IHostEnvironment(h, c)); @@ -1370,6 +1371,8 @@ public class AspireRegistrations { AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEvent", (h, c) -> new IDistributedApplicationEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationResourceEvent", (h, c) -> new IDistributedApplicationResourceEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEventing", (h, c) -> new IDistributedApplicationEventing(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", (h, c) -> new InteractionInputBuilder(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", (h, c) -> new InteractionInputLoadContext(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext", (h, c) -> new EventingSubscriberRegistrationContext(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.AfterResourcesCreatedEvent", (h, c) -> new AfterResourcesCreatedEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.BeforeResourceStartedEvent", (h, c) -> new BeforeResourceStartedEvent(h, c)); @@ -1595,6 +1598,42 @@ public DistributedApplicationModel model() { } +// ===== BoolInteractionResult.java ===== +// BoolInteractionResult.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** BoolInteractionResult DTO. */ +public class BoolInteractionResult implements JsonSerializable { + private boolean canceled; + private Boolean value; + + public boolean getCanceled() { return canceled; } + public void setCanceled(boolean value) { this.canceled = value; } + public Boolean getValue() { return value; } + public void setValue(Boolean value) { this.value = value; } + + @SuppressWarnings("unchecked") + public static BoolInteractionResult fromMap(Map map) { + var value = new BoolInteractionResult(); + var canceledValue = map.get("Canceled"); + value.setCanceled((Boolean) canceledValue); + var valueValue = map.get("Value"); + value.setValue(valueValue == null ? null : (Boolean) valueValue); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("Canceled", AspireClient.serializeValue(canceled)); + map.put("Value", AspireClient.serializeValue(value)); + return map; + } +} + // ===== BuildOptions.java ===== // BuildOptions.java - GENERATED CODE - DO NOT EDIT @@ -6777,6 +6816,111 @@ public Map toMap() { } } +// ===== CreateChoiceInputOptions.java ===== +// CreateChoiceInputOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Options for CreateChoiceInput. */ +public final class CreateChoiceInputOptions { + private Map choices; + private CreateInteractionInputOptions options; + + public Map getChoices() { return choices; } + public CreateChoiceInputOptions choices(Map value) { + this.choices = value; + return this; + } + + public CreateInteractionInputOptions getOptions() { return options; } + public CreateChoiceInputOptions options(CreateInteractionInputOptions value) { + this.options = value; + return this; + } + +} + +// ===== CreateInteractionInputOptions.java ===== +// CreateInteractionInputOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** CreateInteractionInputOptions DTO. */ +public class CreateInteractionInputOptions implements JsonSerializable { + private String label; + private String description; + private Boolean enableDescriptionMarkdown; + private Boolean required; + private String placeholder; + private String value; + private Boolean allowCustomChoice; + private Boolean disabled; + private Double maxLength; + + public String getLabel() { return label; } + public void setLabel(String value) { this.label = value; } + public String getDescription() { return description; } + public void setDescription(String value) { this.description = value; } + public Boolean getEnableDescriptionMarkdown() { return enableDescriptionMarkdown; } + public void setEnableDescriptionMarkdown(Boolean value) { this.enableDescriptionMarkdown = value; } + public Boolean getRequired() { return required; } + public void setRequired(Boolean value) { this.required = value; } + public String getPlaceholder() { return placeholder; } + public void setPlaceholder(String value) { this.placeholder = value; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + public Boolean getAllowCustomChoice() { return allowCustomChoice; } + public void setAllowCustomChoice(Boolean value) { this.allowCustomChoice = value; } + public Boolean getDisabled() { return disabled; } + public void setDisabled(Boolean value) { this.disabled = value; } + public Double getMaxLength() { return maxLength; } + public void setMaxLength(Double value) { this.maxLength = value; } + + @SuppressWarnings("unchecked") + public static CreateInteractionInputOptions fromMap(Map map) { + var value = new CreateInteractionInputOptions(); + var labelValue = map.get("Label"); + value.setLabel(labelValue == null ? null : (String) labelValue); + var descriptionValue = map.get("Description"); + value.setDescription(descriptionValue == null ? null : (String) descriptionValue); + var enableDescriptionMarkdownValue = map.get("EnableDescriptionMarkdown"); + value.setEnableDescriptionMarkdown(enableDescriptionMarkdownValue == null ? null : (Boolean) enableDescriptionMarkdownValue); + var requiredValue = map.get("Required"); + value.setRequired(requiredValue == null ? null : (Boolean) requiredValue); + var placeholderValue = map.get("Placeholder"); + value.setPlaceholder(placeholderValue == null ? null : (String) placeholderValue); + var valueValue = map.get("Value"); + value.setValue(valueValue == null ? null : (String) valueValue); + var allowCustomChoiceValue = map.get("AllowCustomChoice"); + value.setAllowCustomChoice(allowCustomChoiceValue == null ? null : (Boolean) allowCustomChoiceValue); + var disabledValue = map.get("Disabled"); + value.setDisabled(disabledValue == null ? null : (Boolean) disabledValue); + var maxLengthValue = map.get("MaxLength"); + value.setMaxLength(maxLengthValue == null ? null : ((Number) maxLengthValue).doubleValue()); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("Label", AspireClient.serializeValue(label)); + map.put("Description", AspireClient.serializeValue(description)); + map.put("EnableDescriptionMarkdown", AspireClient.serializeValue(enableDescriptionMarkdown)); + map.put("Required", AspireClient.serializeValue(required)); + map.put("Placeholder", AspireClient.serializeValue(placeholder)); + map.put("Value", AspireClient.serializeValue(value)); + map.put("AllowCustomChoice", AspireClient.serializeValue(allowCustomChoice)); + map.put("Disabled", AspireClient.serializeValue(disabled)); + map.put("MaxLength", AspireClient.serializeValue(maxLength)); + return map; + } +} + // ===== DistributedApplication.java ===== // DistributedApplication.java - GENERATED CODE - DO NOT EDIT @@ -8951,6 +9095,42 @@ public DotnetToolResource withMergeRouteMiddleware(String path, String method, S } +// ===== DynamicLoadingOptions.java ===== +// DynamicLoadingOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** DynamicLoadingOptions DTO. */ +public class DynamicLoadingOptions implements JsonSerializable { + private Boolean alwaysLoadOnStart; + private String[] dependsOnInputs; + + public Boolean getAlwaysLoadOnStart() { return alwaysLoadOnStart; } + public void setAlwaysLoadOnStart(Boolean value) { this.alwaysLoadOnStart = value; } + public String[] getDependsOnInputs() { return dependsOnInputs; } + public void setDependsOnInputs(String[] value) { this.dependsOnInputs = value; } + + @SuppressWarnings("unchecked") + public static DynamicLoadingOptions fromMap(Map map) { + var value = new DynamicLoadingOptions(); + var alwaysLoadOnStartValue = map.get("AlwaysLoadOnStart"); + value.setAlwaysLoadOnStart(alwaysLoadOnStartValue == null ? null : (Boolean) alwaysLoadOnStartValue); + var dependsOnInputsValue = map.get("DependsOnInputs"); + value.setDependsOnInputs((String[]) dependsOnInputsValue); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("AlwaysLoadOnStart", AspireClient.serializeValue(alwaysLoadOnStart)); + map.put("DependsOnInputs", AspireClient.serializeValue(dependsOnInputs)); + return map; + } +} + // ===== EndpointProperty.java ===== // EndpointProperty.java - GENERATED CODE - DO NOT EDIT @@ -11188,6 +11368,14 @@ public class ExecuteCommandContext extends HandleWrapperBase { super(handle, client); } + /** The service provider. */ + public IServiceProvider serviceProvider() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + var result = getClient().invokeCapability("Aspire.Hosting.ApplicationModel/ExecuteCommandContext.serviceProvider", reqArgs); + return (IServiceProvider) result; + } + /** The resource name. */ public String resourceName() { Map reqArgs = new HashMap<>(); @@ -13590,136 +13778,395 @@ public boolean isEnvironment(String environmentName) { } -// ===== ILogger.java ===== -// ILogger.java - GENERATED CODE - DO NOT EDIT +// ===== IInteractionService.java ===== +// IInteractionService.java - GENERATED CODE - DO NOT EDIT package aspire; import java.util.*; import java.util.function.*; -/** Wrapper for Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger. */ -public class ILogger extends HandleWrapperBase { - ILogger(Handle handle, AspireClient client) { +/** Wrapper for Aspire.Hosting/Aspire.Hosting.IInteractionService. */ +public class IInteractionService extends HandleWrapperBase { + IInteractionService(Handle handle, AspireClient client) { super(handle, client); } - /** Logs an information message. */ - public void logInformation(String message) { + /** Gets a value indicating whether the interaction service is available to prompt the user. */ + public boolean isAvailable() { Map reqArgs = new HashMap<>(); - reqArgs.put("logger", AspireClient.serializeValue(getHandle())); - reqArgs.put("message", AspireClient.serializeValue(message)); - getClient().invokeCapability("Aspire.Hosting/logInformation", reqArgs); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + var result = getClient().invokeCapability("Aspire.Hosting/isAvailable", reqArgs); + return (Boolean) result; } - /** Logs a warning message. */ - public void logWarning(String message) { - Map reqArgs = new HashMap<>(); - reqArgs.put("logger", AspireClient.serializeValue(getHandle())); - reqArgs.put("message", AspireClient.serializeValue(message)); - getClient().invokeCapability("Aspire.Hosting/logWarning", reqArgs); + /** Prompts the user for confirmation with an OK/Cancel dialog. */ + public BoolInteractionResult promptConfirmation(String title, String message, PromptConfirmationOptions options) { + var options = options == null ? null : options.getOptions(); + var cancellationToken = options == null ? null : options.getCancellationToken(); + return promptConfirmationImpl(title, message, options, cancellationToken); } - /** Logs an error message. */ - public void logError(String message) { - Map reqArgs = new HashMap<>(); - reqArgs.put("logger", AspireClient.serializeValue(getHandle())); - reqArgs.put("message", AspireClient.serializeValue(message)); - getClient().invokeCapability("Aspire.Hosting/logError", reqArgs); + public BoolInteractionResult promptConfirmation(String title, String message) { + return promptConfirmation(title, message, null); } - /** Logs a debug message. */ - public void logDebug(String message) { + /** Prompts the user for confirmation with an OK/Cancel dialog. */ + private BoolInteractionResult promptConfirmationImpl(String title, String message, InteractionMessageBoxOptions options, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); - reqArgs.put("logger", AspireClient.serializeValue(getHandle())); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("title", AspireClient.serializeValue(title)); reqArgs.put("message", AspireClient.serializeValue(message)); - getClient().invokeCapability("Aspire.Hosting/logDebug", reqArgs); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + if (cancellationToken != null) { + reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); + } + var result = getClient().invokeCapability("Aspire.Hosting/promptConfirmation", reqArgs); + return BoolInteractionResult.fromMap((Map) result); } - /** Logs a message with a specified log level. */ - public void log(String level, String message) { - Map reqArgs = new HashMap<>(); - reqArgs.put("logger", AspireClient.serializeValue(getHandle())); - reqArgs.put("level", AspireClient.serializeValue(level)); - reqArgs.put("message", AspireClient.serializeValue(message)); - getClient().invokeCapability("Aspire.Hosting/log", reqArgs); + /** Prompts the user with a message box dialog. */ + public BoolInteractionResult promptMessageBox(String title, String message, PromptMessageBoxOptions options) { + var options = options == null ? null : options.getOptions(); + var cancellationToken = options == null ? null : options.getCancellationToken(); + return promptMessageBoxImpl(title, message, options, cancellationToken); } -} - -// ===== ILoggerFactory.java ===== -// ILoggerFactory.java - GENERATED CODE - DO NOT EDIT + public BoolInteractionResult promptMessageBox(String title, String message) { + return promptMessageBox(title, message, null); + } -package aspire; + /** Prompts the user with a message box dialog. */ + private BoolInteractionResult promptMessageBoxImpl(String title, String message, InteractionMessageBoxOptions options, CancellationToken cancellationToken) { + Map reqArgs = new HashMap<>(); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("title", AspireClient.serializeValue(title)); + reqArgs.put("message", AspireClient.serializeValue(message)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + if (cancellationToken != null) { + reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); + } + var result = getClient().invokeCapability("Aspire.Hosting/promptMessageBox", reqArgs); + return BoolInteractionResult.fromMap((Map) result); + } -import java.util.*; -import java.util.function.*; + /** Prompts the user with a notification. */ + public BoolInteractionResult promptNotification(String title, String message, PromptNotificationOptions options) { + var options = options == null ? null : options.getOptions(); + var cancellationToken = options == null ? null : options.getCancellationToken(); + return promptNotificationImpl(title, message, options, cancellationToken); + } -/** Wrapper for Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILoggerFactory. */ -public class ILoggerFactory extends HandleWrapperBase { - ILoggerFactory(Handle handle, AspireClient client) { - super(handle, client); + public BoolInteractionResult promptNotification(String title, String message) { + return promptNotification(title, message, null); } - /** Creates a logger for the specified category name. */ - public ILogger createLogger(String categoryName) { + /** Prompts the user with a notification. */ + private BoolInteractionResult promptNotificationImpl(String title, String message, InteractionNotificationOptions options, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); - reqArgs.put("loggerFactory", AspireClient.serializeValue(getHandle())); - reqArgs.put("categoryName", AspireClient.serializeValue(categoryName)); - var result = getClient().invokeCapability("Aspire.Hosting/createLogger", reqArgs); - return (ILogger) result; + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("title", AspireClient.serializeValue(title)); + reqArgs.put("message", AspireClient.serializeValue(message)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + if (cancellationToken != null) { + reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); + } + var result = getClient().invokeCapability("Aspire.Hosting/promptNotification", reqArgs); + return BoolInteractionResult.fromMap((Map) result); } -} - -// ===== IReportingStep.java ===== -// IReportingStep.java - GENERATED CODE - DO NOT EDIT - -package aspire; + /** Prompts the user for a single input. */ + public InputInteractionResult promptInput(String title, String message, InteractionInputBuilder input, PromptInputOptions options) { + var options = options == null ? null : options.getOptions(); + var cancellationToken = options == null ? null : options.getCancellationToken(); + return promptInputImpl(title, message, input, options, cancellationToken); + } -import java.util.*; -import java.util.function.*; + public InputInteractionResult promptInput(String title, String message, HandleWrapperBase input, PromptInputOptions options) { + return promptInput(title, message, new InteractionInputBuilder(input.getHandle(), input.getClient()), options); + } -/** Wrapper for Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingStep. */ -public class IReportingStep extends HandleWrapperBase { - IReportingStep(Handle handle, AspireClient client) { - super(handle, client); + public InputInteractionResult promptInput(String title, String message, InteractionInputBuilder input) { + return promptInput(title, message, input, null); } - public IReportingTask createTask(String statusText) { - return createTask(statusText, null); + public InputInteractionResult promptInput(String title, String message, HandleWrapperBase input) { + return promptInput(title, message, new InteractionInputBuilder(input.getHandle(), input.getClient())); } - /** Creates a reporting task with plain-text status text. */ - public IReportingTask createTask(String statusText, CancellationToken cancellationToken) { + /** Prompts the user for a single input. */ + private InputInteractionResult promptInputImpl(String title, String message, InteractionInputBuilder input, InteractionInputsDialogOptions options, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); - reqArgs.put("reportingStep", AspireClient.serializeValue(getHandle())); - reqArgs.put("statusText", AspireClient.serializeValue(statusText)); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("title", AspireClient.serializeValue(title)); + reqArgs.put("message", AspireClient.serializeValue(message)); + reqArgs.put("input", AspireClient.serializeValue(input)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } if (cancellationToken != null) { reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); } - var result = getClient().invokeCapability("Aspire.Hosting/createTask", reqArgs); - return (IReportingTask) result; + var result = getClient().invokeCapability("Aspire.Hosting/promptInput", reqArgs); + return InputInteractionResult.fromMap((Map) result); } - public IReportingTask createMarkdownTask(String markdownString) { - return createMarkdownTask(markdownString, null); + /** Prompts the user for multiple inputs. */ + public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs, PromptInputsOptions options) { + var options = options == null ? null : options.getOptions(); + var cancellationToken = options == null ? null : options.getCancellationToken(); + return promptInputsImpl(title, message, inputs, options, cancellationToken); } - /** Creates a reporting task with Markdown-formatted status text. */ - public IReportingTask createMarkdownTask(String markdownString, CancellationToken cancellationToken) { + public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs) { + return promptInputs(title, message, inputs, null); + } + + /** Prompts the user for multiple inputs. */ + private InputsInteractionResult promptInputsImpl(String title, String message, InteractionInputBuilder[] inputs, InteractionInputsDialogOptions options, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); - reqArgs.put("reportingStep", AspireClient.serializeValue(getHandle())); - reqArgs.put("markdownString", AspireClient.serializeValue(markdownString)); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("title", AspireClient.serializeValue(title)); + reqArgs.put("message", AspireClient.serializeValue(message)); + reqArgs.put("inputs", AspireClient.serializeValue(inputs)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } if (cancellationToken != null) { reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); } - var result = getClient().invokeCapability("Aspire.Hosting/createMarkdownTask", reqArgs); - return (IReportingTask) result; + var result = getClient().invokeCapability("Aspire.Hosting/promptInputs", reqArgs); + return InputsInteractionResult.fromMap((Map) result); } - /** Logs a plain-text message for the reporting step. */ - public void logStep(String level, String message) { + public InteractionInputBuilder createTextInput(String name) { + return createTextInput(name, null); + } + + /** Creates a single-line text input. */ + public InteractionInputBuilder createTextInput(String name, CreateInteractionInputOptions options) { + Map reqArgs = new HashMap<>(); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + var result = getClient().invokeCapability("Aspire.Hosting/createTextInput", reqArgs); + return (InteractionInputBuilder) result; + } + + public InteractionInputBuilder createSecretInput(String name) { + return createSecretInput(name, null); + } + + /** Creates a secret (masked) text input. */ + public InteractionInputBuilder createSecretInput(String name, CreateInteractionInputOptions options) { + Map reqArgs = new HashMap<>(); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + var result = getClient().invokeCapability("Aspire.Hosting/createSecretInput", reqArgs); + return (InteractionInputBuilder) result; + } + + public InteractionInputBuilder createBooleanInput(String name) { + return createBooleanInput(name, null); + } + + /** Creates a boolean (checkbox) input. */ + public InteractionInputBuilder createBooleanInput(String name, CreateInteractionInputOptions options) { + Map reqArgs = new HashMap<>(); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + var result = getClient().invokeCapability("Aspire.Hosting/createBooleanInput", reqArgs); + return (InteractionInputBuilder) result; + } + + public InteractionInputBuilder createNumberInput(String name) { + return createNumberInput(name, null); + } + + /** Creates a numeric input. */ + public InteractionInputBuilder createNumberInput(String name, CreateInteractionInputOptions options) { + Map reqArgs = new HashMap<>(); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + var result = getClient().invokeCapability("Aspire.Hosting/createNumberInput", reqArgs); + return (InteractionInputBuilder) result; + } + + /** Creates a choice input that selects from a list of options. */ + public InteractionInputBuilder createChoiceInput(String name, CreateChoiceInputOptions options) { + var choices = options == null ? null : options.getChoices(); + var options = options == null ? null : options.getOptions(); + return createChoiceInputImpl(name, choices, options); + } + + public InteractionInputBuilder createChoiceInput(String name) { + return createChoiceInput(name, null); + } + + /** Creates a choice input that selects from a list of options. */ + private InteractionInputBuilder createChoiceInputImpl(String name, Map choices, CreateInteractionInputOptions options) { + Map reqArgs = new HashMap<>(); + reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + if (choices != null) { + reqArgs.put("choices", AspireClient.serializeValue(choices)); + } + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + var result = getClient().invokeCapability("Aspire.Hosting/createChoiceInput", reqArgs); + return (InteractionInputBuilder) result; + } + +} + +// ===== ILogger.java ===== +// ILogger.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger. */ +public class ILogger extends HandleWrapperBase { + ILogger(Handle handle, AspireClient client) { + super(handle, client); + } + + /** Logs an information message. */ + public void logInformation(String message) { + Map reqArgs = new HashMap<>(); + reqArgs.put("logger", AspireClient.serializeValue(getHandle())); + reqArgs.put("message", AspireClient.serializeValue(message)); + getClient().invokeCapability("Aspire.Hosting/logInformation", reqArgs); + } + + /** Logs a warning message. */ + public void logWarning(String message) { + Map reqArgs = new HashMap<>(); + reqArgs.put("logger", AspireClient.serializeValue(getHandle())); + reqArgs.put("message", AspireClient.serializeValue(message)); + getClient().invokeCapability("Aspire.Hosting/logWarning", reqArgs); + } + + /** Logs an error message. */ + public void logError(String message) { + Map reqArgs = new HashMap<>(); + reqArgs.put("logger", AspireClient.serializeValue(getHandle())); + reqArgs.put("message", AspireClient.serializeValue(message)); + getClient().invokeCapability("Aspire.Hosting/logError", reqArgs); + } + + /** Logs a debug message. */ + public void logDebug(String message) { + Map reqArgs = new HashMap<>(); + reqArgs.put("logger", AspireClient.serializeValue(getHandle())); + reqArgs.put("message", AspireClient.serializeValue(message)); + getClient().invokeCapability("Aspire.Hosting/logDebug", reqArgs); + } + + /** Logs a message with a specified log level. */ + public void log(String level, String message) { + Map reqArgs = new HashMap<>(); + reqArgs.put("logger", AspireClient.serializeValue(getHandle())); + reqArgs.put("level", AspireClient.serializeValue(level)); + reqArgs.put("message", AspireClient.serializeValue(message)); + getClient().invokeCapability("Aspire.Hosting/log", reqArgs); + } + +} + +// ===== ILoggerFactory.java ===== +// ILoggerFactory.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILoggerFactory. */ +public class ILoggerFactory extends HandleWrapperBase { + ILoggerFactory(Handle handle, AspireClient client) { + super(handle, client); + } + + /** Creates a logger for the specified category name. */ + public ILogger createLogger(String categoryName) { + Map reqArgs = new HashMap<>(); + reqArgs.put("loggerFactory", AspireClient.serializeValue(getHandle())); + reqArgs.put("categoryName", AspireClient.serializeValue(categoryName)); + var result = getClient().invokeCapability("Aspire.Hosting/createLogger", reqArgs); + return (ILogger) result; + } + +} + +// ===== IReportingStep.java ===== +// IReportingStep.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingStep. */ +public class IReportingStep extends HandleWrapperBase { + IReportingStep(Handle handle, AspireClient client) { + super(handle, client); + } + + public IReportingTask createTask(String statusText) { + return createTask(statusText, null); + } + + /** Creates a reporting task with plain-text status text. */ + public IReportingTask createTask(String statusText, CancellationToken cancellationToken) { + Map reqArgs = new HashMap<>(); + reqArgs.put("reportingStep", AspireClient.serializeValue(getHandle())); + reqArgs.put("statusText", AspireClient.serializeValue(statusText)); + if (cancellationToken != null) { + reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); + } + var result = getClient().invokeCapability("Aspire.Hosting/createTask", reqArgs); + return (IReportingTask) result; + } + + public IReportingTask createMarkdownTask(String markdownString) { + return createMarkdownTask(markdownString, null); + } + + /** Creates a reporting task with Markdown-formatted status text. */ + public IReportingTask createMarkdownTask(String markdownString, CancellationToken cancellationToken) { + Map reqArgs = new HashMap<>(); + reqArgs.put("reportingStep", AspireClient.serializeValue(getHandle())); + reqArgs.put("markdownString", AspireClient.serializeValue(markdownString)); + if (cancellationToken != null) { + reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); + } + var result = getClient().invokeCapability("Aspire.Hosting/createMarkdownTask", reqArgs); + return (IReportingTask) result; + } + + /** Logs a plain-text message for the reporting step. */ + public void logStep(String level, String message) { Map reqArgs = new HashMap<>(); reqArgs.put("reportingStep", AspireClient.serializeValue(getHandle())); reqArgs.put("level", AspireClient.serializeValue(level)); @@ -14054,6 +14501,14 @@ public IDistributedApplicationEventing getEventing() { return (IDistributedApplicationEventing) result; } + /** Gets the interaction service from the service provider. */ + public IInteractionService getInteractionService() { + Map reqArgs = new HashMap<>(); + reqArgs.put("serviceProvider", AspireClient.serializeValue(getHandle())); + var result = getClient().invokeCapability("Aspire.Hosting/getInteractionService", reqArgs); + return (IInteractionService) result; + } + /** Gets the logger factory from the service provider. */ public ILoggerFactory getLoggerFactory() { Map reqArgs = new HashMap<>(); @@ -14324,6 +14779,42 @@ public IServiceProvider services() { } +// ===== InputInteractionResult.java ===== +// InputInteractionResult.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** InputInteractionResult DTO. */ +public class InputInteractionResult implements JsonSerializable { + private boolean canceled; + private InteractionInput input; + + public boolean getCanceled() { return canceled; } + public void setCanceled(boolean value) { this.canceled = value; } + public InteractionInput getInput() { return input; } + public void setInput(InteractionInput value) { this.input = value; } + + @SuppressWarnings("unchecked") + public static InputInteractionResult fromMap(Map map) { + var value = new InputInteractionResult(); + var canceledValue = map.get("Canceled"); + value.setCanceled((Boolean) canceledValue); + var inputValue = map.get("Input"); + value.setInput(inputValue == null ? null : InteractionInput.fromMap((Map) inputValue)); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("Canceled", AspireClient.serializeValue(canceled)); + map.put("Input", AspireClient.serializeValue(input)); + return map; + } +} + // ===== InputType.java ===== // InputType.java - GENERATED CODE - DO NOT EDIT @@ -14397,6 +14888,42 @@ public void addValidationError(String inputName, String errorMessage) { } +// ===== InputsInteractionResult.java ===== +// InputsInteractionResult.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** InputsInteractionResult DTO. */ +public class InputsInteractionResult implements JsonSerializable { + private boolean canceled; + private InteractionInput[] inputs; + + public boolean getCanceled() { return canceled; } + public void setCanceled(boolean value) { this.canceled = value; } + public InteractionInput[] getInputs() { return inputs; } + public void setInputs(InteractionInput[] value) { this.inputs = value; } + + @SuppressWarnings("unchecked") + public static InputsInteractionResult fromMap(Map map) { + var value = new InputsInteractionResult(); + var canceledValue = map.get("Canceled"); + value.setCanceled((Boolean) canceledValue); + var inputsValue = map.get("Inputs"); + value.setInputs((InteractionInput[]) inputsValue); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("Canceled", AspireClient.serializeValue(canceled)); + map.put("Inputs", AspireClient.serializeValue(inputs)); + return map; + } +} + // ===== InteractionInput.java ===== // InteractionInput.java - GENERATED CODE - DO NOT EDIT @@ -14499,6 +15026,63 @@ public Map toMap() { } } +// ===== InteractionInputBuilder.java ===== +// InteractionInputBuilder.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder. */ +public class InteractionInputBuilder extends HandleWrapperBase { + InteractionInputBuilder(Handle handle, AspireClient client) { + super(handle, client); + } + + /** Sets the choice options for the input. */ + public InteractionInputBuilder withChoiceOptions(Map choices) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + reqArgs.put("choices", AspireClient.serializeValue(choices)); + var result = getClient().invokeCapability("Aspire.Hosting.Ats/withChoiceOptions", reqArgs); + return (InteractionInputBuilder) result; + } + + /** Sets the value of the input. */ + public InteractionInputBuilder withValue(String value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + reqArgs.put("value", AspireClient.serializeValue(value)); + var result = getClient().invokeCapability("Aspire.Hosting.Ats/withValue", reqArgs); + return (InteractionInputBuilder) result; + } + + public InteractionInputBuilder withDynamicLoading(AspireAction1 callback) { + return withDynamicLoading(callback, null); + } + + /** Attaches a callback that dynamically loads or updates the input after the prompt starts. */ + public InteractionInputBuilder withDynamicLoading(AspireAction1 callback, DynamicLoadingOptions options) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + var callbackId = getClient().registerCallback(args -> { + var arg = (InteractionInputLoadContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + if (options != null) { + reqArgs.put("options", AspireClient.serializeValue(options)); + } + var result = getClient().invokeCapability("Aspire.Hosting.Ats/withDynamicLoading", reqArgs); + return (InteractionInputBuilder) result; + } + +} + // ===== InteractionInputCollection.java ===== // InteractionInputCollection.java - GENERATED CODE - DO NOT EDIT @@ -14523,6 +15107,241 @@ public InteractionInput[] toArray() { } +// ===== InteractionInputLoadContext.java ===== +// InteractionInputLoadContext.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext. */ +public class InteractionInputLoadContext extends HandleWrapperBase { + InteractionInputLoadContext(Handle handle, AspireClient client) { + super(handle, client); + } + + /** Gets the name of the input that is loading. */ + public String getInputName() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + var result = getClient().invokeCapability("Aspire.Hosting.Ats/getInputName", reqArgs); + return (String) result; + } + + /** Gets the current value of an input in the prompt by name. */ + public String getInputValue(String inputName) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + reqArgs.put("inputName", AspireClient.serializeValue(inputName)); + var result = getClient().invokeCapability("Aspire.Hosting.Ats/getInputValue", reqArgs); + return (String) result; + } + + /** Sets the choice options for the loading input. */ + public void setChoiceOptions(Map choices) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + reqArgs.put("choices", AspireClient.serializeValue(choices)); + getClient().invokeCapability("Aspire.Hosting.Ats/setChoiceOptions", reqArgs); + } + + /** Sets the value of the loading input. */ + public void setValue(String value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + reqArgs.put("value", AspireClient.serializeValue(value)); + getClient().invokeCapability("Aspire.Hosting.Ats/setValue", reqArgs); + } + +} + +// ===== InteractionInputsDialogOptions.java ===== +// InteractionInputsDialogOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** InteractionInputsDialogOptions DTO. */ +public class InteractionInputsDialogOptions implements JsonSerializable { + private String primaryButtonText; + private String secondaryButtonText; + private Boolean showSecondaryButton; + private Boolean showDismiss; + private Boolean enableMessageMarkdown; + + public String getPrimaryButtonText() { return primaryButtonText; } + public void setPrimaryButtonText(String value) { this.primaryButtonText = value; } + public String getSecondaryButtonText() { return secondaryButtonText; } + public void setSecondaryButtonText(String value) { this.secondaryButtonText = value; } + public Boolean getShowSecondaryButton() { return showSecondaryButton; } + public void setShowSecondaryButton(Boolean value) { this.showSecondaryButton = value; } + public Boolean getShowDismiss() { return showDismiss; } + public void setShowDismiss(Boolean value) { this.showDismiss = value; } + public Boolean getEnableMessageMarkdown() { return enableMessageMarkdown; } + public void setEnableMessageMarkdown(Boolean value) { this.enableMessageMarkdown = value; } + + @SuppressWarnings("unchecked") + public static InteractionInputsDialogOptions fromMap(Map map) { + var value = new InteractionInputsDialogOptions(); + var primaryButtonTextValue = map.get("PrimaryButtonText"); + value.setPrimaryButtonText(primaryButtonTextValue == null ? null : (String) primaryButtonTextValue); + var secondaryButtonTextValue = map.get("SecondaryButtonText"); + value.setSecondaryButtonText(secondaryButtonTextValue == null ? null : (String) secondaryButtonTextValue); + var showSecondaryButtonValue = map.get("ShowSecondaryButton"); + value.setShowSecondaryButton(showSecondaryButtonValue == null ? null : (Boolean) showSecondaryButtonValue); + var showDismissValue = map.get("ShowDismiss"); + value.setShowDismiss(showDismissValue == null ? null : (Boolean) showDismissValue); + var enableMessageMarkdownValue = map.get("EnableMessageMarkdown"); + value.setEnableMessageMarkdown(enableMessageMarkdownValue == null ? null : (Boolean) enableMessageMarkdownValue); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("PrimaryButtonText", AspireClient.serializeValue(primaryButtonText)); + map.put("SecondaryButtonText", AspireClient.serializeValue(secondaryButtonText)); + map.put("ShowSecondaryButton", AspireClient.serializeValue(showSecondaryButton)); + map.put("ShowDismiss", AspireClient.serializeValue(showDismiss)); + map.put("EnableMessageMarkdown", AspireClient.serializeValue(enableMessageMarkdown)); + return map; + } +} + +// ===== InteractionMessageBoxOptions.java ===== +// InteractionMessageBoxOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** InteractionMessageBoxOptions DTO. */ +public class InteractionMessageBoxOptions implements JsonSerializable { + private String primaryButtonText; + private String secondaryButtonText; + private Boolean showSecondaryButton; + private Boolean showDismiss; + private Boolean enableMessageMarkdown; + private MessageIntent intent; + + public String getPrimaryButtonText() { return primaryButtonText; } + public void setPrimaryButtonText(String value) { this.primaryButtonText = value; } + public String getSecondaryButtonText() { return secondaryButtonText; } + public void setSecondaryButtonText(String value) { this.secondaryButtonText = value; } + public Boolean getShowSecondaryButton() { return showSecondaryButton; } + public void setShowSecondaryButton(Boolean value) { this.showSecondaryButton = value; } + public Boolean getShowDismiss() { return showDismiss; } + public void setShowDismiss(Boolean value) { this.showDismiss = value; } + public Boolean getEnableMessageMarkdown() { return enableMessageMarkdown; } + public void setEnableMessageMarkdown(Boolean value) { this.enableMessageMarkdown = value; } + public MessageIntent getIntent() { return intent; } + public void setIntent(MessageIntent value) { this.intent = value; } + + @SuppressWarnings("unchecked") + public static InteractionMessageBoxOptions fromMap(Map map) { + var value = new InteractionMessageBoxOptions(); + var primaryButtonTextValue = map.get("PrimaryButtonText"); + value.setPrimaryButtonText(primaryButtonTextValue == null ? null : (String) primaryButtonTextValue); + var secondaryButtonTextValue = map.get("SecondaryButtonText"); + value.setSecondaryButtonText(secondaryButtonTextValue == null ? null : (String) secondaryButtonTextValue); + var showSecondaryButtonValue = map.get("ShowSecondaryButton"); + value.setShowSecondaryButton(showSecondaryButtonValue == null ? null : (Boolean) showSecondaryButtonValue); + var showDismissValue = map.get("ShowDismiss"); + value.setShowDismiss(showDismissValue == null ? null : (Boolean) showDismissValue); + var enableMessageMarkdownValue = map.get("EnableMessageMarkdown"); + value.setEnableMessageMarkdown(enableMessageMarkdownValue == null ? null : (Boolean) enableMessageMarkdownValue); + var intentValue = map.get("Intent"); + value.setIntent(intentValue == null ? null : MessageIntent.fromValue((String) intentValue)); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("PrimaryButtonText", AspireClient.serializeValue(primaryButtonText)); + map.put("SecondaryButtonText", AspireClient.serializeValue(secondaryButtonText)); + map.put("ShowSecondaryButton", AspireClient.serializeValue(showSecondaryButton)); + map.put("ShowDismiss", AspireClient.serializeValue(showDismiss)); + map.put("EnableMessageMarkdown", AspireClient.serializeValue(enableMessageMarkdown)); + map.put("Intent", AspireClient.serializeValue(intent)); + return map; + } +} + +// ===== InteractionNotificationOptions.java ===== +// InteractionNotificationOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** InteractionNotificationOptions DTO. */ +public class InteractionNotificationOptions implements JsonSerializable { + private String primaryButtonText; + private String secondaryButtonText; + private Boolean showSecondaryButton; + private Boolean showDismiss; + private Boolean enableMessageMarkdown; + private MessageIntent intent; + private String linkText; + private String linkUrl; + + public String getPrimaryButtonText() { return primaryButtonText; } + public void setPrimaryButtonText(String value) { this.primaryButtonText = value; } + public String getSecondaryButtonText() { return secondaryButtonText; } + public void setSecondaryButtonText(String value) { this.secondaryButtonText = value; } + public Boolean getShowSecondaryButton() { return showSecondaryButton; } + public void setShowSecondaryButton(Boolean value) { this.showSecondaryButton = value; } + public Boolean getShowDismiss() { return showDismiss; } + public void setShowDismiss(Boolean value) { this.showDismiss = value; } + public Boolean getEnableMessageMarkdown() { return enableMessageMarkdown; } + public void setEnableMessageMarkdown(Boolean value) { this.enableMessageMarkdown = value; } + public MessageIntent getIntent() { return intent; } + public void setIntent(MessageIntent value) { this.intent = value; } + public String getLinkText() { return linkText; } + public void setLinkText(String value) { this.linkText = value; } + public String getLinkUrl() { return linkUrl; } + public void setLinkUrl(String value) { this.linkUrl = value; } + + @SuppressWarnings("unchecked") + public static InteractionNotificationOptions fromMap(Map map) { + var value = new InteractionNotificationOptions(); + var primaryButtonTextValue = map.get("PrimaryButtonText"); + value.setPrimaryButtonText(primaryButtonTextValue == null ? null : (String) primaryButtonTextValue); + var secondaryButtonTextValue = map.get("SecondaryButtonText"); + value.setSecondaryButtonText(secondaryButtonTextValue == null ? null : (String) secondaryButtonTextValue); + var showSecondaryButtonValue = map.get("ShowSecondaryButton"); + value.setShowSecondaryButton(showSecondaryButtonValue == null ? null : (Boolean) showSecondaryButtonValue); + var showDismissValue = map.get("ShowDismiss"); + value.setShowDismiss(showDismissValue == null ? null : (Boolean) showDismissValue); + var enableMessageMarkdownValue = map.get("EnableMessageMarkdown"); + value.setEnableMessageMarkdown(enableMessageMarkdownValue == null ? null : (Boolean) enableMessageMarkdownValue); + var intentValue = map.get("Intent"); + value.setIntent(intentValue == null ? null : MessageIntent.fromValue((String) intentValue)); + var linkTextValue = map.get("LinkText"); + value.setLinkText(linkTextValue == null ? null : (String) linkTextValue); + var linkUrlValue = map.get("LinkUrl"); + value.setLinkUrl(linkUrlValue == null ? null : (String) linkUrlValue); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("PrimaryButtonText", AspireClient.serializeValue(primaryButtonText)); + map.put("SecondaryButtonText", AspireClient.serializeValue(secondaryButtonText)); + map.put("ShowSecondaryButton", AspireClient.serializeValue(showSecondaryButton)); + map.put("ShowDismiss", AspireClient.serializeValue(showDismiss)); + map.put("EnableMessageMarkdown", AspireClient.serializeValue(enableMessageMarkdown)); + map.put("Intent", AspireClient.serializeValue(intent)); + map.put("LinkText", AspireClient.serializeValue(linkText)); + map.put("LinkUrl", AspireClient.serializeValue(linkUrl)); + return map; + } +} + // ===== JsonSerializable.java ===== // JsonSerializable.java - GENERATED CODE - DO NOT EDIT @@ -14588,6 +15407,39 @@ public void debug(String message) { } +// ===== MessageIntent.java ===== +// MessageIntent.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** MessageIntent enum. */ +public enum MessageIntent implements WireValueEnum { + NONE("None"), + SUCCESS("Success"), + WARNING("Warning"), + ERROR("Error"), + INFORMATION("Information"), + CONFIRMATION("Confirmation"); + + private final String value; + + MessageIntent(String value) { + this.value = value; + } + + public String getValue() { return value; } + + public static MessageIntent fromValue(String value) { + for (MessageIntent e : values()) { + if (e.value.equals(value)) return e; + } + throw new IllegalArgumentException("Unknown value: " + value); + } +} + // ===== OtlpProtocol.java ===== // OtlpProtocol.java - GENERATED CODE - DO NOT EDIT @@ -17767,6 +18619,141 @@ public ProjectResourceOptions setExcludeKestrelEndpoints(boolean value) { } +// ===== PromptConfirmationOptions.java ===== +// PromptConfirmationOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Options for PromptConfirmation. */ +public final class PromptConfirmationOptions { + private InteractionMessageBoxOptions options; + private CancellationToken cancellationToken; + + public InteractionMessageBoxOptions getOptions() { return options; } + public PromptConfirmationOptions options(InteractionMessageBoxOptions value) { + this.options = value; + return this; + } + + public CancellationToken getCancellationToken() { return cancellationToken; } + public PromptConfirmationOptions cancellationToken(CancellationToken value) { + this.cancellationToken = value; + return this; + } + +} + +// ===== PromptInputOptions.java ===== +// PromptInputOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Options for PromptInput. */ +public final class PromptInputOptions { + private InteractionInputsDialogOptions options; + private CancellationToken cancellationToken; + + public InteractionInputsDialogOptions getOptions() { return options; } + public PromptInputOptions options(InteractionInputsDialogOptions value) { + this.options = value; + return this; + } + + public CancellationToken getCancellationToken() { return cancellationToken; } + public PromptInputOptions cancellationToken(CancellationToken value) { + this.cancellationToken = value; + return this; + } + +} + +// ===== PromptInputsOptions.java ===== +// PromptInputsOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Options for PromptInputs. */ +public final class PromptInputsOptions { + private InteractionInputsDialogOptions options; + private CancellationToken cancellationToken; + + public InteractionInputsDialogOptions getOptions() { return options; } + public PromptInputsOptions options(InteractionInputsDialogOptions value) { + this.options = value; + return this; + } + + public CancellationToken getCancellationToken() { return cancellationToken; } + public PromptInputsOptions cancellationToken(CancellationToken value) { + this.cancellationToken = value; + return this; + } + +} + +// ===== PromptMessageBoxOptions.java ===== +// PromptMessageBoxOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Options for PromptMessageBox. */ +public final class PromptMessageBoxOptions { + private InteractionMessageBoxOptions options; + private CancellationToken cancellationToken; + + public InteractionMessageBoxOptions getOptions() { return options; } + public PromptMessageBoxOptions options(InteractionMessageBoxOptions value) { + this.options = value; + return this; + } + + public CancellationToken getCancellationToken() { return cancellationToken; } + public PromptMessageBoxOptions cancellationToken(CancellationToken value) { + this.cancellationToken = value; + return this; + } + +} + +// ===== PromptNotificationOptions.java ===== +// PromptNotificationOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Options for PromptNotification. */ +public final class PromptNotificationOptions { + private InteractionNotificationOptions options; + private CancellationToken cancellationToken; + + public InteractionNotificationOptions getOptions() { return options; } + public PromptNotificationOptions options(InteractionNotificationOptions value) { + this.options = value; + return this; + } + + public CancellationToken getCancellationToken() { return cancellationToken; } + public PromptNotificationOptions cancellationToken(CancellationToken value) { + this.cancellationToken = value; + return this; + } + +} + // ===== ProtocolType.java ===== // ProtocolType.java - GENERATED CODE - DO NOT EDIT @@ -25830,6 +26817,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/BeforePublishEvent.java .aspire/modules/BeforeResourceStartedEvent.java .aspire/modules/BeforeStartEvent.java +.aspire/modules/BoolInteractionResult.java .aspire/modules/BuildOptions.java .aspire/modules/CSharpAppResource.java .aspire/modules/CancellationToken.java @@ -25858,6 +26846,8 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/ContainerRegistryResource.java .aspire/modules/ContainerResource.java .aspire/modules/CreateBuilderOptions.java +.aspire/modules/CreateChoiceInputOptions.java +.aspire/modules/CreateInteractionInputOptions.java .aspire/modules/DistributedApplication.java .aspire/modules/DistributedApplicationEventSubscription.java .aspire/modules/DistributedApplicationExecutionContext.java @@ -25870,6 +26860,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/DockerfileFactoryContext.java .aspire/modules/DockerfileStage.java .aspire/modules/DotnetToolResource.java +.aspire/modules/DynamicLoadingOptions.java .aspire/modules/EndpointProperty.java .aspire/modules/EndpointReference.java .aspire/modules/EndpointReferenceExpression.java @@ -25906,6 +26897,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/IExecutionConfigurationResult.java .aspire/modules/IExpressionValue.java .aspire/modules/IHostEnvironment.java +.aspire/modules/IInteractionService.java .aspire/modules/ILogger.java .aspire/modules/ILoggerFactory.java .aspire/modules/IReportingStep.java @@ -25924,12 +26916,20 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/IconVariant.java .aspire/modules/ImagePullPolicy.java .aspire/modules/InitializeResourceEvent.java +.aspire/modules/InputInteractionResult.java .aspire/modules/InputType.java .aspire/modules/InputsDialogValidationContext.java +.aspire/modules/InputsInteractionResult.java .aspire/modules/InteractionInput.java +.aspire/modules/InteractionInputBuilder.java .aspire/modules/InteractionInputCollection.java +.aspire/modules/InteractionInputLoadContext.java +.aspire/modules/InteractionInputsDialogOptions.java +.aspire/modules/InteractionMessageBoxOptions.java +.aspire/modules/InteractionNotificationOptions.java .aspire/modules/JsonSerializable.java .aspire/modules/LogFacade.java +.aspire/modules/MessageIntent.java .aspire/modules/OtlpProtocol.java .aspire/modules/ParameterCustomInputOptions.java .aspire/modules/ParameterResource.java @@ -25946,6 +26946,11 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/ProcessCommandSpecExportData.java .aspire/modules/ProjectResource.java .aspire/modules/ProjectResourceOptions.java +.aspire/modules/PromptConfirmationOptions.java +.aspire/modules/PromptInputOptions.java +.aspire/modules/PromptInputsOptions.java +.aspire/modules/PromptMessageBoxOptions.java +.aspire/modules/PromptNotificationOptions.java .aspire/modules/ProtocolType.java .aspire/modules/PublishResourceUpdateOptions.java .aspire/modules/ReferenceEnvironmentInjectionOptions.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 1c6a47e43f8..72bcb60dbf6 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1526,6 +1526,8 @@ def _validate_dict_types(args: typing.Any, arg_types: typing.Any) -> bool: InputType = typing.Literal["Text", "SecretText", "Choice", "Boolean", "Number"] +MessageIntent = typing.Literal["None", "Success", "Warning", "Error", "Information", "Confirmation"] + OtlpProtocol = typing.Literal["Grpc", "HttpProtobuf", "HttpJson"] ProbeType = typing.Literal["Startup", "Readiness", "Liveness"] @@ -1743,6 +1745,10 @@ class AddContainerOptions(typing.TypedDict, total=False): Image: str Tag: str | None +class BoolInteractionResult(typing.TypedDict, total=False): + Canceled: bool + Value: bool + class CertificateTrustExecutionConfigurationContext(typing.TypedDict, total=False): CertificateBundlePath: ReferenceExpression CertificateDirectoriesPath: ReferenceExpression @@ -1786,6 +1792,21 @@ class CreateBuilderOptions(typing.TypedDict, total=False): AllowUnsecuredTransport: bool EnableResourceLogging: bool +class CreateInteractionInputOptions(typing.TypedDict, total=False): + Label: str | None + Description: str | None + EnableDescriptionMarkdown: bool | None + Required: bool | None + Placeholder: str | None + Value: str | None + AllowCustomChoice: bool | None + Disabled: bool | None + MaxLength: int | None + +class DynamicLoadingOptions(typing.TypedDict, total=False): + AlwaysLoadOnStart: bool | None + DependsOnInputs: typing.Iterable[str] + class ExecuteCommandResult(typing.TypedDict, total=False): Success: bool Canceled: bool @@ -1834,6 +1855,14 @@ class HttpsCertificateInfo(typing.TypedDict, total=False): Issuer: str Thumbprint: str | None +class InputInteractionResult(typing.TypedDict, total=False): + Canceled: bool + Input: InteractionInput + +class InputsInteractionResult(typing.TypedDict, total=False): + Canceled: bool + Inputs: typing.Iterable[InteractionInput] + class InteractionInput(typing.TypedDict, total=False): Name: str Label: str | None @@ -1849,6 +1878,31 @@ class InteractionInput(typing.TypedDict, total=False): Disabled: bool MaxLength: int | None +class InteractionInputsDialogOptions(typing.TypedDict, total=False): + PrimaryButtonText: str | None + SecondaryButtonText: str | None + ShowSecondaryButton: bool | None + ShowDismiss: bool | None + EnableMessageMarkdown: bool | None + +class InteractionMessageBoxOptions(typing.TypedDict, total=False): + PrimaryButtonText: str | None + SecondaryButtonText: str | None + ShowSecondaryButton: bool | None + ShowDismiss: bool | None + EnableMessageMarkdown: bool | None + Intent: MessageIntent | None + +class InteractionNotificationOptions(typing.TypedDict, total=False): + PrimaryButtonText: str | None + SecondaryButtonText: str | None + ShowSecondaryButton: bool | None + ShowDismiss: bool | None + EnableMessageMarkdown: bool | None + Intent: MessageIntent | None + LinkText: str | None + LinkUrl: str | None + class ParameterCustomInputOptions(typing.TypedDict, total=False): InputType: InputType | None Label: str | None @@ -2828,6 +2882,170 @@ def is_env(self, env_name: str) -> bool: return result +class AbstractInteractionService: + """Type class for AbstractInteractionService.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"AbstractInteractionService(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + def is_available(self) -> bool: + """Gets a value indicating whether the interaction service is available to prompt the user.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/isAvailable', + rpc_args, + ) + return result + + def prompt_confirmation(self, title: str, message: str, *, options: InteractionMessageBoxOptions | None = None, timeout: int | None = None) -> BoolInteractionResult: + """Prompts the user for confirmation with an OK/Cancel dialog.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['title'] = title + rpc_args['message'] = message + if options is not None: + rpc_args['options'] = options + if timeout is not None: + rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) + result = self._client.invoke_capability( + 'Aspire.Hosting/promptConfirmation', + rpc_args, + ) + return typing.cast(BoolInteractionResult, result) + + def prompt_message_box(self, title: str, message: str, *, options: InteractionMessageBoxOptions | None = None, timeout: int | None = None) -> BoolInteractionResult: + """Prompts the user with a message box dialog.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['title'] = title + rpc_args['message'] = message + if options is not None: + rpc_args['options'] = options + if timeout is not None: + rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) + result = self._client.invoke_capability( + 'Aspire.Hosting/promptMessageBox', + rpc_args, + ) + return typing.cast(BoolInteractionResult, result) + + def prompt_notification(self, title: str, message: str, *, options: InteractionNotificationOptions | None = None, timeout: int | None = None) -> BoolInteractionResult: + """Prompts the user with a notification.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['title'] = title + rpc_args['message'] = message + if options is not None: + rpc_args['options'] = options + if timeout is not None: + rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) + result = self._client.invoke_capability( + 'Aspire.Hosting/promptNotification', + rpc_args, + ) + return typing.cast(BoolInteractionResult, result) + + def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, *, options: InteractionInputsDialogOptions | None = None, timeout: int | None = None) -> InputInteractionResult: + """Prompts the user for a single input.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['title'] = title + rpc_args['message'] = message + rpc_args['input'] = input + if options is not None: + rpc_args['options'] = options + if timeout is not None: + rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) + result = self._client.invoke_capability( + 'Aspire.Hosting/promptInput', + rpc_args, + ) + return typing.cast(InputInteractionResult, result) + + def prompt_inputs(self, title: str, message: str, inputs: typing.Iterable[InteractionInputBuilder], *, options: InteractionInputsDialogOptions | None = None, timeout: int | None = None) -> InputsInteractionResult: + """Prompts the user for multiple inputs.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['title'] = title + rpc_args['message'] = message + rpc_args['inputs'] = inputs + if options is not None: + rpc_args['options'] = options + if timeout is not None: + rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) + result = self._client.invoke_capability( + 'Aspire.Hosting/promptInputs', + rpc_args, + ) + return typing.cast(InputsInteractionResult, result) + + def create_text_input(self, name: str, *, options: CreateInteractionInputOptions | None = None) -> InteractionInputBuilder: + """Creates a single-line text input.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['name'] = name + if options is not None: + rpc_args['options'] = options + result = self._client.invoke_capability( + 'Aspire.Hosting/createTextInput', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + def create_secret_input(self, name: str, *, options: CreateInteractionInputOptions | None = None) -> InteractionInputBuilder: + """Creates a secret (masked) text input.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['name'] = name + if options is not None: + rpc_args['options'] = options + result = self._client.invoke_capability( + 'Aspire.Hosting/createSecretInput', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + def create_boolean_input(self, name: str, *, options: CreateInteractionInputOptions | None = None) -> InteractionInputBuilder: + """Creates a boolean (checkbox) input.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['name'] = name + if options is not None: + rpc_args['options'] = options + result = self._client.invoke_capability( + 'Aspire.Hosting/createBooleanInput', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + def create_number_input(self, name: str, *, options: CreateInteractionInputOptions | None = None) -> InteractionInputBuilder: + """Creates a numeric input.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['name'] = name + if options is not None: + rpc_args['options'] = options + result = self._client.invoke_capability( + 'Aspire.Hosting/createNumberInput', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + def create_choice_input(self, name: str, *, choices: typing.Mapping[str, str] | None = None, options: CreateInteractionInputOptions | None = None) -> InteractionInputBuilder: + """Creates a choice input that selects from a list of options.""" + rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} + rpc_args['name'] = name + if choices is not None: + rpc_args['choices'] = choices + if options is not None: + rpc_args['options'] = options + result = self._client.invoke_capability( + 'Aspire.Hosting/createChoiceInput', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + class AbstractLogger: """Type class for AbstractLogger.""" @@ -3103,6 +3321,15 @@ def get_eventing(self) -> AbstractDistributedApplicationEventing: ) return typing.cast(AbstractDistributedApplicationEventing, result) + def get_interaction_service(self) -> AbstractInteractionService: + """Gets the interaction service from the service provider.""" + rpc_args: dict[str, typing.Any] = {'serviceProvider': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/getInteractionService', + rpc_args, + ) + return typing.cast(AbstractInteractionService, result) + def get_logger_factory(self) -> AbstractLoggerFactory: """Gets the logger factory from the service provider.""" rpc_args: dict[str, typing.Any] = {'serviceProvider': self._handle} @@ -4767,6 +4994,15 @@ def handle(self) -> Handle: """The underlying object reference handle.""" return self._handle + @_cached_property + def service_provider(self) -> AbstractServiceProvider: + """The service provider.""" + result = self._client.invoke_capability( + 'Aspire.Hosting.ApplicationModel/ExecuteCommandContext.serviceProvider', + {'context': self._handle} + ) + return typing.cast(AbstractServiceProvider, result) + @_cached_property def resource_name(self) -> str: """The resource name.""" @@ -4907,6 +5143,54 @@ def add_validation_error(self, input_name: str, error_message: str) -> None: ) +class InteractionInputBuilder: + """Type class for InteractionInputBuilder.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"InteractionInputBuilder(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + def with_choice_options(self, choices: typing.Mapping[str, str]) -> InteractionInputBuilder: + """Sets the choice options for the input.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + rpc_args['choices'] = choices + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/withChoiceOptions', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + def with_value(self, value: str) -> InteractionInputBuilder: + """Sets the value of the input.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + rpc_args['value'] = value + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/withValue', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + def with_dynamic_loading(self, callback: typing.Callable[[InteractionInputLoadContext], None], *, options: DynamicLoadingOptions | None = None) -> InteractionInputBuilder: + """Attaches a callback that dynamically loads or updates the input after the prompt starts.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + rpc_args['callback'] = self._client.register_callback(callback) + if options is not None: + rpc_args['options'] = options + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/withDynamicLoading', + rpc_args, + ) + return typing.cast(InteractionInputBuilder, result) + + class InteractionInputCollection: """Type class for InteractionInputCollection.""" @@ -4932,6 +5216,59 @@ def to_array(self) -> typing.Iterable[InteractionInput]: return result +class InteractionInputLoadContext: + """Type class for InteractionInputLoadContext.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"InteractionInputLoadContext(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + def get_input_name(self) -> str: + """Gets the name of the input that is loading.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/getInputName', + rpc_args, + ) + return result + + def get_input_value(self, input_name: str) -> str: + """Gets the current value of an input in the prompt by name.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + rpc_args['inputName'] = input_name + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/getInputValue', + rpc_args, + ) + return result + + def set_choice_options(self, choices: typing.Mapping[str, str]) -> None: + """Sets the choice options for the loading input.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + rpc_args['choices'] = choices + self._client.invoke_capability( + 'Aspire.Hosting.Ats/setChoiceOptions', + rpc_args + ) + + def set_value(self, value: str) -> None: + """Sets the value of the loading input.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + rpc_args['value'] = value + self._client.invoke_capability( + 'Aspire.Hosting.Ats/setValue', + rpc_args + ) + + class LogFacade: """Type class for LogFacade.""" @@ -11500,6 +11837,7 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExecutionConfigurationBuilder", AbstractExecutionConfigurationBuilder) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExecutionConfigurationResult", AbstractExecutionConfigurationResult) _register_handle_wrapper("Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.IHostEnvironment", AbstractHostEnvironment) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.IInteractionService", AbstractInteractionService) _register_handle_wrapper("Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger", AbstractLogger) _register_handle_wrapper("Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILoggerFactory", AbstractLoggerFactory) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingStep", AbstractReportingStep) @@ -11536,7 +11874,9 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecuteCommandContext", ExecuteCommandContext) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.InitializeResourceEvent", InitializeResourceEvent) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext", InputsDialogValidationContext) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", InteractionInputBuilder) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.InteractionInputCollection", InteractionInputCollection) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", InteractionInputLoadContext) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade", LogFacade) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineConfigurationContext", PipelineConfigurationContext) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineContext", PipelineContext) diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 9c369a36885..f99678d7cd9 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -362,6 +362,37 @@ impl std::fmt::Display for InputType { } } +/// MessageIntent +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum MessageIntent { + #[default] + #[serde(rename = "None")] + None, + #[serde(rename = "Success")] + Success, + #[serde(rename = "Warning")] + Warning, + #[serde(rename = "Error")] + Error, + #[serde(rename = "Information")] + Information, + #[serde(rename = "Confirmation")] + Confirmation, +} + +impl std::fmt::Display for MessageIntent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => write!(f, "None"), + Self::Success => write!(f, "Success"), + Self::Warning => write!(f, "Warning"), + Self::Error => write!(f, "Error"), + Self::Information => write!(f, "Information"), + Self::Confirmation => write!(f, "Confirmation"), + } + } +} + /// ResourceCommandVisibility #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ResourceCommandVisibility { @@ -776,6 +807,250 @@ impl HttpsCertificateExecutionConfigurationExportData { } } +/// CreateInteractionInputOptions +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CreateInteractionInputOptions { + #[serde(rename = "Label")] + pub label: String, + #[serde(rename = "Description")] + pub description: String, + #[serde(rename = "EnableDescriptionMarkdown", skip_serializing_if = "Option::is_none")] + pub enable_description_markdown: Option, + #[serde(rename = "Required", skip_serializing_if = "Option::is_none")] + pub required: Option, + #[serde(rename = "Placeholder")] + pub placeholder: String, + #[serde(rename = "Value")] + pub value: String, + #[serde(rename = "AllowCustomChoice", skip_serializing_if = "Option::is_none")] + pub allow_custom_choice: Option, + #[serde(rename = "Disabled", skip_serializing_if = "Option::is_none")] + pub disabled: Option, + #[serde(rename = "MaxLength", skip_serializing_if = "Option::is_none")] + pub max_length: Option, +} + +impl CreateInteractionInputOptions { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("Label".to_string(), serde_json::to_value(&self.label).unwrap_or(Value::Null)); + map.insert("Description".to_string(), serde_json::to_value(&self.description).unwrap_or(Value::Null)); + if let Some(ref v) = self.enable_description_markdown { + map.insert("EnableDescriptionMarkdown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.required { + map.insert("Required".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map.insert("Placeholder".to_string(), serde_json::to_value(&self.placeholder).unwrap_or(Value::Null)); + map.insert("Value".to_string(), serde_json::to_value(&self.value).unwrap_or(Value::Null)); + if let Some(ref v) = self.allow_custom_choice { + map.insert("AllowCustomChoice".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.disabled { + map.insert("Disabled".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.max_length { + map.insert("MaxLength".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map + } +} + +/// DynamicLoadingOptions +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DynamicLoadingOptions { + #[serde(rename = "AlwaysLoadOnStart", skip_serializing_if = "Option::is_none")] + pub always_load_on_start: Option, + #[serde(rename = "DependsOnInputs")] + pub depends_on_inputs: Vec, +} + +impl DynamicLoadingOptions { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + if let Some(ref v) = self.always_load_on_start { + map.insert("AlwaysLoadOnStart".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map.insert("DependsOnInputs".to_string(), serde_json::to_value(&self.depends_on_inputs).unwrap_or(Value::Null)); + map + } +} + +/// InteractionMessageBoxOptions +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct InteractionMessageBoxOptions { + #[serde(rename = "PrimaryButtonText")] + pub primary_button_text: String, + #[serde(rename = "SecondaryButtonText")] + pub secondary_button_text: String, + #[serde(rename = "ShowSecondaryButton", skip_serializing_if = "Option::is_none")] + pub show_secondary_button: Option, + #[serde(rename = "ShowDismiss", skip_serializing_if = "Option::is_none")] + pub show_dismiss: Option, + #[serde(rename = "EnableMessageMarkdown", skip_serializing_if = "Option::is_none")] + pub enable_message_markdown: Option, + #[serde(rename = "Intent", skip_serializing_if = "Option::is_none")] + pub intent: Option, +} + +impl InteractionMessageBoxOptions { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("PrimaryButtonText".to_string(), serde_json::to_value(&self.primary_button_text).unwrap_or(Value::Null)); + map.insert("SecondaryButtonText".to_string(), serde_json::to_value(&self.secondary_button_text).unwrap_or(Value::Null)); + if let Some(ref v) = self.show_secondary_button { + map.insert("ShowSecondaryButton".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.show_dismiss { + map.insert("ShowDismiss".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.enable_message_markdown { + map.insert("EnableMessageMarkdown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.intent { + map.insert("Intent".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map + } +} + +/// InteractionNotificationOptions +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct InteractionNotificationOptions { + #[serde(rename = "PrimaryButtonText")] + pub primary_button_text: String, + #[serde(rename = "SecondaryButtonText")] + pub secondary_button_text: String, + #[serde(rename = "ShowSecondaryButton", skip_serializing_if = "Option::is_none")] + pub show_secondary_button: Option, + #[serde(rename = "ShowDismiss", skip_serializing_if = "Option::is_none")] + pub show_dismiss: Option, + #[serde(rename = "EnableMessageMarkdown", skip_serializing_if = "Option::is_none")] + pub enable_message_markdown: Option, + #[serde(rename = "Intent", skip_serializing_if = "Option::is_none")] + pub intent: Option, + #[serde(rename = "LinkText")] + pub link_text: String, + #[serde(rename = "LinkUrl")] + pub link_url: String, +} + +impl InteractionNotificationOptions { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("PrimaryButtonText".to_string(), serde_json::to_value(&self.primary_button_text).unwrap_or(Value::Null)); + map.insert("SecondaryButtonText".to_string(), serde_json::to_value(&self.secondary_button_text).unwrap_or(Value::Null)); + if let Some(ref v) = self.show_secondary_button { + map.insert("ShowSecondaryButton".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.show_dismiss { + map.insert("ShowDismiss".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.enable_message_markdown { + map.insert("EnableMessageMarkdown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.intent { + map.insert("Intent".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map.insert("LinkText".to_string(), serde_json::to_value(&self.link_text).unwrap_or(Value::Null)); + map.insert("LinkUrl".to_string(), serde_json::to_value(&self.link_url).unwrap_or(Value::Null)); + map + } +} + +/// InteractionInputsDialogOptions +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct InteractionInputsDialogOptions { + #[serde(rename = "PrimaryButtonText")] + pub primary_button_text: String, + #[serde(rename = "SecondaryButtonText")] + pub secondary_button_text: String, + #[serde(rename = "ShowSecondaryButton", skip_serializing_if = "Option::is_none")] + pub show_secondary_button: Option, + #[serde(rename = "ShowDismiss", skip_serializing_if = "Option::is_none")] + pub show_dismiss: Option, + #[serde(rename = "EnableMessageMarkdown", skip_serializing_if = "Option::is_none")] + pub enable_message_markdown: Option, +} + +impl InteractionInputsDialogOptions { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("PrimaryButtonText".to_string(), serde_json::to_value(&self.primary_button_text).unwrap_or(Value::Null)); + map.insert("SecondaryButtonText".to_string(), serde_json::to_value(&self.secondary_button_text).unwrap_or(Value::Null)); + if let Some(ref v) = self.show_secondary_button { + map.insert("ShowSecondaryButton".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.show_dismiss { + map.insert("ShowDismiss".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.enable_message_markdown { + map.insert("EnableMessageMarkdown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map + } +} + +/// BoolInteractionResult +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct BoolInteractionResult { + #[serde(rename = "Canceled")] + pub canceled: bool, + #[serde(rename = "Value", skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +impl BoolInteractionResult { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("Canceled".to_string(), serde_json::to_value(&self.canceled).unwrap_or(Value::Null)); + if let Some(ref v) = self.value { + map.insert("Value".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map + } +} + +/// InputInteractionResult +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct InputInteractionResult { + #[serde(rename = "Canceled")] + pub canceled: bool, + #[serde(rename = "Input", skip_serializing_if = "Option::is_none")] + pub input: Option, +} + +impl InputInteractionResult { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("Canceled".to_string(), serde_json::to_value(&self.canceled).unwrap_or(Value::Null)); + if let Some(ref v) = self.input { + map.insert("Input".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map + } +} + +/// InputsInteractionResult +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct InputsInteractionResult { + #[serde(rename = "Canceled")] + pub canceled: bool, + #[serde(rename = "Inputs", skip_serializing_if = "Option::is_none")] + pub inputs: Option>, +} + +impl InputsInteractionResult { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("Canceled".to_string(), serde_json::to_value(&self.canceled).unwrap_or(Value::Null)); + if let Some(ref v) = self.inputs { + map.insert("Inputs".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + map + } +} + /// ResourceEventDto #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ResourceEventDto { @@ -8880,6 +9155,15 @@ impl ExecuteCommandContext { &self.client } + /// The service provider. + pub fn service_provider(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.ApplicationModel/ExecuteCommandContext.serviceProvider", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IServiceProvider::new(handle, self.client.clone())) + } + /// The resource name. pub fn resource_name(&self) -> Result> { let mut args: HashMap = HashMap::new(); @@ -10620,6 +10904,196 @@ impl IHostEnvironment { } } +/// Wrapper for Aspire.Hosting/Aspire.Hosting.IInteractionService +pub struct IInteractionService { + handle: Handle, + client: Arc, +} + +impl HasHandle for IInteractionService { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl IInteractionService { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Gets a value indicating whether the interaction service is available to prompt the user. + pub fn is_available(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/isAvailable", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Prompts the user for confirmation with an OK/Cancel dialog. + pub fn prompt_confirmation(&self, title: &str, message: &str, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); + args.insert("message".to_string(), serde_json::to_value(&message).unwrap_or(Value::Null)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(token) = cancellation_token { + let token_id = register_cancellation(token, self.client.clone()); + args.insert("cancellationToken".to_string(), Value::String(token_id)); + } + let result = self.client.invoke_capability("Aspire.Hosting/promptConfirmation", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Prompts the user with a message box dialog. + pub fn prompt_message_box(&self, title: &str, message: &str, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); + args.insert("message".to_string(), serde_json::to_value(&message).unwrap_or(Value::Null)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(token) = cancellation_token { + let token_id = register_cancellation(token, self.client.clone()); + args.insert("cancellationToken".to_string(), Value::String(token_id)); + } + let result = self.client.invoke_capability("Aspire.Hosting/promptMessageBox", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Prompts the user with a notification. + pub fn prompt_notification(&self, title: &str, message: &str, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); + args.insert("message".to_string(), serde_json::to_value(&message).unwrap_or(Value::Null)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(token) = cancellation_token { + let token_id = register_cancellation(token, self.client.clone()); + args.insert("cancellationToken".to_string(), Value::String(token_id)); + } + let result = self.client.invoke_capability("Aspire.Hosting/promptNotification", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Prompts the user for a single input. + pub fn prompt_input(&self, title: &str, message: &str, input: &InteractionInputBuilder, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); + args.insert("message".to_string(), serde_json::to_value(&message).unwrap_or(Value::Null)); + args.insert("input".to_string(), input.handle().to_json()); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(token) = cancellation_token { + let token_id = register_cancellation(token, self.client.clone()); + args.insert("cancellationToken".to_string(), Value::String(token_id)); + } + let result = self.client.invoke_capability("Aspire.Hosting/promptInput", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Prompts the user for multiple inputs. + pub fn prompt_inputs(&self, title: &str, message: &str, inputs: Vec, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); + args.insert("message".to_string(), serde_json::to_value(&message).unwrap_or(Value::Null)); + let handles: Vec = inputs.iter().map(|item| item.handle().to_json()).collect(); + args.insert("inputs".to_string(), Value::Array(handles)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(token) = cancellation_token { + let token_id = register_cancellation(token, self.client.clone()); + args.insert("cancellationToken".to_string(), Value::String(token_id)); + } + let result = self.client.invoke_capability("Aspire.Hosting/promptInputs", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Creates a single-line text input. + pub fn create_text_input(&self, name: &str, options: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/createTextInput", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } + + /// Creates a secret (masked) text input. + pub fn create_secret_input(&self, name: &str, options: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/createSecretInput", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } + + /// Creates a boolean (checkbox) input. + pub fn create_boolean_input(&self, name: &str, options: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/createBooleanInput", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } + + /// Creates a numeric input. + pub fn create_number_input(&self, name: &str, options: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/createNumberInput", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } + + /// Creates a choice input that selects from a list of options. + pub fn create_choice_input(&self, name: &str, choices: Option>, options: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("interactionService".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + if let Some(ref v) = choices { + args.insert("choices".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/createChoiceInput", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } +} + /// Wrapper for Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger pub struct ILogger { handle: Handle, @@ -11181,6 +11655,15 @@ impl IServiceProvider { Ok(IDistributedApplicationEventing::new(handle, self.client.clone())) } + /// Gets the interaction service from the service provider. + pub fn get_interaction_service(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("serviceProvider".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/getInteractionService", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IInteractionService::new(handle, self.client.clone())) + } + /// Gets the logger factory from the service provider. pub fn get_logger_factory(&self) -> Result> { let mut args: HashMap = HashMap::new(); @@ -11481,6 +11964,66 @@ impl InputsDialogValidationContext { } } +/// Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder +pub struct InteractionInputBuilder { + handle: Handle, + client: Arc, +} + +impl HasHandle for InteractionInputBuilder { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl InteractionInputBuilder { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Sets the choice options for the input. + pub fn with_choice_options(&self, choices: HashMap) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + args.insert("choices".to_string(), serde_json::to_value(&choices).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/withChoiceOptions", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } + + /// Sets the value of the input. + pub fn with_value(&self, value: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/withValue", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } + + /// Attaches a callback that dynamically loads or updates the input after the prompt starts. + pub fn with_dynamic_loading(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, options: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + if let Some(ref v) = options { + args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting.Ats/withDynamicLoading", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputBuilder::new(handle, self.client.clone())) + } +} + /// Wrapper for Aspire.Hosting/Aspire.Hosting.InteractionInputCollection pub struct InteractionInputCollection { handle: Handle, @@ -11515,6 +12058,67 @@ impl InteractionInputCollection { } } +/// Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext +pub struct InteractionInputLoadContext { + handle: Handle, + client: Arc, +} + +impl HasHandle for InteractionInputLoadContext { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl InteractionInputLoadContext { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Gets the name of the input that is loading. + pub fn get_input_name(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/getInputName", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Gets the current value of an input in the prompt by name. + pub fn get_input_value(&self, input_name: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + args.insert("inputName".to_string(), serde_json::to_value(&input_name).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/getInputValue", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Sets the choice options for the loading input. + pub fn set_choice_options(&self, choices: HashMap) -> Result<(), Box> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + args.insert("choices".to_string(), serde_json::to_value(&choices).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/setChoiceOptions", args)?; + Ok(()) + } + + /// Sets the value of the loading input. + pub fn set_value(&self, value: &str) -> Result<(), Box> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/setValue", args)?; + Ok(()) + } +} + /// Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade pub struct LogFacade { handle: Handle, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index fbe40381e18..b858ed88043 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -309,6 +309,18 @@ type UpdateCommandStateContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.App /** Context passed to ATS-friendly eventing subscriber registrations. */ type EventingSubscriberRegistrationContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext'>; +/** + * An opaque, server-side builder for an `InteractionInput` used by polyglot app hosts. + * + * The builder owns the live `InteractionInput` instance. Dynamic-loading callbacks mutate this same + * instance through `InteractionInputLoadContext`, which is why the input is modeled as a handle here + * instead of the by-value `InteractionInput` DTO. + */ +type InteractionInputBuilderHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder'>; + +/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the prompt, and provides guarded setters to update the loading input. */ +type InteractionInputLoadContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext'>; + /** Represents a distributed application that implements the {@link IHost} and {@link IAsyncDisposable} interfaces. */ type DistributedApplicationHandle = Handle<'Aspire.Hosting/Aspire.Hosting.DistributedApplication'>; @@ -327,6 +339,9 @@ type ExternalServiceResourceHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Exter /** A builder for creating instances of {@link DistributedApplication}. */ type IDistributedApplicationBuilderHandle = Handle<'Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder'>; +/** A service to interact with the current development environment. */ +type IInteractionServiceHandle = Handle<'Aspire.Hosting/Aspire.Hosting.IInteractionService'>; + /** Represents the context for validating inputs in an inputs dialog interaction. */ type InputsDialogValidationContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext'>; @@ -530,6 +545,22 @@ export enum ImagePullPolicy { Never = "Never", } +/** Specifies the intent or purpose of a message in an interaction. */ +export enum MessageIntent { + /** No specific intent. */ + None = "None", + /** Indicates a successful operation. */ + Success = "Success", + /** Indicates a warning. */ + Warning = "Warning", + /** Indicates an error. */ + Error = "Error", + /** Provides informational content. */ + Information = "Information", + /** Requests confirmation from the user. */ + Confirmation = "Confirmation", +} + /** Protocols available for OTLP exporters. */ export enum OtlpProtocol { /** A gRPC-based OTLP exporter. */ @@ -646,6 +677,14 @@ export interface AddContainerOptions { tag?: string | null; } +/** The result of a boolean interaction prompt. */ +export interface BoolInteractionResult { + /** Gets a value indicating whether the interaction was canceled by the user. */ + canceled?: boolean; + /** Gets the value returned from the interaction. Not meaningful when `Canceled` is `true`. */ + value?: boolean; +} + /** Context for configuring certificate trust configuration properties. */ export interface CertificateTrustExecutionConfigurationContext { /** The path to the PEM certificate bundle file in the resource context (e.g., container filesystem). */ @@ -754,6 +793,36 @@ export interface CreateBuilderOptions { throwOnPendingRejections?: boolean; } +/** Optional configuration shared by interaction input factory capabilities. */ +export interface CreateInteractionInputOptions { + /** Gets or sets the label for the input. Defaults to the input name when not specified. */ + label?: string | null; + /** Gets or sets the description for the input. */ + description?: string | null; + /** Gets or sets a value indicating whether the description is rendered as Markdown. */ + enableDescriptionMarkdown?: boolean | null; + /** Gets or sets a value indicating whether the input is required. */ + required?: boolean | null; + /** Gets or sets the placeholder text for the input. */ + placeholder?: string | null; + /** Gets or sets the initial value of the input. */ + value?: string | null; + /** Gets or sets a value indicating whether a custom choice is allowed. Only used by choice inputs. */ + allowCustomChoice?: boolean | null; + /** Gets or sets a value indicating whether the input is disabled. */ + disabled?: boolean | null; + /** Gets or sets the maximum length for text inputs. */ + maxLength?: number | null; +} + +/** Options controlling when a dynamic-loading callback runs. */ +export interface DynamicLoadingOptions { + /** Gets or sets a value indicating whether the callback always runs at the start of the prompt. */ + alwaysLoadOnStart?: boolean | null; + /** Gets or sets the names of inputs this input depends on. The callback runs when any of them change. */ + dependsOnInputs?: string[]; +} + /** The result of executing a command. Returned from `ExecuteCommand`. */ export interface ExecuteCommandResult { /** A flag that indicates whether the command was successful. */ @@ -875,6 +944,72 @@ export interface HttpsCertificateInfo { thumbprint?: string | null; } +/** The result of a single-input interaction prompt. */ +export interface InputInteractionResult { + /** Gets a value indicating whether the interaction was canceled by the user. */ + canceled?: boolean; + /** Gets the input returned from the interaction. Not present when `Canceled` is `true`. */ + input?: InteractionInput; +} + +/** The result of a multi-input interaction prompt. */ +export interface InputsInteractionResult { + /** Gets a value indicating whether the interaction was canceled by the user. */ + canceled?: boolean; + /** Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. */ + inputs?: InteractionInput[]; +} + +/** Options for inputs dialog prompts. */ +export interface InteractionInputsDialogOptions { + /** Gets or sets the primary button text. */ + primaryButtonText?: string | null; + /** Gets or sets the secondary button text. */ + secondaryButtonText?: string | null; + /** Gets or sets a value indicating whether the secondary button is shown. */ + showSecondaryButton?: boolean | null; + /** Gets or sets a value indicating whether the dismiss button is shown. */ + showDismiss?: boolean | null; + /** Gets or sets a value indicating whether Markdown in the message is rendered. */ + enableMessageMarkdown?: boolean | null; +} + +/** Options for message box and confirmation prompts. */ +export interface InteractionMessageBoxOptions { + /** Gets or sets the primary button text. */ + primaryButtonText?: string | null; + /** Gets or sets the secondary button text. */ + secondaryButtonText?: string | null; + /** Gets or sets a value indicating whether the secondary button is shown. */ + showSecondaryButton?: boolean | null; + /** Gets or sets a value indicating whether the dismiss button is shown. */ + showDismiss?: boolean | null; + /** Gets or sets a value indicating whether Markdown in the message is rendered. */ + enableMessageMarkdown?: boolean | null; + /** Gets or sets the intent of the message box. */ + intent?: MessageIntent | null; +} + +/** Options for notification prompts. */ +export interface InteractionNotificationOptions { + /** Gets or sets the primary button text. */ + primaryButtonText?: string | null; + /** Gets or sets the secondary button text. */ + secondaryButtonText?: string | null; + /** Gets or sets a value indicating whether the secondary button is shown. */ + showSecondaryButton?: boolean | null; + /** Gets or sets a value indicating whether the dismiss button is shown. */ + showDismiss?: boolean | null; + /** Gets or sets a value indicating whether Markdown in the message is rendered. */ + enableMessageMarkdown?: boolean | null; + /** Gets or sets the intent of the notification. */ + intent?: MessageIntent | null; + /** Gets or sets the text for a link in the notification. */ + linkText?: string | null; + /** Gets or sets the URL for the link in the notification. */ + linkUrl?: string | null; +} + /** Options for customizing parameter inputs from polyglot app hosts. */ export interface ParameterCustomInputOptions { /** Gets or sets the type of the input. */ @@ -1255,6 +1390,13 @@ export interface CopyOptions { chown?: string; } +export interface CreateChoiceInputOptions { + /** The available choices, keyed by submitted value. */ + choices?: Record; + /** Optional configuration for the input. */ + options?: CreateInteractionInputOptions; +} + export interface CreateMarkdownTaskOptions { cancellationToken?: AbortSignal | CancellationToken; } @@ -1283,6 +1425,31 @@ export interface GetValueAsyncOptions { cancellationToken?: AbortSignal | CancellationToken; } +export interface PromptConfirmationOptions { + options?: InteractionMessageBoxOptions; + cancellationToken?: AbortSignal | CancellationToken; +} + +export interface PromptInputOptions { + options?: InteractionInputsDialogOptions; + cancellationToken?: AbortSignal | CancellationToken; +} + +export interface PromptInputsOptions { + options?: InteractionInputsDialogOptions; + cancellationToken?: AbortSignal | CancellationToken; +} + +export interface PromptMessageBoxOptions { + options?: InteractionMessageBoxOptions; + cancellationToken?: AbortSignal | CancellationToken; +} + +export interface PromptNotificationOptions { + options?: InteractionNotificationOptions; + cancellationToken?: AbortSignal | CancellationToken; +} + export interface PublishAsDockerFileOptions { /** Optional action to configure the container resource */ configure?: (obj: ContainerResource) => Promise; @@ -4932,6 +5099,13 @@ class EventingSubscriberRegistrationContextPromiseImpl implements EventingSubscr /** Context for {@link ResourceCommandAnnotation.ExecuteCommand}. */ export interface ExecuteCommandContext { toJSON(): MarshalledHandle; + /** + * The service provider. + * + * Polyglot command callbacks use this handle to resolve app host services, such as the interaction service via + * `serviceProvider().getInteractionService()`, so they can prompt the user while the command executes. + */ + serviceProvider(): ServiceProviderPromise; /** The resource name. */ resourceName(): Promise; /** The cancellation token. */ @@ -4949,6 +5123,13 @@ export interface ExecuteCommandContext { } export interface ExecuteCommandContextPromise extends PromiseLike { + /** + * The service provider. + * + * Polyglot command callbacks use this handle to resolve app host services, such as the interaction service via + * `serviceProvider().getInteractionService()`, so they can prompt the user while the command executes. + */ + serviceProvider(): ServiceProviderPromise; /** The resource name. */ resourceName(): Promise; /** The cancellation token. */ @@ -4976,6 +5157,17 @@ class ExecuteCommandContextImpl implements ExecuteCommandContext { /** Serialize for JSON-RPC transport */ toJSON(): MarshalledHandle { return this._handle.toJSON(); } + serviceProvider(): ServiceProviderPromise { + const promise = (async () => { + const handle = await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/ExecuteCommandContext.serviceProvider', + { context: this._handle } + ); + return new ServiceProviderImpl(handle, this._client); + })(); + return new ServiceProviderPromiseImpl(promise, this._client, false); + } + async resourceName(): Promise { return await this._client.invokeCapability( 'Aspire.Hosting.ApplicationModel/ExecuteCommandContext.resourceName', @@ -5026,6 +5218,10 @@ class ExecuteCommandContextPromiseImpl implements ExecuteCommandContextPromise { return this._promise.then(onfulfilled, onrejected); } + serviceProvider(): ServiceProviderPromise { + return new ServiceProviderPromiseImpl(this._promise.then(obj => obj.serviceProvider()), this._client, false); + } + resourceName(): Promise { return this._promise.then(obj => obj.resourceName()); } @@ -5299,6 +5495,335 @@ class InputsDialogValidationContextPromiseImpl implements InputsDialogValidation } +// ============================================================================ +// InteractionInputBuilder +// ============================================================================ + +/** + * An opaque, server-side builder for an `InteractionInput` used by polyglot app hosts. + * + * The builder owns the live `InteractionInput` instance. Dynamic-loading callbacks mutate this same + * instance through `InteractionInputLoadContext`, which is why the input is modeled as a handle here + * instead of the by-value `InteractionInput` DTO. + */ +export interface InteractionInputBuilder { + toJSON(): MarshalledHandle; + /** + * Sets the choice options for the input. + * @param choices The available choices, keyed by submitted value. + * @returns The same builder handle. + */ + withChoiceOptions(choices: Record): InteractionInputBuilderPromise; + /** + * Sets the value of the input. + * @param value The value to assign. + * @returns The same builder handle. + */ + withValue(value: string): InteractionInputBuilderPromise; + /** + * Attaches a callback that dynamically loads or updates the input after the prompt starts. + * @param callback The callback invoked to load the input. Use the supplied context to read other inputs and update this input. + * @param options Additional options. + * @returns The same builder handle. + */ + withDynamicLoading(callback: (arg: InteractionInputLoadContext) => Promise, options?: DynamicLoadingOptions): InteractionInputBuilderPromise; +} + +export interface InteractionInputBuilderPromise extends PromiseLike { + /** + * Sets the choice options for the input. + * @param choices The available choices, keyed by submitted value. + * @returns The same builder handle. + */ + withChoiceOptions(choices: Record): InteractionInputBuilderPromise; + /** + * Sets the value of the input. + * @param value The value to assign. + * @returns The same builder handle. + */ + withValue(value: string): InteractionInputBuilderPromise; + /** + * Attaches a callback that dynamically loads or updates the input after the prompt starts. + * @param callback The callback invoked to load the input. Use the supplied context to read other inputs and update this input. + * @param options Additional options. + * @returns The same builder handle. + */ + withDynamicLoading(callback: (arg: InteractionInputLoadContext) => Promise, options?: DynamicLoadingOptions): InteractionInputBuilderPromise; +} + +// ============================================================================ +// InteractionInputBuilderImpl +// ============================================================================ + +/** + * An opaque, server-side builder for an `InteractionInput` used by polyglot app hosts. + * + * The builder owns the live `InteractionInput` instance. Dynamic-loading callbacks mutate this same + * instance through `InteractionInputLoadContext`, which is why the input is modeled as a handle here + * instead of the by-value `InteractionInput` DTO. + */ +class InteractionInputBuilderImpl implements InteractionInputBuilder { + constructor(private _handle: InteractionInputBuilderHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** @internal */ + async _withChoiceOptionsInternal(choices: Record): Promise { + const rpcArgs: Record = { context: this._handle, choices }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting.Ats/withChoiceOptions', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Sets the choice options for the input. + * @param choices The available choices, keyed by submitted value. + * @returns The same builder handle. + */ + withChoiceOptions(choices: Record): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._withChoiceOptionsInternal(choices), this._client); + } + + /** @internal */ + async _withValueInternal(value: string): Promise { + const rpcArgs: Record = { context: this._handle, value }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting.Ats/withValue', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Sets the value of the input. + * @param value The value to assign. + * @returns The same builder handle. + */ + withValue(value: string): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._withValueInternal(value), this._client); + } + + /** @internal */ + async _withDynamicLoadingInternal(callback: (arg: InteractionInputLoadContext) => Promise, options?: DynamicLoadingOptions): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as InteractionInputLoadContextHandle; + const arg = new InteractionInputLoadContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { context: this._handle, callback: callbackId }; + if (options !== undefined) rpcArgs.options = options; + const result = await this._client.invokeCapability( + 'Aspire.Hosting.Ats/withDynamicLoading', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Attaches a callback that dynamically loads or updates the input after the prompt starts. + * @param callback The callback invoked to load the input. Use the supplied context to read other inputs and update this input. + * @param options Additional options. + * @returns The same builder handle. + */ + withDynamicLoading(callback: (arg: InteractionInputLoadContext) => Promise, options?: DynamicLoadingOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._withDynamicLoadingInternal(callback, options), this._client); + } + +} + +/** + * Thenable wrapper for InteractionInputBuilder that enables fluent chaining. + */ +class InteractionInputBuilderPromiseImpl implements InteractionInputBuilderPromise { + constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { + if (track) { _client.trackPromise(_promise); } + } + + then( + onfulfilled?: ((value: InteractionInputBuilder) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + withChoiceOptions(choices: Record): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.withChoiceOptions(choices)), this._client); + } + + withValue(value: string): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.withValue(value)), this._client); + } + + withDynamicLoading(callback: (arg: InteractionInputLoadContext) => Promise, options?: DynamicLoadingOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.withDynamicLoading(callback, options)), this._client); + } + +} + +// ============================================================================ +// InteractionInputLoadContext +// ============================================================================ + +/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the prompt, and provides guarded setters to update the loading input. */ +export interface InteractionInputLoadContext { + toJSON(): MarshalledHandle; + /** + * Gets the name of the input that is loading. + * @returns The input name. + */ + getInputName(): Promise; + /** + * Gets the current value of an input in the prompt by name. + * @param inputName The name of the input to read. + * @returns The input value, or `null` when the input has no value or does not exist. + */ + getInputValue(inputName: string): Promise; + /** + * Sets the choice options for the loading input. + * @param choices The available choices, keyed by submitted value. + */ + setChoiceOptions(choices: Record): InteractionInputLoadContextPromise; + /** + * Sets the value of the loading input. + * @param value The value to assign. + */ + setValue(value: string): InteractionInputLoadContextPromise; +} + +export interface InteractionInputLoadContextPromise extends PromiseLike { + /** + * Gets the name of the input that is loading. + * @returns The input name. + */ + getInputName(): Promise; + /** + * Gets the current value of an input in the prompt by name. + * @param inputName The name of the input to read. + * @returns The input value, or `null` when the input has no value or does not exist. + */ + getInputValue(inputName: string): Promise; + /** + * Sets the choice options for the loading input. + * @param choices The available choices, keyed by submitted value. + */ + setChoiceOptions(choices: Record): InteractionInputLoadContextPromise; + /** + * Sets the value of the loading input. + * @param value The value to assign. + */ + setValue(value: string): InteractionInputLoadContextPromise; +} + +// ============================================================================ +// InteractionInputLoadContextImpl +// ============================================================================ + +/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the prompt, and provides guarded setters to update the loading input. */ +class InteractionInputLoadContextImpl implements InteractionInputLoadContext { + constructor(private _handle: InteractionInputLoadContextHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** + * Gets the name of the input that is loading. + * @returns The input name. + */ + async getInputName(): Promise { + const rpcArgs: Record = { context: this._handle }; + return await this._client.invokeCapability( + 'Aspire.Hosting.Ats/getInputName', + rpcArgs + ); + } + + /** + * Gets the current value of an input in the prompt by name. + * @param inputName The name of the input to read. + * @returns The input value, or `null` when the input has no value or does not exist. + */ + async getInputValue(inputName: string): Promise { + const rpcArgs: Record = { context: this._handle, inputName }; + return await this._client.invokeCapability( + 'Aspire.Hosting.Ats/getInputValue', + rpcArgs + ); + } + + /** @internal */ + async _setChoiceOptionsInternal(choices: Record): Promise { + const rpcArgs: Record = { context: this._handle, choices }; + await this._client.invokeCapability( + 'Aspire.Hosting.Ats/setChoiceOptions', + rpcArgs + ); + return this; + } + + /** + * Sets the choice options for the loading input. + * @param choices The available choices, keyed by submitted value. + */ + setChoiceOptions(choices: Record): InteractionInputLoadContextPromise { + return new InteractionInputLoadContextPromiseImpl(this._setChoiceOptionsInternal(choices), this._client); + } + + /** @internal */ + async _setValueInternal(value: string): Promise { + const rpcArgs: Record = { context: this._handle, value }; + await this._client.invokeCapability( + 'Aspire.Hosting.Ats/setValue', + rpcArgs + ); + return this; + } + + /** + * Sets the value of the loading input. + * @param value The value to assign. + */ + setValue(value: string): InteractionInputLoadContextPromise { + return new InteractionInputLoadContextPromiseImpl(this._setValueInternal(value), this._client); + } + +} + +/** + * Thenable wrapper for InteractionInputLoadContext that enables fluent chaining. + */ +class InteractionInputLoadContextPromiseImpl implements InteractionInputLoadContextPromise { + constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { + if (track) { _client.trackPromise(_promise); } + } + + then( + onfulfilled?: ((value: InteractionInputLoadContext) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + getInputName(): Promise { + return this._promise.then(obj => obj.getInputName()); + } + + getInputValue(inputName: string): Promise { + return this._promise.then(obj => obj.getInputValue(inputName)); + } + + setChoiceOptions(choices: Record): InteractionInputLoadContextPromise { + return new InteractionInputLoadContextPromiseImpl(this._promise.then(obj => obj.setChoiceOptions(choices)), this._client); + } + + setValue(value: string): InteractionInputLoadContextPromise { + return new InteractionInputLoadContextPromiseImpl(this._promise.then(obj => obj.setValue(value)), this._client); + } + +} + // ============================================================================ // LogFacade // ============================================================================ @@ -10532,6 +11057,396 @@ class HostEnvironmentPromiseImpl implements HostEnvironmentPromise { } +// ============================================================================ +// InteractionService +// ============================================================================ + +/** A service to interact with the current development environment. */ +export interface InteractionService { + toJSON(): MarshalledHandle; + /** + * Gets a value indicating whether the interaction service is available to prompt the user. + * @returns `true` when the service can prompt the user; otherwise `false`. + */ + isAvailable(): Promise; + /** + * Prompts the user for confirmation with an OK/Cancel dialog. + * @param options Additional options. + */ + promptConfirmation(title: string, message: string, options?: PromptConfirmationOptions): Promise; + /** + * Prompts the user with a message box dialog. + * @param options Additional options. + */ + promptMessageBox(title: string, message: string, options?: PromptMessageBoxOptions): Promise; + /** + * Prompts the user with a notification. + * @param options Additional options. + */ + promptNotification(title: string, message: string, options?: PromptNotificationOptions): Promise; + /** + * Prompts the user for a single input. + * @param options Additional options. + */ + promptInput(title: string, message: string, input: Awaitable, options?: PromptInputOptions): Promise; + /** + * Prompts the user for multiple inputs. + * @param options Additional options. + */ + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): Promise; + /** + * Creates a single-line text input. + * @param options Additional options. + */ + createTextInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a secret (masked) text input. + * @param options Additional options. + */ + createSecretInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a boolean (checkbox) input. + * @param options Additional options. + */ + createBooleanInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a numeric input. + * @param options Additional options. + */ + createNumberInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a choice input that selects from a list of options. + * @param name The name of the input. + * @param options Additional options. + */ + createChoiceInput(name: string, options?: CreateChoiceInputOptions): InteractionInputBuilderPromise; +} + +export interface InteractionServicePromise extends PromiseLike { + /** + * Gets a value indicating whether the interaction service is available to prompt the user. + * @returns `true` when the service can prompt the user; otherwise `false`. + */ + isAvailable(): Promise; + /** + * Prompts the user for confirmation with an OK/Cancel dialog. + * @param options Additional options. + */ + promptConfirmation(title: string, message: string, options?: PromptConfirmationOptions): Promise; + /** + * Prompts the user with a message box dialog. + * @param options Additional options. + */ + promptMessageBox(title: string, message: string, options?: PromptMessageBoxOptions): Promise; + /** + * Prompts the user with a notification. + * @param options Additional options. + */ + promptNotification(title: string, message: string, options?: PromptNotificationOptions): Promise; + /** + * Prompts the user for a single input. + * @param options Additional options. + */ + promptInput(title: string, message: string, input: Awaitable, options?: PromptInputOptions): Promise; + /** + * Prompts the user for multiple inputs. + * @param options Additional options. + */ + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): Promise; + /** + * Creates a single-line text input. + * @param options Additional options. + */ + createTextInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a secret (masked) text input. + * @param options Additional options. + */ + createSecretInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a boolean (checkbox) input. + * @param options Additional options. + */ + createBooleanInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a numeric input. + * @param options Additional options. + */ + createNumberInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise; + /** + * Creates a choice input that selects from a list of options. + * @param name The name of the input. + * @param options Additional options. + */ + createChoiceInput(name: string, options?: CreateChoiceInputOptions): InteractionInputBuilderPromise; +} + +// ============================================================================ +// InteractionServiceImpl +// ============================================================================ + +/** A service to interact with the current development environment. */ +class InteractionServiceImpl implements InteractionService { + constructor(private _handle: IInteractionServiceHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** + * Gets a value indicating whether the interaction service is available to prompt the user. + * @returns `true` when the service can prompt the user; otherwise `false`. + */ + async isAvailable(): Promise { + const rpcArgs: Record = { interactionService: this._handle }; + return await this._client.invokeCapability( + 'Aspire.Hosting/isAvailable', + rpcArgs + ); + } + + /** + * Prompts the user for confirmation with an OK/Cancel dialog. + * @param optionsBag Additional options. + */ + async promptConfirmation(title: string, message: string, optionsBag?: PromptConfirmationOptions): Promise { + const options = optionsBag?.options; + const cancellationToken = optionsBag?.cancellationToken; + const rpcArgs: Record = { interactionService: this._handle, title, message }; + if (options !== undefined) rpcArgs.options = options; + if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); + return await this._client.invokeCapability( + 'Aspire.Hosting/promptConfirmation', + rpcArgs + ); + } + + /** + * Prompts the user with a message box dialog. + * @param optionsBag Additional options. + */ + async promptMessageBox(title: string, message: string, optionsBag?: PromptMessageBoxOptions): Promise { + const options = optionsBag?.options; + const cancellationToken = optionsBag?.cancellationToken; + const rpcArgs: Record = { interactionService: this._handle, title, message }; + if (options !== undefined) rpcArgs.options = options; + if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); + return await this._client.invokeCapability( + 'Aspire.Hosting/promptMessageBox', + rpcArgs + ); + } + + /** + * Prompts the user with a notification. + * @param optionsBag Additional options. + */ + async promptNotification(title: string, message: string, optionsBag?: PromptNotificationOptions): Promise { + const options = optionsBag?.options; + const cancellationToken = optionsBag?.cancellationToken; + const rpcArgs: Record = { interactionService: this._handle, title, message }; + if (options !== undefined) rpcArgs.options = options; + if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); + return await this._client.invokeCapability( + 'Aspire.Hosting/promptNotification', + rpcArgs + ); + } + + /** + * Prompts the user for a single input. + * @param optionsBag Additional options. + */ + async promptInput(title: string, message: string, input: Awaitable, optionsBag?: PromptInputOptions): Promise { + const options = optionsBag?.options; + const cancellationToken = optionsBag?.cancellationToken; + input = isPromiseLike(input) ? await input : input; + const rpcArgs: Record = { interactionService: this._handle, title, message, input }; + if (options !== undefined) rpcArgs.options = options; + if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); + return await this._client.invokeCapability( + 'Aspire.Hosting/promptInput', + rpcArgs + ); + } + + /** + * Prompts the user for multiple inputs. + * @param optionsBag Additional options. + */ + async promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], optionsBag?: PromptInputsOptions): Promise { + const options = optionsBag?.options; + const cancellationToken = optionsBag?.cancellationToken; + const rpcArgs: Record = { interactionService: this._handle, title, message, inputs }; + if (options !== undefined) rpcArgs.options = options; + if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); + return await this._client.invokeCapability( + 'Aspire.Hosting/promptInputs', + rpcArgs + ); + } + + /** @internal */ + async _createTextInputInternal(name: string, options?: CreateInteractionInputOptions): Promise { + const rpcArgs: Record = { interactionService: this._handle, name }; + if (options !== undefined) rpcArgs.options = options; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/createTextInput', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Creates a single-line text input. + * @param options Additional options. + */ + createTextInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._createTextInputInternal(name, options), this._client); + } + + /** @internal */ + async _createSecretInputInternal(name: string, options?: CreateInteractionInputOptions): Promise { + const rpcArgs: Record = { interactionService: this._handle, name }; + if (options !== undefined) rpcArgs.options = options; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/createSecretInput', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Creates a secret (masked) text input. + * @param options Additional options. + */ + createSecretInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._createSecretInputInternal(name, options), this._client); + } + + /** @internal */ + async _createBooleanInputInternal(name: string, options?: CreateInteractionInputOptions): Promise { + const rpcArgs: Record = { interactionService: this._handle, name }; + if (options !== undefined) rpcArgs.options = options; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/createBooleanInput', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Creates a boolean (checkbox) input. + * @param options Additional options. + */ + createBooleanInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._createBooleanInputInternal(name, options), this._client); + } + + /** @internal */ + async _createNumberInputInternal(name: string, options?: CreateInteractionInputOptions): Promise { + const rpcArgs: Record = { interactionService: this._handle, name }; + if (options !== undefined) rpcArgs.options = options; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/createNumberInput', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Creates a numeric input. + * @param options Additional options. + */ + createNumberInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._createNumberInputInternal(name, options), this._client); + } + + /** @internal */ + async _createChoiceInputInternal(name: string, choices?: Record, options?: CreateInteractionInputOptions): Promise { + const rpcArgs: Record = { interactionService: this._handle, name }; + if (choices !== undefined) rpcArgs.choices = choices; + if (options !== undefined) rpcArgs.options = options; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/createChoiceInput', + rpcArgs + ); + return new InteractionInputBuilderImpl(result, this._client); + } + + /** + * Creates a choice input that selects from a list of options. + * @param name The name of the input. + * @param optionsBag Additional options. + */ + createChoiceInput(name: string, optionsBag?: CreateChoiceInputOptions): InteractionInputBuilderPromise { + const choices = optionsBag?.choices; + const options = optionsBag?.options; + return new InteractionInputBuilderPromiseImpl(this._createChoiceInputInternal(name, choices, options), this._client); + } + +} + +/** + * Thenable wrapper for InteractionService that enables fluent chaining. + */ +class InteractionServicePromiseImpl implements InteractionServicePromise { + constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { + if (track) { _client.trackPromise(_promise); } + } + + then( + onfulfilled?: ((value: InteractionService) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + isAvailable(): Promise { + return this._promise.then(obj => obj.isAvailable()); + } + + promptConfirmation(title: string, message: string, options?: PromptConfirmationOptions): Promise { + return this._promise.then(obj => obj.promptConfirmation(title, message, options)); + } + + promptMessageBox(title: string, message: string, options?: PromptMessageBoxOptions): Promise { + return this._promise.then(obj => obj.promptMessageBox(title, message, options)); + } + + promptNotification(title: string, message: string, options?: PromptNotificationOptions): Promise { + return this._promise.then(obj => obj.promptNotification(title, message, options)); + } + + promptInput(title: string, message: string, input: Awaitable, options?: PromptInputOptions): Promise { + return this._promise.then(obj => obj.promptInput(title, message, input, options)); + } + + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): Promise { + return this._promise.then(obj => obj.promptInputs(title, message, inputs, options)); + } + + createTextInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.createTextInput(name, options)), this._client); + } + + createSecretInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.createSecretInput(name, options)), this._client); + } + + createBooleanInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.createBooleanInput(name, options)), this._client); + } + + createNumberInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.createNumberInput(name, options)), this._client); + } + + createChoiceInput(name: string, options?: CreateChoiceInputOptions): InteractionInputBuilderPromise { + return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.createChoiceInput(name, options)), this._client); + } + +} + // ============================================================================ // Logger // ============================================================================ @@ -11187,6 +12102,11 @@ export interface ServiceProvider { * @returns The distributed application eventing handle. */ getEventing(): DistributedApplicationEventingPromise; + /** + * Gets the interaction service from the service provider. + * @returns An interaction service handle. + */ + getInteractionService(): InteractionServicePromise; /** * Gets the logger factory from the service provider. * @returns A logger factory handle. @@ -11230,6 +12150,11 @@ export interface ServiceProviderPromise extends PromiseLike { * @returns The distributed application eventing handle. */ getEventing(): DistributedApplicationEventingPromise; + /** + * Gets the interaction service from the service provider. + * @returns An interaction service handle. + */ + getInteractionService(): InteractionServicePromise; /** * Gets the logger factory from the service provider. * @returns A logger factory handle. @@ -11296,6 +12221,24 @@ class ServiceProviderImpl implements ServiceProvider { return new DistributedApplicationEventingPromiseImpl(this._getEventingInternal(), this._client); } + /** @internal */ + async _getInteractionServiceInternal(): Promise { + const rpcArgs: Record = { serviceProvider: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/getInteractionService', + rpcArgs + ); + return new InteractionServiceImpl(result, this._client); + } + + /** + * Gets the interaction service from the service provider. + * @returns An interaction service handle. + */ + getInteractionService(): InteractionServicePromise { + return new InteractionServicePromiseImpl(this._getInteractionServiceInternal(), this._client); + } + /** @internal */ async _getLoggerFactoryInternal(): Promise { const rpcArgs: Record = { serviceProvider: this._handle }; @@ -11443,6 +12386,10 @@ class ServiceProviderPromiseImpl implements ServiceProviderPromise { return new DistributedApplicationEventingPromiseImpl(this._promise.then(obj => obj.getEventing()), this._client); } + getInteractionService(): InteractionServicePromise { + return new InteractionServicePromiseImpl(this._promise.then(obj => obj.getInteractionService()), this._client); + } + getLoggerFactory(): LoggerFactoryPromise { return new LoggerFactoryPromiseImpl(this._promise.then(obj => obj.getLoggerFactory()), this._client); } @@ -53766,6 +54713,8 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegis registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecuteCommandContext', (handle, client) => new ExecuteCommandContextImpl(handle as ExecuteCommandContextHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.InitializeResourceEvent', (handle, client) => new InitializeResourceEventImpl(handle as InitializeResourceEventHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext', (handle, client) => new InputsDialogValidationContextImpl(handle as InputsDialogValidationContextHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder', (handle, client) => new InteractionInputBuilderImpl(handle as InteractionInputBuilderHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext', (handle, client) => new InteractionInputLoadContextImpl(handle as InteractionInputLoadContextHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade', (handle, client) => new LogFacadeImpl(handle as LogFacadeHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineConfigurationContext', (handle, client) => new PipelineConfigurationContextImpl(handle as PipelineConfigurationContextHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineContext', (handle, client) => new PipelineContextImpl(handle as PipelineContextHandle, client)); @@ -53799,6 +54748,7 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedAppli registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExecutionConfigurationBuilder', (handle, client) => new ExecutionConfigurationBuilderImpl(handle as IExecutionConfigurationBuilderHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExecutionConfigurationResult', (handle, client) => new ExecutionConfigurationResultImpl(handle as IExecutionConfigurationResultHandle, client)); registerHandleWrapper('Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.IHostEnvironment', (handle, client) => new HostEnvironmentImpl(handle as IHostEnvironmentHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.IInteractionService', (handle, client) => new InteractionServiceImpl(handle as IInteractionServiceHandle, client)); registerHandleWrapper('Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger', (handle, client) => new LoggerImpl(handle as ILoggerHandle, client)); registerHandleWrapper('Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILoggerFactory', (handle, client) => new LoggerFactoryImpl(handle as ILoggerFactoryHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingStep', (handle, client) => new ReportingStepImpl(handle as IReportingStepHandle, client)); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index e633017b700..7e8df9b7688 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -585,6 +585,42 @@ ENTRYPOINT ["dotnet", "App.dll"] return result }) + // Test bench for the polyglot IInteractionService API: prompts for a region, then dynamically + // loads the available zones for that region into a second choice input. Reached via the command's + // service provider (ServiceProvider().GetInteractionService()), which only prompts when the + // interaction service is available (the interactive dashboard path). + _ = container.WithCommand("pick-zone", "Pick Zone", func(ctx aspire.ExecuteCommandContext) *aspire.ExecuteCommandResult { + interactionService := ctx.ServiceProvider().GetInteractionService() + + available, err := interactionService.IsAvailable() + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + if !available { + return &aspire.ExecuteCommandResult{Success: true, Message: aspire.StringPtr("Interaction service is not available.")} + } + + regionInput := interactionService.CreateChoiceInput("region", &aspire.CreateChoiceInputOptions{ + Choices: map[string]string{"us": "United States", "eu": "Europe"}, + }) + + zoneInput := interactionService.CreateChoiceInput("zone").WithDynamicLoading(func(loadContext aspire.InteractionInputLoadContext) { + region, _ := loadContext.GetInputValue("region") + zones := map[string]string{"us-east": "US East", "us-west": "US West"} + if region == "eu" { + zones = map[string]string{"eu-west": "EU West", "eu-north": "EU North"} + } + _ = loadContext.SetChoiceOptions(zones) + }) + + result, err := interactionService.PromptInputs("Pick a zone", "Choose a region, then pick a zone from the dynamically loaded options.", []aspire.InteractionInputBuilder{regionInput, zoneInput}) + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + + return &aspire.ExecuteCommandResult{Success: !result.Canceled, Canceled: aspire.BoolPtr(result.Canceled)} + }) + app, err := builder.Build() if err != nil { log.Fatalf(aspire.FormatError(err)) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index d90dc8a4f47..50a94c4b269 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -268,6 +268,41 @@ void main() throws Exception { .arguments(Map.of("message", "hello")) .cancellationToken(cancellationToken)); }); + // Test bench for the polyglot IInteractionService API: prompts for a region, then dynamically + // loads the available zones for that region into a second choice input. Reached via the command's + // service provider (serviceProvider().getInteractionService()), which only prompts when the + // interaction service is available (the interactive dashboard path). + container.withCommand("pick-zone", "Pick Zone", (ctx) -> { + var interactionService = ctx.serviceProvider().getInteractionService(); + if (!interactionService.isAvailable()) { + var unavailable = new ExecuteCommandResult(); + unavailable.setSuccess(true); + unavailable.setMessage("Interaction service is not available."); + return unavailable; + } + + var regionInput = interactionService.createChoiceInput( + "region", + new CreateChoiceInputOptions().choices(Map.of("us", "United States", "eu", "Europe"))); + + var zoneInput = interactionService.createChoiceInput("zone").withDynamicLoading((loadContext) -> { + var region = loadContext.getInputValue("region"); + Map zones = "eu".equals(region) + ? Map.of("eu-west", "EU West", "eu-north", "EU North") + : Map.of("us-east", "US East", "us-west", "US West"); + loadContext.setChoiceOptions(zones); + }); + + var result = interactionService.promptInputs( + "Pick a zone", + "Choose a region, then pick a zone from the dynamically loaded options.", + new InteractionInputBuilder[] { regionInput, zoneInput }); + + var commandResult = new ExecuteCommandResult(); + commandResult.setSuccess(!result.getCanceled()); + commandResult.setCanceled(result.getCanceled()); + return commandResult; + }); container.withHttpCommand("/health", "Health Check"); var httpCmdOptions = new HttpCommandExportOptions(); httpCmdOptions.setMethodName("POST"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index cdcb0c9a664..180dfe9ff4d 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -437,6 +437,37 @@ def restart_command(_ctx): command_options={"Arguments": [{"Name": "message", "InputType": "Text", "Required": True}]} ) container.with_command("restart", "Restart", restart_command) + # Test bench for the polyglot IInteractionService API: prompts for a region, then dynamically + # loads the available zones for that region into a second choice input. Reached via the command's + # service provider (service_provider.get_interaction_service()), which only prompts when the + # interaction service is available (the interactive dashboard path). + def pick_zone_command(ctx): + interaction_service = ctx.service_provider.get_interaction_service() + if not interaction_service.is_available(): + return {"success": True, "message": "Interaction service is not available."} + + region_input = interaction_service.create_choice_input( + "region", + choices={"us": "United States", "eu": "Europe"} + ) + + def load_zones(load_context): + region = load_context.get_input_value("region") + zones = ({"eu-west": "EU West", "eu-north": "EU North"} + if region == "eu" + else {"us-east": "US East", "us-west": "US West"}) + load_context.set_choice_options(zones) + + zone_input = interaction_service.create_choice_input("zone").with_dynamic_loading(load_zones) + + result = interaction_service.prompt_inputs( + "Pick a zone", + "Choose a region, then pick a zone from the dynamically loaded options.", + [region_input, zone_input] + ) + return {"success": not result.get("Canceled", False), "canceled": result.get("Canceled", False)} + + container.with_command("pick-zone", "Pick Zone", pick_zone_command) # withHttpCommand container.with_http_command("/health", "Health Check") container.with_http_command("/api/reset", "Reset", options={"MethodName": "POST", "ConfirmationMessage": "Are you sure?"}) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index 6e5c77870c2..5e297ef74c8 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -748,6 +748,40 @@ await container.withCommand("restart", "Restart", async (ctx) => { cancellationToken }); }); +// Test bench for the polyglot IInteractionService API: prompts for a region, then dynamically +// loads the available zones for that region into a second choice input. Reached via the command's +// service provider (serviceProvider().getInteractionService()), which only prompts when the +// interaction service is available (the interactive dashboard path). +await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { + const interactionService = await ctx.serviceProvider().getInteractionService(); + + if (!(await interactionService.isAvailable())) { + return { success: true, message: "Interaction service is not available." }; + } + + const regionInput = await interactionService.createChoiceInput("region", { + choices: { "us": "United States", "eu": "Europe" } + }); + + const zoneInput = await interactionService + .createChoiceInput("zone") + .withDynamicLoading(async (loadContext) => { + const region = await loadContext.getInputValue("region"); + + const zones = region === "eu" + ? { "eu-west": "EU West", "eu-north": "EU North" } + : { "us-east": "US East", "us-west": "US West" }; + + await loadContext.setChoiceOptions(zones); + }); + + const result = await interactionService.promptInputs( + "Pick a zone", + "Choose a region, then pick a zone from the dynamically loaded options.", + [regionInput, zoneInput]); + + return { success: !result.canceled, canceled: result.canceled }; +}); // withProcessCommand await container.withProcessCommand("dotnet-version", "Show .NET version", { From 2bb7239e0baea0ff0e31f17152f813796e918a02 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 09:09:26 -0700 Subject: [PATCH 02/20] Add interaction-showcase test bench covering full IInteractionService surface Expands the polyglot test bench so every newly added IInteractionService member is exercised by the per-language typecheck: all prompt overloads (confirmation/messageBox/notification/input/inputs), every input factory (text/secret/boolean/number/choice), the builder methods (withValue/ withChoiceOptions/withDynamicLoading), the dynamic-loading context accessors/setters (getInputName/getInputValue/setChoiceOptions/setValue), and all option/result DTO fields plus the MessageIntent enum. Added to the TypeScript, Python, Go and Java apphosts. Also removes the redundant on ExecuteCommandContext.ServiceProvider and regenerates the TypeScript snapshot to drop the propagated doc comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ResourceCommandAnnotation.cs | 4 - ...TwoPassScanningGeneratedAspire.verified.ts | 14 +- .../Aspire.Hosting/Go/apphost.go | 132 ++++++++++++++++++ .../Aspire.Hosting/Java/AppHost.java | 118 ++++++++++++++++ .../Aspire.Hosting/Python/apphost.py | 108 ++++++++++++++ .../Aspire.Hosting/TypeScript/apphost.mts | 99 +++++++++++++ 6 files changed, 459 insertions(+), 16 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs index 52f7f7b985a..753d763a0ed 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs @@ -432,10 +432,6 @@ public sealed class ExecuteCommandContext /// /// The service provider. /// - /// - /// Polyglot command callbacks use this handle to resolve app host services, such as the interaction service via - /// serviceProvider().getInteractionService(), so they can prompt the user while the command executes. - /// public required IServiceProvider ServiceProvider { get; init; } /// diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index b858ed88043..d6a8f79559c 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -5099,12 +5099,7 @@ class EventingSubscriberRegistrationContextPromiseImpl implements EventingSubscr /** Context for {@link ResourceCommandAnnotation.ExecuteCommand}. */ export interface ExecuteCommandContext { toJSON(): MarshalledHandle; - /** - * The service provider. - * - * Polyglot command callbacks use this handle to resolve app host services, such as the interaction service via - * `serviceProvider().getInteractionService()`, so they can prompt the user while the command executes. - */ + /** The service provider. */ serviceProvider(): ServiceProviderPromise; /** The resource name. */ resourceName(): Promise; @@ -5123,12 +5118,7 @@ export interface ExecuteCommandContext { } export interface ExecuteCommandContextPromise extends PromiseLike { - /** - * The service provider. - * - * Polyglot command callbacks use this handle to resolve app host services, such as the interaction service via - * `serviceProvider().getInteractionService()`, so they can prompt the user while the command executes. - */ + /** The service provider. */ serviceProvider(): ServiceProviderPromise; /** The resource name. */ resourceName(): Promise; diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 7e8df9b7688..9a1d95de4ce 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log" "apphost/modules/aspire" @@ -621,6 +622,137 @@ ENTRYPOINT ["dotnet", "App.dll"] return &aspire.ExecuteCommandResult{Success: !result.Canceled, Canceled: aspire.BoolPtr(result.Canceled)} }) + // Exhaustive coverage of the remaining IInteractionService surface so every newly added member is + // exercised by the polyglot typecheck: all prompt overloads, every input factory and builder method, + // the dynamic-loading context accessors/setters, and the option/result DTO fields. + _ = container.WithCommand("interaction-showcase", "Interaction Showcase", func(ctx aspire.ExecuteCommandContext) *aspire.ExecuteCommandResult { + interactionService := ctx.ServiceProvider().GetInteractionService() + + available, err := interactionService.IsAvailable() + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + if !available { + return &aspire.ExecuteCommandResult{Success: true, Message: aspire.StringPtr("Interaction service is not available.")} + } + + confirmIntent := aspire.MessageIntentConfirmation + confirmation, err := interactionService.PromptConfirmation("Confirm", "Proceed?", &aspire.PromptConfirmationOptions{ + Options: &aspire.InteractionMessageBoxOptions{ + PrimaryButtonText: "Yes", + SecondaryButtonText: "No", + ShowSecondaryButton: aspire.BoolPtr(true), + ShowDismiss: aspire.BoolPtr(true), + EnableMessageMarkdown: aspire.BoolPtr(true), + Intent: &confirmIntent, + }, + }) + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + + infoIntent := aspire.MessageIntentInformation + messageBox, err := interactionService.PromptMessageBox("Notice", "Read this.", &aspire.PromptMessageBoxOptions{ + Options: &aspire.InteractionMessageBoxOptions{PrimaryButtonText: "OK", Intent: &infoIntent}, + }) + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + + warnIntent := aspire.MessageIntentWarning + notification, err := interactionService.PromptNotification("Heads up", "Something happened.", &aspire.PromptNotificationOptions{ + Options: &aspire.InteractionNotificationOptions{ + Intent: &warnIntent, + LinkText: "Learn more", + LinkUrl: "https://aspire.dev", + ShowDismiss: aspire.BoolPtr(true), + }, + }) + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + + textInput := interactionService.CreateTextInput("name", &aspire.CreateTextInputOptions{ + Options: &aspire.CreateInteractionInputOptions{ + Label: "Name", + Description: "Your **name**", + EnableDescriptionMarkdown: aspire.BoolPtr(true), + Required: aspire.BoolPtr(true), + Placeholder: "Jane Doe", + Value: "Jane", + MaxLength: aspire.Float64Ptr(64), + Disabled: aspire.BoolPtr(false), + }, + }) + secretInput := interactionService.CreateSecretInput("password", &aspire.CreateSecretInputOptions{ + Options: &aspire.CreateInteractionInputOptions{Required: aspire.BoolPtr(true)}, + }) + booleanInput := interactionService.CreateBooleanInput("enabled", &aspire.CreateBooleanInputOptions{ + Options: &aspire.CreateInteractionInputOptions{Value: "true"}, + }) + numberInput := interactionService.CreateNumberInput("count", &aspire.CreateNumberInputOptions{ + Options: &aspire.CreateInteractionInputOptions{Value: "1"}, + }) + choiceInput := interactionService.CreateChoiceInput("color", &aspire.CreateChoiceInputOptions{ + Choices: map[string]string{"r": "Red", "g": "Green"}, + Options: &aspire.CreateInteractionInputOptions{AllowCustomChoice: aspire.BoolPtr(true)}, + }) + presetInput := interactionService.CreateTextInput("greeting").WithValue("hello") + sizeInput := interactionService.CreateChoiceInput("size").WithChoiceOptions(map[string]string{"s": "Small", "l": "Large"}) + dependentInput := interactionService.CreateChoiceInput("shade").WithDynamicLoading(func(loadContext aspire.InteractionInputLoadContext) { + inputName, _ := loadContext.GetInputName() + color, _ := loadContext.GetInputValue("color") + shades := map[string]string{"lime": "Lime", "forest": "Forest"} + if color == "r" { + shades = map[string]string{"crimson": "Crimson", "scarlet": "Scarlet"} + } + _ = loadContext.SetChoiceOptions(shades) + _ = loadContext.SetValue(inputName) + }, &aspire.WithDynamicLoadingOptions{ + Options: &aspire.DynamicLoadingOptions{AlwaysLoadOnStart: aspire.BoolPtr(true), DependsOnInputs: []string{"color"}}, + }) + + single, err := interactionService.PromptInput("Single input", "Enter a value.", interactionService.CreateTextInput("solo"), &aspire.PromptInputOptions{ + Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: "Save"}, + }) + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + + multi, err := interactionService.PromptInputs("Multiple inputs", "Fill out the form.", + []aspire.InteractionInputBuilder{textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput}, + &aspire.PromptInputsOptions{ + Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: "Submit", EnableMessageMarkdown: aspire.BoolPtr(true)}, + }) + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + + selectedColor := "" + for _, input := range multi.Inputs { + if input.Name == "color" { + selectedColor = input.Value + } + } + soloValue := "" + if single.Input != nil { + soloValue = single.Input.Value + } + + success := !confirmation.Canceled && + confirmation.Value != nil && *confirmation.Value && + !messageBox.Canceled && + !notification.Canceled && + !single.Canceled && + !multi.Canceled + + return &aspire.ExecuteCommandResult{ + Success: success, + Canceled: aspire.BoolPtr(multi.Canceled), + Message: aspire.StringPtr(fmt.Sprintf("color=%s solo=%s", selectedColor, soloValue)), + } + }) + app, err := builder.Build() if err != nil { log.Fatalf(aspire.FormatError(err)) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index 50a94c4b269..a144cd50543 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -303,6 +303,124 @@ void main() throws Exception { commandResult.setCanceled(result.getCanceled()); return commandResult; }); + // Exhaustive coverage of the remaining IInteractionService surface so every newly added member is + // exercised by the polyglot typecheck: all prompt overloads, every input factory and builder method, + // the dynamic-loading context accessors/setters, and the option/result DTO fields. + container.withCommand("interaction-showcase", "Interaction Showcase", (ctx) -> { + var interactionService = ctx.serviceProvider().getInteractionService(); + if (!interactionService.isAvailable()) { + var unavailable = new ExecuteCommandResult(); + unavailable.setSuccess(true); + unavailable.setMessage("Interaction service is not available."); + return unavailable; + } + + var confirmBox = new InteractionMessageBoxOptions(); + confirmBox.setPrimaryButtonText("Yes"); + confirmBox.setSecondaryButtonText("No"); + confirmBox.setShowSecondaryButton(true); + confirmBox.setShowDismiss(true); + confirmBox.setEnableMessageMarkdown(true); + confirmBox.setIntent(MessageIntent.CONFIRMATION); + var confirmation = interactionService.promptConfirmation("Confirm", "Proceed?", + new PromptConfirmationOptions().options(confirmBox)); + + var infoBox = new InteractionMessageBoxOptions(); + infoBox.setPrimaryButtonText("OK"); + infoBox.setIntent(MessageIntent.INFORMATION); + var messageBox = interactionService.promptMessageBox("Notice", "Read this.", + new PromptMessageBoxOptions().options(infoBox)); + + var notificationOptions = new InteractionNotificationOptions(); + notificationOptions.setIntent(MessageIntent.WARNING); + notificationOptions.setLinkText("Learn more"); + notificationOptions.setLinkUrl("https://aspire.dev"); + notificationOptions.setShowDismiss(true); + var notification = interactionService.promptNotification("Heads up", "Something happened.", + new PromptNotificationOptions().options(notificationOptions)); + + var textOptions = new CreateInteractionInputOptions(); + textOptions.setLabel("Name"); + textOptions.setDescription("Your **name**"); + textOptions.setEnableDescriptionMarkdown(true); + textOptions.setRequired(true); + textOptions.setPlaceholder("Jane Doe"); + textOptions.setValue("Jane"); + textOptions.setMaxLength(64.0); + textOptions.setDisabled(false); + var textInput = interactionService.createTextInput("name", textOptions); + + var secretOptions = new CreateInteractionInputOptions(); + secretOptions.setRequired(true); + var secretInput = interactionService.createSecretInput("password", secretOptions); + + var booleanOptions = new CreateInteractionInputOptions(); + booleanOptions.setValue("true"); + var booleanInput = interactionService.createBooleanInput("enabled", booleanOptions); + + var numberOptions = new CreateInteractionInputOptions(); + numberOptions.setValue("1"); + var numberInput = interactionService.createNumberInput("count", numberOptions); + + var choiceExtras = new CreateInteractionInputOptions(); + choiceExtras.setAllowCustomChoice(true); + var choiceInput = interactionService.createChoiceInput("color", + new CreateChoiceInputOptions().choices(Map.of("r", "Red", "g", "Green")).options(choiceExtras)); + + var presetInput = interactionService.createTextInput("greeting").withValue("hello"); + var sizeInput = interactionService.createChoiceInput("size") + .withChoiceOptions(Map.of("s", "Small", "l", "Large")); + + var dynamicLoadingOptions = new DynamicLoadingOptions(); + dynamicLoadingOptions.setAlwaysLoadOnStart(true); + dynamicLoadingOptions.setDependsOnInputs(new String[] { "color" }); + var dependentInput = interactionService.createChoiceInput("shade").withDynamicLoading((loadContext) -> { + var inputName = loadContext.getInputName(); + var color = loadContext.getInputValue("color"); + Map shades = "r".equals(color) + ? Map.of("crimson", "Crimson", "scarlet", "Scarlet") + : Map.of("lime", "Lime", "forest", "Forest"); + loadContext.setChoiceOptions(shades); + loadContext.setValue(inputName); + }, dynamicLoadingOptions); + + var singleDialogOptions = new InteractionInputsDialogOptions(); + singleDialogOptions.setPrimaryButtonText("Save"); + var single = interactionService.promptInput("Single input", "Enter a value.", + interactionService.createTextInput("solo"), + new PromptInputOptions().options(singleDialogOptions)); + + var multiDialogOptions = new InteractionInputsDialogOptions(); + multiDialogOptions.setPrimaryButtonText("Submit"); + multiDialogOptions.setEnableMessageMarkdown(true); + var multi = interactionService.promptInputs("Multiple inputs", "Fill out the form.", + new InteractionInputBuilder[] { textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput }, + new PromptInputsOptions().options(multiDialogOptions)); + + String selectedColor = ""; + if (multi.getInputs() != null) { + for (var input : multi.getInputs()) { + if ("color".equals(input.getName())) { + selectedColor = input.getValue(); + } + } + } + String soloValue = single.getInput() != null ? single.getInput().getValue() : ""; + + var success = !confirmation.getCanceled() + && Boolean.TRUE.equals(confirmation.getValue()) + && !messageBox.getCanceled() + && !notification.getCanceled() + && !single.getCanceled() + && !multi.getCanceled(); + + var commandResult = new ExecuteCommandResult(); + commandResult.setSuccess(success); + commandResult.setCanceled(multi.getCanceled()); + commandResult.setMessage("color=" + (selectedColor == null ? "" : selectedColor) + + " solo=" + (soloValue == null ? "" : soloValue)); + return commandResult; + }); container.withHttpCommand("/health", "Health Check"); var httpCmdOptions = new HttpCommandExportOptions(); httpCmdOptions.setMethodName("POST"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index 180dfe9ff4d..822c87c7c23 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -468,6 +468,114 @@ def load_zones(load_context): return {"success": not result.get("Canceled", False), "canceled": result.get("Canceled", False)} container.with_command("pick-zone", "Pick Zone", pick_zone_command) + # Exhaustive coverage of the remaining IInteractionService surface so every newly added member is + # exercised by the polyglot typecheck: all prompt overloads, every input factory and builder method, + # the dynamic-loading context accessors/setters, and the option/result DTO fields. + def interaction_showcase_command(ctx): + interaction_service = ctx.service_provider.get_interaction_service() + if not interaction_service.is_available(): + return {"success": True, "message": "Interaction service is not available."} + + confirmation = interaction_service.prompt_confirmation( + "Confirm", + "Proceed?", + options={ + "PrimaryButtonText": "Yes", + "SecondaryButtonText": "No", + "ShowSecondaryButton": True, + "ShowDismiss": True, + "EnableMessageMarkdown": True, + "Intent": "Confirmation", + } + ) + + message_box = interaction_service.prompt_message_box( + "Notice", + "Read this.", + options={"PrimaryButtonText": "OK", "Intent": "Information"} + ) + + notification = interaction_service.prompt_notification( + "Heads up", + "Something happened.", + options={ + "Intent": "Warning", + "LinkText": "Learn more", + "LinkUrl": "https://aspire.dev", + "ShowDismiss": True, + } + ) + + text_input = interaction_service.create_text_input( + "name", + options={ + "Label": "Name", + "Description": "Your **name**", + "EnableDescriptionMarkdown": True, + "Required": True, + "Placeholder": "Jane Doe", + "Value": "Jane", + "MaxLength": 64, + "Disabled": False, + } + ) + secret_input = interaction_service.create_secret_input("password", options={"Required": True}) + boolean_input = interaction_service.create_boolean_input("enabled", options={"Value": "true"}) + number_input = interaction_service.create_number_input("count", options={"Value": "1"}) + choice_input = interaction_service.create_choice_input( + "color", + choices={"r": "Red", "g": "Green"}, + options={"AllowCustomChoice": True} + ) + preset_input = interaction_service.create_text_input("greeting").with_value("hello") + size_input = interaction_service.create_choice_input("size").with_choice_options({"s": "Small", "l": "Large"}) + + def load_shade(load_context): + input_name = load_context.get_input_name() + color = load_context.get_input_value("color") + load_context.set_choice_options( + {"crimson": "Crimson", "scarlet": "Scarlet"} + if color == "r" + else {"lime": "Lime", "forest": "Forest"} + ) + load_context.set_value(input_name) + + dependent_input = interaction_service.create_choice_input("shade").with_dynamic_loading( + load_shade, + options={"AlwaysLoadOnStart": True, "DependsOnInputs": ["color"]} + ) + + single = interaction_service.prompt_input( + "Single input", + "Enter a value.", + interaction_service.create_text_input("solo"), + options={"PrimaryButtonText": "Save"} + ) + + multi = interaction_service.prompt_inputs( + "Multiple inputs", + "Fill out the form.", + [text_input, secret_input, boolean_input, number_input, choice_input, preset_input, size_input, dependent_input], + options={"PrimaryButtonText": "Submit", "EnableMessageMarkdown": True} + ) + + selected_color = next((i.get("Value") for i in (multi.get("Inputs") or []) if i.get("Name") == "color"), None) + solo_value = (single.get("Input") or {}).get("Value") + + success = (not confirmation.get("Canceled", False) + and confirmation.get("Value", False) + and not message_box.get("Canceled", False) + and not notification.get("Canceled", False) + and not single.get("Canceled", False) + and not multi.get("Canceled", False)) + + return { + "success": bool(success), + "canceled": multi.get("Canceled", False), + "message": f"color={selected_color or ''} solo={solo_value or ''}", + } + + container.with_command("interaction-showcase", "Interaction Showcase", interaction_showcase_command) # withHttpCommand container.with_http_command("/health", "Health Check") container.with_http_command("/api/reset", "Reset", options={"MethodName": "POST", "ConfirmationMessage": "Are you sure?"}) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index 5e297ef74c8..c12a857b7f0 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -9,6 +9,7 @@ import { HealthStatus, IconVariant, InputType, + MessageIntent, OtlpProtocol, ProbeType, ResourceCommandState, @@ -782,6 +783,104 @@ await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { return { success: !result.canceled, canceled: result.canceled }; }); +// Exhaustive coverage of the remaining IInteractionService surface so every newly added member is +// exercised by the polyglot typecheck: all prompt overloads, every input factory and builder method, +// the dynamic-loading context accessors/setters, and the option/result DTO fields. +await container.withCommand("interaction-showcase", "Interaction Showcase", async (ctx) => { + const interactionService = await ctx.serviceProvider().getInteractionService(); + + if (!(await interactionService.isAvailable())) { + return { success: true, message: "Interaction service is not available." }; + } + + const confirmation = await interactionService.promptConfirmation("Confirm", "Proceed?", { + options: { + primaryButtonText: "Yes", + secondaryButtonText: "No", + showSecondaryButton: true, + showDismiss: true, + enableMessageMarkdown: true, + intent: MessageIntent.Confirmation + } + }); + + const messageBox = await interactionService.promptMessageBox("Notice", "Read this.", { + options: { primaryButtonText: "OK", intent: MessageIntent.Information } + }); + + const notification = await interactionService.promptNotification("Heads up", "Something happened.", { + options: { + intent: MessageIntent.Warning, + linkText: "Learn more", + linkUrl: "https://aspire.dev", + showDismiss: true + } + }); + + const textInput = await interactionService.createTextInput("name", { + label: "Name", + description: "Your **name**", + enableDescriptionMarkdown: true, + required: true, + placeholder: "Jane Doe", + value: "Jane", + maxLength: 64, + disabled: false + }); + const secretInput = await interactionService.createSecretInput("password", { required: true }); + const booleanInput = await interactionService.createBooleanInput("enabled", { value: "true" }); + const numberInput = await interactionService.createNumberInput("count", { value: "1" }); + const choiceInput = await interactionService.createChoiceInput("color", { + choices: { "r": "Red", "g": "Green" }, + options: { allowCustomChoice: true } + }); + const presetInput = await interactionService.createTextInput("greeting").withValue("hello"); + const sizeInput = await interactionService + .createChoiceInput("size") + .withChoiceOptions({ "s": "Small", "l": "Large" }); + const dependentInput = await interactionService + .createChoiceInput("shade") + .withDynamicLoading(async (loadContext) => { + const inputName = await loadContext.getInputName(); + const color = await loadContext.getInputValue("color"); + + await loadContext.setChoiceOptions(color === "r" + ? { "crimson": "Crimson", "scarlet": "Scarlet" } + : { "lime": "Lime", "forest": "Forest" }); + await loadContext.setValue(inputName); + }, { + alwaysLoadOnStart: true, + dependsOnInputs: ["color"] + }); + + const single = await interactionService.promptInput( + "Single input", + "Enter a value.", + interactionService.createTextInput("solo"), + { options: { primaryButtonText: "Save" } }); + + const multi = await interactionService.promptInputs( + "Multiple inputs", + "Fill out the form.", + [textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput], + { options: { primaryButtonText: "Submit", enableMessageMarkdown: true } }); + + const selectedColor = multi.inputs?.find(input => input.name === "color")?.value; + const soloValue = single.input?.value; + + const success = !confirmation.canceled + && confirmation.value === true + && !messageBox.canceled + && !notification.canceled + && !single.canceled + && !multi.canceled; + + return { + success, + canceled: multi.canceled, + message: `color=${selectedColor ?? ""} solo=${soloValue ?? ""}` + }; +}); // withProcessCommand await container.withProcessCommand("dotnet-version", "Show .NET version", { From 9eead7c3fae8c6622eccc6f25dcce786a3959296 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 09:18:03 -0700 Subject: [PATCH 03/20] Fix TypeScript type error in pick-zone dynamic-loading callback tsc (strict) widened the const zones ternary into a union of object literals with mutually-exclusive optional undefined properties, which is not assignable to setChoiceOptions(Record). Annotate the variable as Record so each branch is contextually typed. Verified by generating the SDK modules with 'aspire restore' and running 'tsc --noEmit' on the TypeScript apphost (now passes clean). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index c12a857b7f0..032066f5f2f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -769,7 +769,7 @@ await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { .withDynamicLoading(async (loadContext) => { const region = await loadContext.getInputValue("region"); - const zones = region === "eu" + const zones: Record = region === "eu" ? { "eu-west": "EU West", "eu-north": "EU North" } : { "us-east": "US East", "us-west": "US West" }; From 1713dfc65acc2cbba5132685f444471a9069642e Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 09:29:43 -0700 Subject: [PATCH 04/20] Strip non-serializable dynamic-loading callback from interaction results PromptInput/PromptInputs echoed the engine's InteractionInput instances back to the polyglot caller. When an input was created via withDynamicLoading, the input still carried its LoadCallback Func on DynamicLoading, which System.Text.Json cannot serialize across the ATS/JSON-RPC boundary, surfacing as a failed 'promptInputs' invocation. Project result inputs onto callback-free copies so only data fields (name, value, options, etc.) cross the boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 30 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index 7c01dc2c737..9b044616f59 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -214,6 +214,30 @@ internal static IReadOnlyList> ToOptionList(IReadOn return list; } + + // The engine returns the same InteractionInput instances that the builders own, and those still carry the + // dynamic-loading delegate on DynamicLoading.LoadCallback. That delegate is a .NET Func that cannot be + // serialized across the ATS/JSON-RPC boundary, so project result inputs onto callback-free copies before they + // are sent back to the polyglot caller. The caller only consumes data fields such as Name, Value and Options. + internal static InteractionInput ToResultInput(InteractionInput input) + { + return new InteractionInput + { + Name = input.Name, + Label = input.Label, + Description = input.Description, + EnableDescriptionMarkdown = input.EnableDescriptionMarkdown, + InputType = input.InputType, + Required = input.Required, + Options = input.Options, + Value = input.Value, + Placeholder = input.Placeholder, + AllowCustomChoice = input.AllowCustomChoice, + Disabled = input.Disabled, + MaxLength = input.MaxLength, + // DynamicLoading is intentionally omitted: it holds the non-serializable LoadCallback delegate. + }; + } } /// @@ -641,7 +665,7 @@ internal static InputInteractionResult From(InteractionResult return new InputInteractionResult { Canceled = result.Canceled, - Input = result.Canceled ? null : result.Data, + Input = result.Canceled || result.Data is null ? null : InteractionExports.ToResultInput(result.Data), }; } } @@ -667,7 +691,9 @@ internal static InputsInteractionResult From(InteractionResult Date: Fri, 5 Jun 2026 10:00:28 -0700 Subject: [PATCH 05/20] Expose inputs-dialog validation callback to polyglot app hosts The native InputsDialogInteractionOptions.ValidationCallback was not reachable from the polyglot surface: it is a Func and InteractionInputsDialogOptions is a serializable DTO that cannot carry delegates. Add an optional validationCallback parameter to the ATS PromptInput/PromptInputs exports (the callback travels as a method argument, not a DTO field) and bridge it onto the native options via a fresh instance so the shared Default singleton is never mutated. The callback receives the already-curated, exported InputsDialogValidationContext handle (its IServiceProvider is [AspireExportIgnore]'d), so polyglot callers can read submitted inputs and call addValidationError. Regenerate all 5 codegen snapshots + ats.txt and exercise the new callback in the interaction-showcase command of every polyglot test bench app (TS/Python/Go/Java). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 32 ++++++++++++++- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 4 +- ...TwoPassScanningGeneratedAspire.verified.go | 18 +++++++++ ...oPassScanningGeneratedAspire.verified.java | 40 +++++++++++++++++-- ...TwoPassScanningGeneratedAspire.verified.py | 8 +++- ...TwoPassScanningGeneratedAspire.verified.rs | 8 +++- ...TwoPassScanningGeneratedAspire.verified.ts | 16 ++++++++ .../Aspire.Hosting/Go/apphost.go | 16 ++++++++ .../Aspire.Hosting/Java/AppHost.java | 16 +++++++- .../Aspire.Hosting/Python/apphost.py | 18 ++++++++- .../Aspire.Hosting/TypeScript/apphost.mts | 22 +++++++++- 11 files changed, 180 insertions(+), 18 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index 9b044616f59..e9c0e843b2b 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -110,18 +110,21 @@ public static async Task PromptInput( string? message, InteractionInputBuilder input, InteractionInputsDialogOptions? options = null, + Func? validationCallback = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(interactionService); ArgumentNullException.ThrowIfNull(input); - var result = await interactionService.PromptInputAsync(title, message, input.Input, options?.ToOptions(), cancellationToken).ConfigureAwait(false); + var result = await interactionService.PromptInputAsync(title, message, input.Input, BuildDialogOptions(options, validationCallback), cancellationToken).ConfigureAwait(false); return InputInteractionResult.From(result); } /// /// Prompts the user for multiple inputs. /// + // Prompts can invoke dynamic-loading and validation callbacks that re-enter the remote host through ATS, so the + // synchronous invocation path must run on a background thread to keep the JSON-RPC loop processing nested callbacks. [AspireExport(RunSyncOnBackgroundThread = true)] public static async Task PromptInputs( this IInteractionService interactionService, @@ -129,6 +132,7 @@ public static async Task PromptInputs( string? message, InteractionInputBuilder[] inputs, InteractionInputsDialogOptions? options = null, + Func? validationCallback = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(interactionService); @@ -140,10 +144,34 @@ public static async Task PromptInputs( interactionInputs[i] = inputs[i].Input; } - var result = await interactionService.PromptInputsAsync(title, message, interactionInputs, options?.ToOptions(), cancellationToken).ConfigureAwait(false); + var result = await interactionService.PromptInputsAsync(title, message, interactionInputs, BuildDialogOptions(options, validationCallback), cancellationToken).ConfigureAwait(false); return InputsInteractionResult.From(result); } + // Bridges the polyglot dialog options DTO and an optional validation callback to the native dialog options. + // The validation callback is supplied as a method argument (not on the DTO) because delegates cannot be + // serialized across the ATS boundary. The native InputsDialogValidationContext is already an exported, curated + // handle (its IServiceProvider is [AspireExportIgnore]'d), so it can be handed to the polyglot callback directly. + private static InputsDialogInteractionOptions? BuildDialogOptions( + InteractionInputsDialogOptions? options, + Func? validationCallback) + { + if (options is null && validationCallback is null) + { + return null; + } + + // Never mutate the shared InputsDialogInteractionOptions.Default singleton; ToOptions() already returns a + // fresh instance, so only allocate a new one when no DTO options were provided. + var nativeOptions = options?.ToOptions() ?? new InputsDialogInteractionOptions(); + if (validationCallback is not null) + { + nativeOptions.ValidationCallback = validationCallback; + } + + return nativeOptions; + } + // The input factories hang off IInteractionService so the ATS scanner treats the service handle as the // receiver (polyglot: interactionService.createTextInput(...)). The receiver itself is unused because inputs // are independent of the service, so suppress the unused-parameter analyzer for the factory block. diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 6eed1088944..9661c190523 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -648,8 +648,8 @@ Aspire.Hosting/ProjectResourceOptions.setExcludeKestrelEndpoints(context: Aspire Aspire.Hosting/ProjectResourceOptions.setExcludeLaunchProfile(context: Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions, value: boolean) -> Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions Aspire.Hosting/ProjectResourceOptions.setLaunchProfileName(context: Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions, value: string) -> Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions Aspire.Hosting/promptConfirmation(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult -Aspire.Hosting/promptInput(title: string, message: string, input: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult -Aspire.Hosting/promptInputs(title: string, message: string, inputs: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder[], options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult +Aspire.Hosting/promptInput(title: string, message: string, input: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, validationCallback?: callback, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult +Aspire.Hosting/promptInputs(title: string, message: string, inputs: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder[], options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, validationCallback?: callback, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult Aspire.Hosting/promptMessageBox(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult Aspire.Hosting/promptNotification(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionNotificationOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult Aspire.Hosting/publishAsConnectionString() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 05ae188883d..6d5d383ce6b 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -16372,6 +16372,14 @@ func (s *interactionService) PromptInput(title string, message string, input Int if opt != nil { merged = deepUpdate(merged, opt) } } for k, v := range merged.ToMap() { reqArgs[k] = v } + if merged.ValidationCallback != nil { + cb := merged.ValidationCallback + shim := func(args ...any) any { + cb(callbackArg[InputsDialogValidationContext](args, 0)) + return nil + } + reqArgs["validationCallback"] = s.client.registerCallback(shim) + } if merged.CancellationToken != nil { ctx = merged.CancellationToken.Context() if id := s.client.registerCancellation(merged.CancellationToken); id != "" { @@ -16403,6 +16411,14 @@ func (s *interactionService) PromptInputs(title string, message string, inputs [ if opt != nil { merged = deepUpdate(merged, opt) } } for k, v := range merged.ToMap() { reqArgs[k] = v } + if merged.ValidationCallback != nil { + cb := merged.ValidationCallback + shim := func(args ...any) any { + cb(callbackArg[InputsDialogValidationContext](args, 0)) + return nil + } + reqArgs["validationCallback"] = s.client.registerCallback(shim) + } if merged.CancellationToken != nil { ctx = merged.CancellationToken.Context() if id := s.client.registerCancellation(merged.CancellationToken); id != "" { @@ -26706,6 +26722,7 @@ func (o *PromptNotificationOptions) ToMap() map[string]any { // PromptInputOptions carries optional parameters for PromptInput. type PromptInputOptions struct { Options *InteractionInputsDialogOptions `json:"options,omitempty"` + ValidationCallback func(arg InputsDialogValidationContext) `json:"-"` CancellationToken *CancellationToken `json:"-"` } @@ -26719,6 +26736,7 @@ func (o *PromptInputOptions) ToMap() map[string]any { // PromptInputsOptions carries optional parameters for PromptInputs. type PromptInputsOptions struct { Options *InteractionInputsDialogOptions `json:"options,omitempty"` + ValidationCallback func(arg InputsDialogValidationContext) `json:"-"` CancellationToken *CancellationToken `json:"-"` } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 895c3d4c3c9..3969966ff60 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -13884,8 +13884,9 @@ private BoolInteractionResult promptNotificationImpl(String title, String messag /** Prompts the user for a single input. */ public InputInteractionResult promptInput(String title, String message, InteractionInputBuilder input, PromptInputOptions options) { var options = options == null ? null : options.getOptions(); + var validationCallback = options == null ? null : options.getValidationCallback(); var cancellationToken = options == null ? null : options.getCancellationToken(); - return promptInputImpl(title, message, input, options, cancellationToken); + return promptInputImpl(title, message, input, options, validationCallback, cancellationToken); } public InputInteractionResult promptInput(String title, String message, HandleWrapperBase input, PromptInputOptions options) { @@ -13901,7 +13902,7 @@ public InputInteractionResult promptInput(String title, String message, HandleWr } /** Prompts the user for a single input. */ - private InputInteractionResult promptInputImpl(String title, String message, InteractionInputBuilder input, InteractionInputsDialogOptions options, CancellationToken cancellationToken) { + private InputInteractionResult promptInputImpl(String title, String message, InteractionInputBuilder input, InteractionInputsDialogOptions options, AspireAction1 validationCallback, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); reqArgs.put("title", AspireClient.serializeValue(title)); @@ -13910,6 +13911,14 @@ private InputInteractionResult promptInputImpl(String title, String message, Int if (options != null) { reqArgs.put("options", AspireClient.serializeValue(options)); } + var validationCallbackId = validationCallback == null ? null : getClient().registerCallback(args -> { + var arg = (InputsDialogValidationContext) args[0]; + validationCallback.invoke(arg); + return null; + }); + if (validationCallbackId != null) { + reqArgs.put("validationCallback", validationCallbackId); + } if (cancellationToken != null) { reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); } @@ -13920,8 +13929,9 @@ private InputInteractionResult promptInputImpl(String title, String message, Int /** Prompts the user for multiple inputs. */ public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs, PromptInputsOptions options) { var options = options == null ? null : options.getOptions(); + var validationCallback = options == null ? null : options.getValidationCallback(); var cancellationToken = options == null ? null : options.getCancellationToken(); - return promptInputsImpl(title, message, inputs, options, cancellationToken); + return promptInputsImpl(title, message, inputs, options, validationCallback, cancellationToken); } public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs) { @@ -13929,7 +13939,7 @@ public InputsInteractionResult promptInputs(String title, String message, Intera } /** Prompts the user for multiple inputs. */ - private InputsInteractionResult promptInputsImpl(String title, String message, InteractionInputBuilder[] inputs, InteractionInputsDialogOptions options, CancellationToken cancellationToken) { + private InputsInteractionResult promptInputsImpl(String title, String message, InteractionInputBuilder[] inputs, InteractionInputsDialogOptions options, AspireAction1 validationCallback, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); reqArgs.put("title", AspireClient.serializeValue(title)); @@ -13938,6 +13948,14 @@ private InputsInteractionResult promptInputsImpl(String title, String message, I if (options != null) { reqArgs.put("options", AspireClient.serializeValue(options)); } + var validationCallbackId = validationCallback == null ? null : getClient().registerCallback(args -> { + var arg = (InputsDialogValidationContext) args[0]; + validationCallback.invoke(arg); + return null; + }); + if (validationCallbackId != null) { + reqArgs.put("validationCallback", validationCallbackId); + } if (cancellationToken != null) { reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); } @@ -18657,6 +18675,7 @@ public PromptConfirmationOptions cancellationToken(CancellationToken value) { /** Options for PromptInput. */ public final class PromptInputOptions { private InteractionInputsDialogOptions options; + private AspireAction1 validationCallback; private CancellationToken cancellationToken; public InteractionInputsDialogOptions getOptions() { return options; } @@ -18665,6 +18684,12 @@ public PromptInputOptions options(InteractionInputsDialogOptions value) { return this; } + public AspireAction1 getValidationCallback() { return validationCallback; } + public PromptInputOptions validationCallback(AspireAction1 value) { + this.validationCallback = value; + return this; + } + public CancellationToken getCancellationToken() { return cancellationToken; } public PromptInputOptions cancellationToken(CancellationToken value) { this.cancellationToken = value; @@ -18684,6 +18709,7 @@ public PromptInputOptions cancellationToken(CancellationToken value) { /** Options for PromptInputs. */ public final class PromptInputsOptions { private InteractionInputsDialogOptions options; + private AspireAction1 validationCallback; private CancellationToken cancellationToken; public InteractionInputsDialogOptions getOptions() { return options; } @@ -18692,6 +18718,12 @@ public PromptInputsOptions options(InteractionInputsDialogOptions value) { return this; } + public AspireAction1 getValidationCallback() { return validationCallback; } + public PromptInputsOptions validationCallback(AspireAction1 value) { + this.validationCallback = value; + return this; + } + public CancellationToken getCancellationToken() { return cancellationToken; } public PromptInputsOptions cancellationToken(CancellationToken value) { this.cancellationToken = value; diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 72bcb60dbf6..c6345a23a31 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -2951,7 +2951,7 @@ def prompt_notification(self, title: str, message: str, *, options: InteractionN ) return typing.cast(BoolInteractionResult, result) - def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, *, options: InteractionInputsDialogOptions | None = None, timeout: int | None = None) -> InputInteractionResult: + def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, *, options: InteractionInputsDialogOptions | None = None, validation_callback: typing.Callable[[InputsDialogValidationContext], None] | None = None, timeout: int | None = None) -> InputInteractionResult: """Prompts the user for a single input.""" rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} rpc_args['title'] = title @@ -2959,6 +2959,8 @@ def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, rpc_args['input'] = input if options is not None: rpc_args['options'] = options + if validation_callback is not None: + rpc_args['validationCallback'] = self._client.register_callback(validation_callback) if timeout is not None: rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) result = self._client.invoke_capability( @@ -2967,7 +2969,7 @@ def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, ) return typing.cast(InputInteractionResult, result) - def prompt_inputs(self, title: str, message: str, inputs: typing.Iterable[InteractionInputBuilder], *, options: InteractionInputsDialogOptions | None = None, timeout: int | None = None) -> InputsInteractionResult: + def prompt_inputs(self, title: str, message: str, inputs: typing.Iterable[InteractionInputBuilder], *, options: InteractionInputsDialogOptions | None = None, validation_callback: typing.Callable[[InputsDialogValidationContext], None] | None = None, timeout: int | None = None) -> InputsInteractionResult: """Prompts the user for multiple inputs.""" rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} rpc_args['title'] = title @@ -2975,6 +2977,8 @@ def prompt_inputs(self, title: str, message: str, inputs: typing.Iterable[Intera rpc_args['inputs'] = inputs if options is not None: rpc_args['options'] = options + if validation_callback is not None: + rpc_args['validationCallback'] = self._client.register_callback(validation_callback) if timeout is not None: rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) result = self._client.invoke_capability( diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index f99678d7cd9..b6100dc1f69 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -10989,7 +10989,7 @@ impl IInteractionService { } /// Prompts the user for a single input. - pub fn prompt_input(&self, title: &str, message: &str, input: &InteractionInputBuilder, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { + pub fn prompt_input(&self, title: &str, message: &str, input: &InteractionInputBuilder, options: Option, validation_callback: impl Fn(Vec) -> Value + Send + Sync + 'static, cancellation_token: Option<&CancellationToken>) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("interactionService".to_string(), self.handle.to_json()); args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); @@ -10998,6 +10998,8 @@ impl IInteractionService { if let Some(ref v) = options { args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } + let callback_id = register_callback(validation_callback); + args.insert("validationCallback".to_string(), Value::String(callback_id)); if let Some(token) = cancellation_token { let token_id = register_cancellation(token, self.client.clone()); args.insert("cancellationToken".to_string(), Value::String(token_id)); @@ -11007,7 +11009,7 @@ impl IInteractionService { } /// Prompts the user for multiple inputs. - pub fn prompt_inputs(&self, title: &str, message: &str, inputs: Vec, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { + pub fn prompt_inputs(&self, title: &str, message: &str, inputs: Vec, options: Option, validation_callback: impl Fn(Vec) -> Value + Send + Sync + 'static, cancellation_token: Option<&CancellationToken>) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("interactionService".to_string(), self.handle.to_json()); args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); @@ -11017,6 +11019,8 @@ impl IInteractionService { if let Some(ref v) = options { args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } + let callback_id = register_callback(validation_callback); + args.insert("validationCallback".to_string(), Value::String(callback_id)); if let Some(token) = cancellation_token { let token_id = register_cancellation(token, self.client.clone()); args.insert("cancellationToken".to_string(), Value::String(token_id)); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index d6a8f79559c..24436b161d2 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -1432,11 +1432,13 @@ export interface PromptConfirmationOptions { export interface PromptInputOptions { options?: InteractionInputsDialogOptions; + validationCallback?: (arg: InputsDialogValidationContext) => Promise; cancellationToken?: AbortSignal | CancellationToken; } export interface PromptInputsOptions { options?: InteractionInputsDialogOptions; + validationCallback?: (arg: InputsDialogValidationContext) => Promise; cancellationToken?: AbortSignal | CancellationToken; } @@ -11248,10 +11250,17 @@ class InteractionServiceImpl implements InteractionService { */ async promptInput(title: string, message: string, input: Awaitable, optionsBag?: PromptInputOptions): Promise { const options = optionsBag?.options; + const validationCallback = optionsBag?.validationCallback; const cancellationToken = optionsBag?.cancellationToken; + const validationCallbackId = validationCallback ? registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as InputsDialogValidationContextHandle; + const arg = new InputsDialogValidationContextImpl(argHandle, this._client); + await validationCallback(arg); + }) : undefined; input = isPromiseLike(input) ? await input : input; const rpcArgs: Record = { interactionService: this._handle, title, message, input }; if (options !== undefined) rpcArgs.options = options; + if (validationCallback !== undefined) rpcArgs.validationCallback = validationCallbackId; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); return await this._client.invokeCapability( 'Aspire.Hosting/promptInput', @@ -11265,9 +11274,16 @@ class InteractionServiceImpl implements InteractionService { */ async promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], optionsBag?: PromptInputsOptions): Promise { const options = optionsBag?.options; + const validationCallback = optionsBag?.validationCallback; const cancellationToken = optionsBag?.cancellationToken; + const validationCallbackId = validationCallback ? registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as InputsDialogValidationContextHandle; + const arg = new InputsDialogValidationContextImpl(argHandle, this._client); + await validationCallback(arg); + }) : undefined; const rpcArgs: Record = { interactionService: this._handle, title, message, inputs }; if (options !== undefined) rpcArgs.options = options; + if (validationCallback !== undefined) rpcArgs.validationCallback = validationCallbackId; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); return await this._client.invokeCapability( 'Aspire.Hosting/promptInputs', diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 9a1d95de4ce..49b8ac0d979 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -714,6 +714,14 @@ ENTRYPOINT ["dotnet", "App.dll"] single, err := interactionService.PromptInput("Single input", "Enter a value.", interactionService.CreateTextInput("solo"), &aspire.PromptInputOptions{ Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: "Save"}, + ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { + inputs, _ := validationContext.Inputs().ToArray() + for _, input := range inputs { + if input.Name == "solo" && input.Value == "" { + _ = validationContext.AddValidationError("solo", "A value is required.") + } + } + }, }) if err != nil { return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} @@ -723,6 +731,14 @@ ENTRYPOINT ["dotnet", "App.dll"] []aspire.InteractionInputBuilder{textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput}, &aspire.PromptInputsOptions{ Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: "Submit", EnableMessageMarkdown: aspire.BoolPtr(true)}, + ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { + inputs, _ := validationContext.Inputs().ToArray() + for _, input := range inputs { + if input.Name == "name" && input.Value == "bad" { + _ = validationContext.AddValidationError("name", "Name cannot be 'bad'.") + } + } + }, }) if err != nil { return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index a144cd50543..08c224a7333 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -388,14 +388,26 @@ void main() throws Exception { singleDialogOptions.setPrimaryButtonText("Save"); var single = interactionService.promptInput("Single input", "Enter a value.", interactionService.createTextInput("solo"), - new PromptInputOptions().options(singleDialogOptions)); + new PromptInputOptions().options(singleDialogOptions).validationCallback((validationContext) -> { + for (var input : validationContext.inputs().toArray()) { + if ("solo".equals(input.getName()) && (input.getValue() == null || input.getValue().isEmpty())) { + validationContext.addValidationError("solo", "A value is required."); + } + } + })); var multiDialogOptions = new InteractionInputsDialogOptions(); multiDialogOptions.setPrimaryButtonText("Submit"); multiDialogOptions.setEnableMessageMarkdown(true); var multi = interactionService.promptInputs("Multiple inputs", "Fill out the form.", new InteractionInputBuilder[] { textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput }, - new PromptInputsOptions().options(multiDialogOptions)); + new PromptInputsOptions().options(multiDialogOptions).validationCallback((validationContext) -> { + for (var input : validationContext.inputs().toArray()) { + if ("name".equals(input.getName()) && "bad".equals(input.getValue())) { + validationContext.addValidationError("name", "Name cannot be 'bad'."); + } + } + })); String selectedColor = ""; if (multi.getInputs() != null) { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index 822c87c7c23..43f1aa3db3e 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -545,18 +545,32 @@ def load_shade(load_context): options={"AlwaysLoadOnStart": True, "DependsOnInputs": ["color"]} ) + def validate_solo(validation_context): + inputs = list(validation_context.inputs().to_array()) + solo = next((i for i in inputs if i.get("Name") == "solo"), None) + if not (solo or {}).get("Value"): + validation_context.add_validation_error("solo", "A value is required.") + single = interaction_service.prompt_input( "Single input", "Enter a value.", interaction_service.create_text_input("solo"), - options={"PrimaryButtonText": "Save"} + options={"PrimaryButtonText": "Save"}, + validation_callback=validate_solo, ) + def validate_form(validation_context): + inputs = list(validation_context.inputs().to_array()) + name = next((i for i in inputs if i.get("Name") == "name"), None) + if (name or {}).get("Value") == "bad": + validation_context.add_validation_error("name", "Name cannot be 'bad'.") + multi = interaction_service.prompt_inputs( "Multiple inputs", "Fill out the form.", [text_input, secret_input, boolean_input, number_input, choice_input, preset_input, size_input, dependent_input], - options={"PrimaryButtonText": "Submit", "EnableMessageMarkdown": True} + options={"PrimaryButtonText": "Submit", "EnableMessageMarkdown": True}, + validation_callback=validate_form, ) selected_color = next((i.get("Value") for i in (multi.get("Inputs") or []) if i.get("Name") == "color"), None) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index 032066f5f2f..eaf1d30edda 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -857,13 +857,31 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn "Single input", "Enter a value.", interactionService.createTextInput("solo"), - { options: { primaryButtonText: "Save" } }); + { + options: { primaryButtonText: "Save" }, + validationCallback: async (validationContext) => { + const inputs = await (await validationContext.inputs()).toArray(); + const solo = inputs.find(input => input.name === "solo"); + if (!solo?.value) { + await validationContext.addValidationError("solo", "A value is required."); + } + } + }); const multi = await interactionService.promptInputs( "Multiple inputs", "Fill out the form.", [textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput], - { options: { primaryButtonText: "Submit", enableMessageMarkdown: true } }); + { + options: { primaryButtonText: "Submit", enableMessageMarkdown: true }, + validationCallback: async (validationContext) => { + const inputs = await (await validationContext.inputs()).toArray(); + const name = inputs.find(input => input.name === "name"); + if (name?.value === "bad") { + await validationContext.addValidationError("name", "Name cannot be 'bad'."); + } + } + }); const selectedColor = multi.inputs?.find(input => input.name === "color")?.value; const soloValue = single.input?.value; From bbb5cae92230fc0b10d7ffce1c2aea71e4b650df Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 10:37:00 -0700 Subject: [PATCH 06/20] Hide non-serializable DynamicLoading from polyglot interaction input surface InteractionInput.DynamicLoading holds an InputLoadOptions with a non-serializable LoadCallback delegate, and the dynamic-loading payload is always stripped from interaction results at runtime (InteractionExports.ToResultInput). Polyglot app hosts configure dynamic loading exclusively through InteractionInputBuilder.WithDynamicLoading and never read this property back, so advertising it on the result DTO described a value that is always null on the wire (and pulled the otherwise-unused InputLoadOptions type into the surface). Annotate the property with [AspireExportIgnore] so it is excluded from the ATS surface, regenerate ats.txt and the Go/Java/Python/Rust snapshots (which drop the generated field), and remove the matching hand-authored line from the TypeScript base.mts so all five languages stay consistent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Resources/base.mts | 1 - src/Aspire.Hosting/IInteractionService.cs | 5 +++++ src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 1 - .../Snapshots/TwoPassScanningGeneratedAspire.verified.go | 2 -- .../Snapshots/TwoPassScanningGeneratedAspire.verified.java | 6 ------ .../Snapshots/TwoPassScanningGeneratedAspire.verified.py | 1 - .../Snapshots/TwoPassScanningGeneratedAspire.verified.rs | 5 ----- 7 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts index 12b183f2161..8a4e5a4763c 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts @@ -360,7 +360,6 @@ export interface InteractionInput { inputType?: InputType; required?: boolean; options?: InteractionInputOption[]; - dynamicLoading?: unknown; value?: string; placeholder?: string; allowCustomChoice?: boolean; diff --git a/src/Aspire.Hosting/IInteractionService.cs b/src/Aspire.Hosting/IInteractionService.cs index 9dc6643c925..05ab3eefd82 100644 --- a/src/Aspire.Hosting/IInteractionService.cs +++ b/src/Aspire.Hosting/IInteractionService.cs @@ -327,6 +327,11 @@ public bool Required /// Dynamic loading is used to load data and update inputs after a prompt has started. /// It can also be used to reload data and update inputs after a dependant input has changed. /// + // Excluded from the ATS surface: InputLoadOptions holds a non-serializable LoadCallback delegate, and the + // dynamic-loading payload is always stripped from interaction results at runtime (see InteractionExports.ToResultInput). + // Polyglot app hosts configure dynamic loading through InteractionInputBuilder.WithDynamicLoading, never by reading + // this property back, so advertising it on the result DTO would describe a value that is always null on the wire. + [AspireExportIgnore(Reason = "InputLoadOptions carries a non-serializable callback and is never populated on interaction results.")] public InputLoadOptions? DynamicLoading { get => _dynamicLoading; diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 9661c190523..8288c14c14d 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -285,7 +285,6 @@ Aspire.Hosting/Aspire.Hosting.InteractionInput # Represents an input for an inte AllowCustomChoice?: boolean # Gets a value indicating whether a custom choice is allowed. Only used by `Choice` inputs. Description?: string # Gets or sets the description for the input. Disabled: boolean # Gets or sets a value indicating whether a custom choice is allowed. Only used by `Choice` inputs. - DynamicLoading?: Aspire.Hosting/Aspire.Hosting.InputLoadOptions # Gets the `InputLoadOptions` for the input. Dynamic loading is used to load data and update inputs after a prompt has started. It can also be used to reload data and update inputs after a dependant input has changed. EnableDescriptionMarkdown?: boolean # Gets or sets a value indicating whether the description should be rendered as Markdown. Setting this to `true` allows a description to contain Markdown elements such as links, text decoration and lists. InputType: enum:Aspire.Hosting.InputType # Gets or sets the type of the input. Label?: string # Gets or sets the label for the input. If not specified, the name will be used as the label. diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 6d5d383ce6b..5b9d97a5ed5 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -255,7 +255,6 @@ type InteractionInput struct { InputType InputType `json:"InputType,omitempty"` Required *bool `json:"Required,omitempty"` Options []any `json:"Options,omitempty"` - DynamicLoading any `json:"DynamicLoading,omitempty"` Value string `json:"Value,omitempty"` Placeholder *string `json:"Placeholder,omitempty"` AllowCustomChoice *bool `json:"AllowCustomChoice,omitempty"` @@ -273,7 +272,6 @@ func (d *InteractionInput) ToMap() map[string]any { m["InputType"] = serializeValue(d.InputType) if d.Required != nil { m["Required"] = serializeValue(d.Required) } if d.Options != nil { m["Options"] = serializeValue(d.Options) } - if d.DynamicLoading != nil { m["DynamicLoading"] = serializeValue(d.DynamicLoading) } m["Value"] = serializeValue(d.Value) if d.Placeholder != nil { m["Placeholder"] = serializeValue(d.Placeholder) } if d.AllowCustomChoice != nil { m["AllowCustomChoice"] = serializeValue(d.AllowCustomChoice) } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 3969966ff60..4068fa3c5d7 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -14959,7 +14959,6 @@ public class InteractionInput implements JsonSerializable { private InputType inputType; private Boolean required; private Object[] options; - private Object dynamicLoading; private String value; private String placeholder; private Boolean allowCustomChoice; @@ -14980,8 +14979,6 @@ public class InteractionInput implements JsonSerializable { public void setRequired(Boolean value) { this.required = value; } public Object[] getOptions() { return options; } public void setOptions(Object[] value) { this.options = value; } - public Object getDynamicLoading() { return dynamicLoading; } - public void setDynamicLoading(Object value) { this.dynamicLoading = value; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getPlaceholder() { return placeholder; } @@ -15010,8 +15007,6 @@ public static InteractionInput fromMap(Map map) { value.setRequired(requiredValue == null ? null : (Boolean) requiredValue); var optionsValue = map.get("Options"); value.setOptions((Object[]) optionsValue); - var dynamicLoadingValue = map.get("DynamicLoading"); - value.setDynamicLoading(dynamicLoadingValue); var valueValue = map.get("Value"); value.setValue(valueValue == null ? null : (String) valueValue); var placeholderValue = map.get("Placeholder"); @@ -15034,7 +15029,6 @@ public Map toMap() { map.put("InputType", AspireClient.serializeValue(inputType)); map.put("Required", AspireClient.serializeValue(required)); map.put("Options", AspireClient.serializeValue(options)); - map.put("DynamicLoading", AspireClient.serializeValue(dynamicLoading)); map.put("Value", AspireClient.serializeValue(value)); map.put("Placeholder", AspireClient.serializeValue(placeholder)); map.put("AllowCustomChoice", AspireClient.serializeValue(allowCustomChoice)); diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index c6345a23a31..0526c4e5286 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1871,7 +1871,6 @@ class InteractionInput(typing.TypedDict, total=False): InputType: InputType Required: bool Options: typing.Iterable[typing.Any] - DynamicLoading: typing.Any Value: str | None Placeholder: str | None AllowCustomChoice: bool diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index b6100dc1f69..91f9eab75a6 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -593,8 +593,6 @@ pub struct InteractionInput { pub required: Option, #[serde(rename = "Options")] pub options: Vec, - #[serde(rename = "DynamicLoading", skip_serializing_if = "Option::is_none")] - pub dynamic_loading: Option, #[serde(rename = "Value")] pub value: String, #[serde(rename = "Placeholder", skip_serializing_if = "Option::is_none")] @@ -625,9 +623,6 @@ impl InteractionInput { map.insert("Required".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } map.insert("Options".to_string(), serde_json::to_value(&self.options).unwrap_or(Value::Null)); - if let Some(ref v) = self.dynamic_loading { - map.insert("DynamicLoading".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } map.insert("Value".to_string(), serde_json::to_value(&self.value).unwrap_or(Value::Null)); if let Some(ref v) = self.placeholder { map.insert("Placeholder".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); From 97cfe11305af03f30b7fc39d37f4c481d6a2fc6e Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 11:27:10 -0700 Subject: [PATCH 07/20] Use ordered choice options for polyglot interaction inputs The polyglot choice input factories (createChoiceInput, withChoiceOptions, setChoiceOptions) accepted an IReadOnlyDictionary, which does not guarantee option order across the ATS boundary. Dropdown option order is user-visible, so model choices as an ordered InteractionChoiceOption[] (value + label) instead, matching the native IReadOnlyList ordering contract. Regenerate ats.txt and all five language snapshots, and update the TypeScript, Go, Java, and Python polyglot test bench apphosts to the new ordered array form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 35 ++++++++++--- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 9 ++-- ...TwoPassScanningGeneratedAspire.verified.go | 24 +++++++-- ...oPassScanningGeneratedAspire.verified.java | 49 ++++++++++++++++--- ...TwoPassScanningGeneratedAspire.verified.py | 10 ++-- ...TwoPassScanningGeneratedAspire.verified.rs | 24 +++++++-- ...TwoPassScanningGeneratedAspire.verified.ts | 46 ++++++++++------- .../Aspire.Hosting/Go/apphost.go | 14 +++--- .../Aspire.Hosting/Java/AppHost.java | 25 ++++++---- .../Aspire.Hosting/Python/apphost.py | 14 +++--- .../Aspire.Hosting/TypeScript/apphost.mts | 18 +++---- 11 files changed, 189 insertions(+), 79 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index e9c0e843b2b..46bc3bb5c21 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -217,10 +217,10 @@ public static InteractionInputBuilder CreateNumberInput(this IInteractionService /// /// The interaction service. /// The name of the input. - /// The available choices, keyed by submitted value. + /// The available choices, in display order. Each option pairs a submitted value with a display label. /// Optional configuration for the input. [AspireExport] - public static InteractionInputBuilder CreateChoiceInput(this IInteractionService interactionService, string name, IReadOnlyDictionary? choices = null, CreateInteractionInputOptions? options = null) + public static InteractionInputBuilder CreateChoiceInput(this IInteractionService interactionService, string name, IReadOnlyList? choices = null, CreateInteractionInputOptions? options = null) { var builder = InteractionInputBuilder.Create(name, InputType.Choice, options); if (choices is { Count: > 0 }) @@ -232,12 +232,14 @@ public static InteractionInputBuilder CreateChoiceInput(this IInteractionService } #pragma warning restore IDE0060 // Remove unused parameter - internal static IReadOnlyList> ToOptionList(IReadOnlyDictionary choices) + // Preserve the caller-specified order: the native Options list is ordered, and the order is user-visible in the + // rendered dropdown. Materialize a copy so a caller-held list cannot mutate the input after the fact. + internal static IReadOnlyList> ToOptionList(IReadOnlyList choices) { var list = new List>(choices.Count); foreach (var choice in choices) { - list.Add(KeyValuePair.Create(choice.Key, choice.Value)); + list.Add(KeyValuePair.Create(choice.Value, choice.Label)); } return list; @@ -311,10 +313,10 @@ internal static InteractionInputBuilder Create(string name, InputType inputType, /// /// Sets the choice options for the input. /// - /// The available choices, keyed by submitted value. + /// The available choices, in display order. Each option pairs a submitted value with a display label. /// The same builder handle. [AspireExport] - public InteractionInputBuilder WithChoiceOptions(IReadOnlyDictionary choices) + public InteractionInputBuilder WithChoiceOptions(IReadOnlyList choices) { ArgumentNullException.ThrowIfNull(choices); @@ -398,9 +400,9 @@ public string GetInputName() /// /// Sets the choice options for the loading input. /// - /// The available choices, keyed by submitted value. + /// The available choices, in display order. Each option pairs a submitted value with a display label. [AspireExport] - public void SetChoiceOptions(IReadOnlyDictionary choices) + public void SetChoiceOptions(IReadOnlyList choices) { ArgumentNullException.ThrowIfNull(choices); @@ -421,6 +423,23 @@ public void SetValue(string? value) } } +/// +/// A single selectable option for a choice input. Options are presented in the order supplied. +/// +[AspireDto] +internal sealed class InteractionChoiceOption +{ + /// + /// Gets or sets the value submitted when this option is selected. + /// + public string Value { get; set; } = string.Empty; + + /// + /// Gets or sets the label displayed for this option. + /// + public string Label { get; set; } = string.Empty; +} + /// /// Optional configuration shared by interaction input factory capabilities. /// diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 8288c14c14d..77a6f48bc02 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -236,6 +236,9 @@ Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult # The result of a singl Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult # The result of a multi-input interaction prompt. Canceled: boolean # Gets a value indicating whether the interaction was canceled by the user. Inputs?: Aspire.Hosting/Aspire.Hosting.InteractionInput[] # Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. +Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption # A single selectable option for a choice input. Options are presented in the order supplied. + Label: string # Gets or sets the label displayed for this option. + Value: string # Gets or sets the value submitted when this option is selected. Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions # Options for inputs dialog prompts. EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. PrimaryButtonText: string # Gets or sets the primary button text. @@ -458,9 +461,9 @@ Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.cancellationToken(conte Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.executionContext(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext Aspire.Hosting.Ats/getInputName(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext) -> string Aspire.Hosting.Ats/getInputValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, inputName: string) -> string -Aspire.Hosting.Ats/setChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, choices: Aspire.Hosting/Dict) -> void +Aspire.Hosting.Ats/setChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> void Aspire.Hosting.Ats/setValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, value: string) -> void -Aspire.Hosting.Ats/withChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, choices: Aspire.Hosting/Dict) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder +Aspire.Hosting.Ats/withChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting.Ats/withDynamicLoading(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, callback: callback, options?: Aspire.Hosting/Aspire.Hosting.Ats.DynamicLoadingOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting.Ats/withValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, value: string) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting.Eventing/IDistributedApplicationEventing.unsubscribe(context: Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEventing, subscription: Aspire.Hosting/Aspire.Hosting.Eventing.DistributedApplicationEventSubscription) -> void @@ -531,7 +534,7 @@ Aspire.Hosting/completeTaskMarkdown(markdownString: string, completionState?: st Aspire.Hosting/configure(callback: callback) -> void Aspire.Hosting/createBooleanInput(name: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/createBuilder() -> Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder -Aspire.Hosting/createChoiceInput(name: string, choices?: Aspire.Hosting/Dict, options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder +Aspire.Hosting/createChoiceInput(name: string, choices?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[], options?: Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/createExecutionConfiguration() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExecutionConfigurationBuilder Aspire.Hosting/createLogger(categoryName: string) -> Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger Aspire.Hosting/createMarkdownTask(markdownString: string, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingTask diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 5b9d97a5ed5..45b72a4f06c 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -392,6 +392,20 @@ func (d *HttpsCertificateExecutionConfigurationExportData) ToMap() map[string]an return m } +// InteractionChoiceOption represents InteractionChoiceOption. +type InteractionChoiceOption struct { + Value string `json:"Value,omitempty"` + Label string `json:"Label,omitempty"` +} + +// ToMap converts the DTO to a map for JSON serialization. +func (d *InteractionChoiceOption) ToMap() map[string]any { + m := map[string]any{} + m["Value"] = serializeValue(d.Value) + m["Label"] = serializeValue(d.Label) + return m +} + // CreateInteractionInputOptions represents CreateInteractionInputOptions. type CreateInteractionInputOptions struct { Label string `json:"Label,omitempty"` @@ -15973,7 +15987,7 @@ func (s *inputsDialogValidationContext) Inputs() InteractionInputCollection { // InteractionInputBuilder is the public interface for handle type InteractionInputBuilder. type InteractionInputBuilder interface { handleReference - WithChoiceOptions(choices map[string]string) InteractionInputBuilder + WithChoiceOptions(choices []*InteractionChoiceOption) InteractionInputBuilder WithDynamicLoading(callback func(arg InteractionInputLoadContext), options ...*WithDynamicLoadingOptions) InteractionInputBuilder WithValue(value string) InteractionInputBuilder Err() error @@ -15990,7 +16004,7 @@ func newInteractionInputBuilderFromHandle(h *handle, c *client) InteractionInput } // WithChoiceOptions sets the choice options for the input. -func (s *interactionInputBuilder) WithChoiceOptions(choices map[string]string) InteractionInputBuilder { +func (s *interactionInputBuilder) WithChoiceOptions(choices []*InteractionChoiceOption) InteractionInputBuilder { if s.err != nil { return s } ctx := context.Background() reqArgs := map[string]any{ @@ -16076,7 +16090,7 @@ type InteractionInputLoadContext interface { handleReference GetInputName() (string, error) GetInputValue(inputName string) (string, error) - SetChoiceOptions(choices map[string]string) error + SetChoiceOptions(choices []*InteractionChoiceOption) error SetValue(value string) error Err() error } @@ -16123,7 +16137,7 @@ func (s *interactionInputLoadContext) GetInputValue(inputName string) (string, e } // SetChoiceOptions sets the choice options for the loading input. -func (s *interactionInputLoadContext) SetChoiceOptions(choices map[string]string) error { +func (s *interactionInputLoadContext) SetChoiceOptions(choices []*InteractionChoiceOption) error { if s.err != nil { return s.err } ctx := context.Background() reqArgs := map[string]any{ @@ -26795,7 +26809,7 @@ func (o *CreateNumberInputOptions) ToMap() map[string]any { // CreateChoiceInputOptions carries optional parameters for CreateChoiceInput. type CreateChoiceInputOptions struct { - Choices map[string]string `json:"choices,omitempty"` + Choices []*InteractionChoiceOption `json:"choices,omitempty"` Options *CreateInteractionInputOptions `json:"options,omitempty"` } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 4068fa3c5d7..e818d9bb3f8 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -6826,11 +6826,11 @@ public Map toMap() { /** Options for CreateChoiceInput. */ public final class CreateChoiceInputOptions { - private Map choices; + private InteractionChoiceOption[] choices; private CreateInteractionInputOptions options; - public Map getChoices() { return choices; } - public CreateChoiceInputOptions choices(Map value) { + public InteractionChoiceOption[] getChoices() { return choices; } + public CreateChoiceInputOptions choices(InteractionChoiceOption[] value) { this.choices = value; return this; } @@ -14039,7 +14039,7 @@ public InteractionInputBuilder createChoiceInput(String name) { } /** Creates a choice input that selects from a list of options. */ - private InteractionInputBuilder createChoiceInputImpl(String name, Map choices, CreateInteractionInputOptions options) { + private InteractionInputBuilder createChoiceInputImpl(String name, InteractionChoiceOption[] choices, CreateInteractionInputOptions options) { Map reqArgs = new HashMap<>(); reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); @@ -14942,6 +14942,42 @@ public Map toMap() { } } +// ===== InteractionChoiceOption.java ===== +// InteractionChoiceOption.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** InteractionChoiceOption DTO. */ +public class InteractionChoiceOption implements JsonSerializable { + private String value; + private String label; + + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + public String getLabel() { return label; } + public void setLabel(String value) { this.label = value; } + + @SuppressWarnings("unchecked") + public static InteractionChoiceOption fromMap(Map map) { + var value = new InteractionChoiceOption(); + var valueValue = map.get("Value"); + value.setValue((String) valueValue); + var labelValue = map.get("Label"); + value.setLabel((String) labelValue); + return value; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("Value", AspireClient.serializeValue(value)); + map.put("Label", AspireClient.serializeValue(label)); + return map; + } +} + // ===== InteractionInput.java ===== // InteractionInput.java - GENERATED CODE - DO NOT EDIT @@ -15053,7 +15089,7 @@ public class InteractionInputBuilder extends HandleWrapperBase { } /** Sets the choice options for the input. */ - public InteractionInputBuilder withChoiceOptions(Map choices) { + public InteractionInputBuilder withChoiceOptions(InteractionChoiceOption[] choices) { Map reqArgs = new HashMap<>(); reqArgs.put("context", AspireClient.serializeValue(getHandle())); reqArgs.put("choices", AspireClient.serializeValue(choices)); @@ -15151,7 +15187,7 @@ public String getInputValue(String inputName) { } /** Sets the choice options for the loading input. */ - public void setChoiceOptions(Map choices) { + public void setChoiceOptions(InteractionChoiceOption[] choices) { Map reqArgs = new HashMap<>(); reqArgs.put("context", AspireClient.serializeValue(getHandle())); reqArgs.put("choices", AspireClient.serializeValue(choices)); @@ -26946,6 +26982,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/InputType.java .aspire/modules/InputsDialogValidationContext.java .aspire/modules/InputsInteractionResult.java +.aspire/modules/InteractionChoiceOption.java .aspire/modules/InteractionInput.java .aspire/modules/InteractionInputBuilder.java .aspire/modules/InteractionInputCollection.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 0526c4e5286..87f06dbe51d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1863,6 +1863,10 @@ class InputsInteractionResult(typing.TypedDict, total=False): Canceled: bool Inputs: typing.Iterable[InteractionInput] +class InteractionChoiceOption(typing.TypedDict, total=False): + Value: str + Label: str + class InteractionInput(typing.TypedDict, total=False): Name: str Label: str | None @@ -3034,7 +3038,7 @@ def create_number_input(self, name: str, *, options: CreateInteractionInputOptio ) return typing.cast(InteractionInputBuilder, result) - def create_choice_input(self, name: str, *, choices: typing.Mapping[str, str] | None = None, options: CreateInteractionInputOptions | None = None) -> InteractionInputBuilder: + def create_choice_input(self, name: str, *, choices: typing.Iterable[InteractionChoiceOption] | None = None, options: CreateInteractionInputOptions | None = None) -> InteractionInputBuilder: """Creates a choice input that selects from a list of options.""" rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} rpc_args['name'] = name @@ -5161,7 +5165,7 @@ def handle(self) -> Handle: """The underlying object reference handle.""" return self._handle - def with_choice_options(self, choices: typing.Mapping[str, str]) -> InteractionInputBuilder: + def with_choice_options(self, choices: typing.Iterable[InteractionChoiceOption]) -> InteractionInputBuilder: """Sets the choice options for the input.""" rpc_args: dict[str, typing.Any] = {'context': self._handle} rpc_args['choices'] = choices @@ -5253,7 +5257,7 @@ def get_input_value(self, input_name: str) -> str: ) return result - def set_choice_options(self, choices: typing.Mapping[str, str]) -> None: + def set_choice_options(self, choices: typing.Iterable[InteractionChoiceOption]) -> None: """Sets the choice options for the loading input.""" rpc_args: dict[str, typing.Any] = {'context': self._handle} rpc_args['choices'] = choices diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 91f9eab75a6..30bdf280646 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -802,6 +802,24 @@ impl HttpsCertificateExecutionConfigurationExportData { } } +/// InteractionChoiceOption +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct InteractionChoiceOption { + #[serde(rename = "Value")] + pub value: String, + #[serde(rename = "Label")] + pub label: String, +} + +impl InteractionChoiceOption { + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("Value".to_string(), serde_json::to_value(&self.value).unwrap_or(Value::Null)); + map.insert("Label".to_string(), serde_json::to_value(&self.label).unwrap_or(Value::Null)); + map + } +} + /// CreateInteractionInputOptions #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CreateInteractionInputOptions { @@ -11077,7 +11095,7 @@ impl IInteractionService { } /// Creates a choice input that selects from a list of options. - pub fn create_choice_input(&self, name: &str, choices: Option>, options: Option) -> Result> { + pub fn create_choice_input(&self, name: &str, choices: Option>, options: Option) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("interactionService".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); @@ -11989,7 +12007,7 @@ impl InteractionInputBuilder { } /// Sets the choice options for the input. - pub fn with_choice_options(&self, choices: HashMap) -> Result> { + pub fn with_choice_options(&self, choices: Vec) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("context".to_string(), self.handle.to_json()); args.insert("choices".to_string(), serde_json::to_value(&choices).unwrap_or(Value::Null)); @@ -12100,7 +12118,7 @@ impl InteractionInputLoadContext { } /// Sets the choice options for the loading input. - pub fn set_choice_options(&self, choices: HashMap) -> Result<(), Box> { + pub fn set_choice_options(&self, choices: Vec) -> Result<(), Box> { let mut args: HashMap = HashMap::new(); args.insert("context".to_string(), self.handle.to_json()); args.insert("choices".to_string(), serde_json::to_value(&choices).unwrap_or(Value::Null)); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 24436b161d2..c58cfe6217f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -960,6 +960,14 @@ export interface InputsInteractionResult { inputs?: InteractionInput[]; } +/** A single selectable option for a choice input. Options are presented in the order supplied. */ +export interface InteractionChoiceOption { + /** Gets or sets the value submitted when this option is selected. */ + value?: string; + /** Gets or sets the label displayed for this option. */ + label?: string; +} + /** Options for inputs dialog prompts. */ export interface InteractionInputsDialogOptions { /** Gets or sets the primary button text. */ @@ -1391,8 +1399,8 @@ export interface CopyOptions { } export interface CreateChoiceInputOptions { - /** The available choices, keyed by submitted value. */ - choices?: Record; + /** The available choices, in display order. Each option pairs a submitted value with a display label. */ + choices?: InteractionChoiceOption[]; /** Optional configuration for the input. */ options?: CreateInteractionInputOptions; } @@ -5502,10 +5510,10 @@ export interface InteractionInputBuilder { toJSON(): MarshalledHandle; /** * Sets the choice options for the input. - * @param choices The available choices, keyed by submitted value. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. * @returns The same builder handle. */ - withChoiceOptions(choices: Record): InteractionInputBuilderPromise; + withChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputBuilderPromise; /** * Sets the value of the input. * @param value The value to assign. @@ -5524,10 +5532,10 @@ export interface InteractionInputBuilder { export interface InteractionInputBuilderPromise extends PromiseLike { /** * Sets the choice options for the input. - * @param choices The available choices, keyed by submitted value. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. * @returns The same builder handle. */ - withChoiceOptions(choices: Record): InteractionInputBuilderPromise; + withChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputBuilderPromise; /** * Sets the value of the input. * @param value The value to assign. @@ -5561,7 +5569,7 @@ class InteractionInputBuilderImpl implements InteractionInputBuilder { toJSON(): MarshalledHandle { return this._handle.toJSON(); } /** @internal */ - async _withChoiceOptionsInternal(choices: Record): Promise { + async _withChoiceOptionsInternal(choices: InteractionChoiceOption[]): Promise { const rpcArgs: Record = { context: this._handle, choices }; const result = await this._client.invokeCapability( 'Aspire.Hosting.Ats/withChoiceOptions', @@ -5572,10 +5580,10 @@ class InteractionInputBuilderImpl implements InteractionInputBuilder { /** * Sets the choice options for the input. - * @param choices The available choices, keyed by submitted value. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. * @returns The same builder handle. */ - withChoiceOptions(choices: Record): InteractionInputBuilderPromise { + withChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputBuilderPromise { return new InteractionInputBuilderPromiseImpl(this._withChoiceOptionsInternal(choices), this._client); } @@ -5641,7 +5649,7 @@ class InteractionInputBuilderPromiseImpl implements InteractionInputBuilderPromi return this._promise.then(onfulfilled, onrejected); } - withChoiceOptions(choices: Record): InteractionInputBuilderPromise { + withChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputBuilderPromise { return new InteractionInputBuilderPromiseImpl(this._promise.then(obj => obj.withChoiceOptions(choices)), this._client); } @@ -5675,9 +5683,9 @@ export interface InteractionInputLoadContext { getInputValue(inputName: string): Promise; /** * Sets the choice options for the loading input. - * @param choices The available choices, keyed by submitted value. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. */ - setChoiceOptions(choices: Record): InteractionInputLoadContextPromise; + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise; /** * Sets the value of the loading input. * @param value The value to assign. @@ -5699,9 +5707,9 @@ export interface InteractionInputLoadContextPromise extends PromiseLike; /** * Sets the choice options for the loading input. - * @param choices The available choices, keyed by submitted value. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. */ - setChoiceOptions(choices: Record): InteractionInputLoadContextPromise; + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise; /** * Sets the value of the loading input. * @param value The value to assign. @@ -5746,7 +5754,7 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { } /** @internal */ - async _setChoiceOptionsInternal(choices: Record): Promise { + async _setChoiceOptionsInternal(choices: InteractionChoiceOption[]): Promise { const rpcArgs: Record = { context: this._handle, choices }; await this._client.invokeCapability( 'Aspire.Hosting.Ats/setChoiceOptions', @@ -5757,9 +5765,9 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { /** * Sets the choice options for the loading input. - * @param choices The available choices, keyed by submitted value. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. */ - setChoiceOptions(choices: Record): InteractionInputLoadContextPromise { + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise { return new InteractionInputLoadContextPromiseImpl(this._setChoiceOptionsInternal(choices), this._client); } @@ -5806,7 +5814,7 @@ class InteractionInputLoadContextPromiseImpl implements InteractionInputLoadCont return this._promise.then(obj => obj.getInputValue(inputName)); } - setChoiceOptions(choices: Record): InteractionInputLoadContextPromise { + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise { return new InteractionInputLoadContextPromiseImpl(this._promise.then(obj => obj.setChoiceOptions(choices)), this._client); } @@ -11368,7 +11376,7 @@ class InteractionServiceImpl implements InteractionService { } /** @internal */ - async _createChoiceInputInternal(name: string, choices?: Record, options?: CreateInteractionInputOptions): Promise { + async _createChoiceInputInternal(name: string, choices?: InteractionChoiceOption[], options?: CreateInteractionInputOptions): Promise { const rpcArgs: Record = { interactionService: this._handle, name }; if (choices !== undefined) rpcArgs.choices = choices; if (options !== undefined) rpcArgs.options = options; diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 49b8ac0d979..edb08bed441 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -602,14 +602,14 @@ ENTRYPOINT ["dotnet", "App.dll"] } regionInput := interactionService.CreateChoiceInput("region", &aspire.CreateChoiceInputOptions{ - Choices: map[string]string{"us": "United States", "eu": "Europe"}, + Choices: []*aspire.InteractionChoiceOption{{Value: "us", Label: "United States"}, {Value: "eu", Label: "Europe"}}, }) zoneInput := interactionService.CreateChoiceInput("zone").WithDynamicLoading(func(loadContext aspire.InteractionInputLoadContext) { region, _ := loadContext.GetInputValue("region") - zones := map[string]string{"us-east": "US East", "us-west": "US West"} + zones := []*aspire.InteractionChoiceOption{{Value: "us-east", Label: "US East"}, {Value: "us-west", Label: "US West"}} if region == "eu" { - zones = map[string]string{"eu-west": "EU West", "eu-north": "EU North"} + zones = []*aspire.InteractionChoiceOption{{Value: "eu-west", Label: "EU West"}, {Value: "eu-north", Label: "EU North"}} } _ = loadContext.SetChoiceOptions(zones) }) @@ -694,17 +694,17 @@ ENTRYPOINT ["dotnet", "App.dll"] Options: &aspire.CreateInteractionInputOptions{Value: "1"}, }) choiceInput := interactionService.CreateChoiceInput("color", &aspire.CreateChoiceInputOptions{ - Choices: map[string]string{"r": "Red", "g": "Green"}, + Choices: []*aspire.InteractionChoiceOption{{Value: "r", Label: "Red"}, {Value: "g", Label: "Green"}}, Options: &aspire.CreateInteractionInputOptions{AllowCustomChoice: aspire.BoolPtr(true)}, }) presetInput := interactionService.CreateTextInput("greeting").WithValue("hello") - sizeInput := interactionService.CreateChoiceInput("size").WithChoiceOptions(map[string]string{"s": "Small", "l": "Large"}) + sizeInput := interactionService.CreateChoiceInput("size").WithChoiceOptions([]*aspire.InteractionChoiceOption{{Value: "s", Label: "Small"}, {Value: "l", Label: "Large"}}) dependentInput := interactionService.CreateChoiceInput("shade").WithDynamicLoading(func(loadContext aspire.InteractionInputLoadContext) { inputName, _ := loadContext.GetInputName() color, _ := loadContext.GetInputValue("color") - shades := map[string]string{"lime": "Lime", "forest": "Forest"} + shades := []*aspire.InteractionChoiceOption{{Value: "lime", Label: "Lime"}, {Value: "forest", Label: "Forest"}} if color == "r" { - shades = map[string]string{"crimson": "Crimson", "scarlet": "Scarlet"} + shades = []*aspire.InteractionChoiceOption{{Value: "crimson", Label: "Crimson"}, {Value: "scarlet", Label: "Scarlet"}} } _ = loadContext.SetChoiceOptions(shades) _ = loadContext.SetValue(inputName) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index 08c224a7333..6f6d35927ab 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -283,13 +283,13 @@ void main() throws Exception { var regionInput = interactionService.createChoiceInput( "region", - new CreateChoiceInputOptions().choices(Map.of("us", "United States", "eu", "Europe"))); + new CreateChoiceInputOptions().choices(new InteractionChoiceOption[] { opt("us", "United States"), opt("eu", "Europe") })); var zoneInput = interactionService.createChoiceInput("zone").withDynamicLoading((loadContext) -> { var region = loadContext.getInputValue("region"); - Map zones = "eu".equals(region) - ? Map.of("eu-west", "EU West", "eu-north", "EU North") - : Map.of("us-east", "US East", "us-west", "US West"); + InteractionChoiceOption[] zones = "eu".equals(region) + ? new InteractionChoiceOption[] { opt("eu-west", "EU West"), opt("eu-north", "EU North") } + : new InteractionChoiceOption[] { opt("us-east", "US East"), opt("us-west", "US West") }; loadContext.setChoiceOptions(zones); }); @@ -365,11 +365,11 @@ void main() throws Exception { var choiceExtras = new CreateInteractionInputOptions(); choiceExtras.setAllowCustomChoice(true); var choiceInput = interactionService.createChoiceInput("color", - new CreateChoiceInputOptions().choices(Map.of("r", "Red", "g", "Green")).options(choiceExtras)); + new CreateChoiceInputOptions().choices(new InteractionChoiceOption[] { opt("r", "Red"), opt("g", "Green") }).options(choiceExtras)); var presetInput = interactionService.createTextInput("greeting").withValue("hello"); var sizeInput = interactionService.createChoiceInput("size") - .withChoiceOptions(Map.of("s", "Small", "l", "Large")); + .withChoiceOptions(new InteractionChoiceOption[] { opt("s", "Small"), opt("l", "Large") }); var dynamicLoadingOptions = new DynamicLoadingOptions(); dynamicLoadingOptions.setAlwaysLoadOnStart(true); @@ -377,9 +377,9 @@ void main() throws Exception { var dependentInput = interactionService.createChoiceInput("shade").withDynamicLoading((loadContext) -> { var inputName = loadContext.getInputName(); var color = loadContext.getInputValue("color"); - Map shades = "r".equals(color) - ? Map.of("crimson", "Crimson", "scarlet", "Scarlet") - : Map.of("lime", "Lime", "forest", "Forest"); + InteractionChoiceOption[] shades = "r".equals(color) + ? new InteractionChoiceOption[] { opt("crimson", "Crimson"), opt("scarlet", "Scarlet") } + : new InteractionChoiceOption[] { opt("lime", "Lime"), opt("forest", "Forest") }; loadContext.setChoiceOptions(shades); loadContext.setValue(inputName); }, dynamicLoadingOptions); @@ -441,3 +441,10 @@ void main() throws Exception { var app = builder.build(); app.run(); } + + InteractionChoiceOption opt(String value, String label) { + var option = new InteractionChoiceOption(); + option.setValue(value); + option.setLabel(label); + return option; + } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index 43f1aa3db3e..9a743761815 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -448,14 +448,14 @@ def pick_zone_command(ctx): region_input = interaction_service.create_choice_input( "region", - choices={"us": "United States", "eu": "Europe"} + choices=[{"Value": "us", "Label": "United States"}, {"Value": "eu", "Label": "Europe"}] ) def load_zones(load_context): region = load_context.get_input_value("region") - zones = ({"eu-west": "EU West", "eu-north": "EU North"} + zones = ([{"Value": "eu-west", "Label": "EU West"}, {"Value": "eu-north", "Label": "EU North"}] if region == "eu" - else {"us-east": "US East", "us-west": "US West"}) + else [{"Value": "us-east", "Label": "US East"}, {"Value": "us-west", "Label": "US West"}]) load_context.set_choice_options(zones) zone_input = interaction_service.create_choice_input("zone").with_dynamic_loading(load_zones) @@ -524,19 +524,19 @@ def interaction_showcase_command(ctx): number_input = interaction_service.create_number_input("count", options={"Value": "1"}) choice_input = interaction_service.create_choice_input( "color", - choices={"r": "Red", "g": "Green"}, + choices=[{"Value": "r", "Label": "Red"}, {"Value": "g", "Label": "Green"}], options={"AllowCustomChoice": True} ) preset_input = interaction_service.create_text_input("greeting").with_value("hello") - size_input = interaction_service.create_choice_input("size").with_choice_options({"s": "Small", "l": "Large"}) + size_input = interaction_service.create_choice_input("size").with_choice_options([{"Value": "s", "Label": "Small"}, {"Value": "l", "Label": "Large"}]) def load_shade(load_context): input_name = load_context.get_input_name() color = load_context.get_input_value("color") load_context.set_choice_options( - {"crimson": "Crimson", "scarlet": "Scarlet"} + [{"Value": "crimson", "Label": "Crimson"}, {"Value": "scarlet", "Label": "Scarlet"}] if color == "r" - else {"lime": "Lime", "forest": "Forest"} + else [{"Value": "lime", "Label": "Lime"}, {"Value": "forest", "Label": "Forest"}] ) load_context.set_value(input_name) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index eaf1d30edda..4ee6437177a 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -15,7 +15,7 @@ import { ResourceCommandState, refExpr, } from './.aspire/modules/aspire.mjs'; -import type { DockerfileBuilderCallbackContext, DockerfileFactoryContext } from './.aspire/modules/aspire.mjs'; +import type { DockerfileBuilderCallbackContext, DockerfileFactoryContext, InteractionChoiceOption } from './.aspire/modules/aspire.mjs'; import { fileURLToPath } from 'node:url'; const builder = await createBuilder(); @@ -761,7 +761,7 @@ await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { } const regionInput = await interactionService.createChoiceInput("region", { - choices: { "us": "United States", "eu": "Europe" } + choices: [{ value: "us", label: "United States" }, { value: "eu", label: "Europe" }] }); const zoneInput = await interactionService @@ -769,9 +769,9 @@ await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { .withDynamicLoading(async (loadContext) => { const region = await loadContext.getInputValue("region"); - const zones: Record = region === "eu" - ? { "eu-west": "EU West", "eu-north": "EU North" } - : { "us-east": "US East", "us-west": "US West" }; + const zones: InteractionChoiceOption[] = region === "eu" + ? [{ value: "eu-west", label: "EU West" }, { value: "eu-north", label: "EU North" }] + : [{ value: "us-east", label: "US East" }, { value: "us-west", label: "US West" }]; await loadContext.setChoiceOptions(zones); }); @@ -831,13 +831,13 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn const booleanInput = await interactionService.createBooleanInput("enabled", { value: "true" }); const numberInput = await interactionService.createNumberInput("count", { value: "1" }); const choiceInput = await interactionService.createChoiceInput("color", { - choices: { "r": "Red", "g": "Green" }, + choices: [{ value: "r", label: "Red" }, { value: "g", label: "Green" }], options: { allowCustomChoice: true } }); const presetInput = await interactionService.createTextInput("greeting").withValue("hello"); const sizeInput = await interactionService .createChoiceInput("size") - .withChoiceOptions({ "s": "Small", "l": "Large" }); + .withChoiceOptions([{ value: "s", label: "Small" }, { value: "l", label: "Large" }]); const dependentInput = await interactionService .createChoiceInput("shade") .withDynamicLoading(async (loadContext) => { @@ -845,8 +845,8 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn const color = await loadContext.getInputValue("color"); await loadContext.setChoiceOptions(color === "r" - ? { "crimson": "Crimson", "scarlet": "Scarlet" } - : { "lime": "Lime", "forest": "Forest" }); + ? [{ value: "crimson", label: "Crimson" }, { value: "scarlet", label: "Scarlet" }] + : [{ value: "lime", label: "Lime" }, { value: "forest", label: "Forest" }]); await loadContext.setValue(inputName); }, { alwaysLoadOnStart: true, From ef7af9694f203e4c850109cf7b60fd029287846a Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 11:43:42 -0700 Subject: [PATCH 08/20] Fix Java codegen options-bag parameter shadowing The Java generator named the options-bag parameter "options" and then flattened its properties into locals of the same camelCase name. When a capability has an optional parameter literally named "options" (the interaction prompt methods, createChoiceInput, etc.), the generated "var options = options.getOptions()" shadowed the parameter, producing "variable options is already defined" plus cascading "cannot infer type" javac errors. This broke the Java SDK Validation CI job for every apphost because IInteractionService is part of core Aspire.Hosting. Rename the bag parameter to "optionsBag" (matching the TypeScript generator) so flattened locals never collide. Regenerated the Java snapshots accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsJavaCodeGenerator.cs | 11 +- .../AtsGeneratedAspire.verified.java | 66 +- ...oPassScanningGeneratedAspire.verified.java | 1380 ++++++++--------- 3 files changed, 731 insertions(+), 726 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs index 586b94b2757..9023345692e 100644 --- a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs @@ -1441,9 +1441,14 @@ private void GenerateOptionsOverloads( string optionsClassName) { var requiredParameterList = string.Join(", ", requiredParameters.Select(parameter => $"{MapParameterToJava(parameter)} {ToCamelCase(parameter.Name)}")); + // Name the options-bag parameter "optionsBag" rather than "options" to avoid colliding with a flattened + // local. Some capabilities have an optional parameter literally named "options" (for example the interaction + // prompts), and the flattening below declares "var options = optionsBag.getOptions()". Sharing the name would + // make the local shadow the parameter, which is a Java compile error. This matches the TypeScript generator, + // which also uses "optionsBag". var publicParameterList = string.IsNullOrEmpty(requiredParameterList) - ? $"{optionsClassName} options" - : $"{requiredParameterList}, {optionsClassName} options"; + ? $"{optionsClassName} optionsBag" + : $"{requiredParameterList}, {optionsClassName} optionsBag"; if (!string.IsNullOrEmpty(capability.Description)) { @@ -1454,7 +1459,7 @@ private void GenerateOptionsOverloads( foreach (var parameter in optionalParameters) { var paramName = ToCamelCase(parameter.Name); - WriteLine($" var {paramName} = options == null ? null : options.{GetOptionGetterName(parameter)}();"); + WriteLine($" var {paramName} = optionsBag == null ? null : optionsBag.{GetOptionGetterName(parameter)}();"); } var implementationArguments = requiredParameters diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java index 1511e3911c8..f2b580d7c54 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java @@ -1,4 +1,4 @@ -// ===== Aspire.java ===== +// ===== Aspire.java ===== // Aspire.java - GENERATED CODE - DO NOT EDIT package aspire; @@ -1791,9 +1791,9 @@ public class TestDatabaseResource extends ResourceBuilderBase { } /** Adds an optional string parameter */ - public TestDatabaseResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public TestDatabaseResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -2002,8 +2002,8 @@ public TestDatabaseResource withCancellableOperation(AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public CSharpAppResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -2056,9 +2056,9 @@ private CSharpAppResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public CSharpAppResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -2089,15 +2089,15 @@ private CSharpAppResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public CSharpAppResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -2904,9 +2904,9 @@ public IExecutionConfigurationBuilder createExecutionConfiguration() { } /** Adds an optional string parameter */ - public CSharpAppResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public CSharpAppResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -3155,9 +3155,9 @@ public CSharpAppResource withMergeEndpointScheme(String endpointName, double por } /** Configures resource logging */ - public CSharpAppResource withMergeLogging(String logLevel, WithMergeLoggingOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public CSharpAppResource withMergeLogging(String logLevel, WithMergeLoggingOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingImpl(logLevel, enableConsole, maxFiles); } @@ -3181,9 +3181,9 @@ private CSharpAppResource withMergeLoggingImpl(String logLevel, Boolean enableCo } /** Configures resource logging with file path */ - public CSharpAppResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public CSharpAppResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingPathImpl(logLevel, logPath, enableConsole, maxFiles); } @@ -4159,9 +4159,9 @@ public ContainerRegistryResource withContainerRegistry(ResourceBuilderBase regis } /** Configures custom base images for generated Dockerfiles. */ - public ContainerRegistryResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { - var buildImage = options == null ? null : options.getBuildImage(); - var runtimeImage = options == null ? null : options.getRuntimeImage(); + public ContainerRegistryResource withDockerfileBaseImage(WithDockerfileBaseImageOptions optionsBag) { + var buildImage = optionsBag == null ? null : optionsBag.getBuildImage(); + var runtimeImage = optionsBag == null ? null : optionsBag.getRuntimeImage(); return withDockerfileBaseImageImpl(buildImage, runtimeImage); } @@ -4451,9 +4451,9 @@ public ContainerRegistryResource withHidden() { } /** Hides the resource from default resource lists after successful completion */ - public ContainerRegistryResource withHiddenOnCompletion(WithHiddenOnCompletionOptions options) { - var exitCode = options == null ? null : options.getExitCode(); - var exitCodes = options == null ? null : options.getExitCodes(); + public ContainerRegistryResource withHiddenOnCompletion(WithHiddenOnCompletionOptions optionsBag) { + var exitCode = optionsBag == null ? null : optionsBag.getExitCode(); + var exitCodes = optionsBag == null ? null : optionsBag.getExitCodes(); return withHiddenOnCompletionImpl(exitCode, exitCodes); } @@ -4476,11 +4476,11 @@ private ContainerRegistryResource withHiddenOnCompletionImpl(Double exitCode, do } /** Adds a pipeline step to the resource that will be executed during deployment. */ - public ContainerRegistryResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public ContainerRegistryResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -4614,9 +4614,9 @@ public IExecutionConfigurationBuilder createExecutionConfiguration() { } /** Adds an optional string parameter */ - public ContainerRegistryResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public ContainerRegistryResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -4840,9 +4840,9 @@ public ContainerRegistryResource withMergeEndpointScheme(String endpointName, do } /** Configures resource logging */ - public ContainerRegistryResource withMergeLogging(String logLevel, WithMergeLoggingOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ContainerRegistryResource withMergeLogging(String logLevel, WithMergeLoggingOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingImpl(logLevel, enableConsole, maxFiles); } @@ -4866,9 +4866,9 @@ private ContainerRegistryResource withMergeLoggingImpl(String logLevel, Boolean } /** Configures resource logging with file path */ - public ContainerRegistryResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ContainerRegistryResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingPathImpl(logLevel, logPath, enableConsole, maxFiles); } @@ -5051,9 +5051,9 @@ public ContainerResource publishAsContainer() { } /** Causes Aspire to build the specified container image from a Dockerfile. */ - public ContainerResource withDockerfile(String contextPath, WithDockerfileOptions options) { - var dockerfilePath = options == null ? null : options.getDockerfilePath(); - var stage = options == null ? null : options.getStage(); + public ContainerResource withDockerfile(String contextPath, WithDockerfileOptions optionsBag) { + var dockerfilePath = optionsBag == null ? null : optionsBag.getDockerfilePath(); + var stage = optionsBag == null ? null : optionsBag.getStage(); return withDockerfileImpl(contextPath, dockerfilePath, stage); } @@ -5137,10 +5137,10 @@ public ContainerResource withBuildSecret(String name, ParameterResource value) { } /** Adds container certificate path overrides used for certificate trust at run time. */ - public ContainerResource withContainerCertificatePaths(WithContainerCertificatePathsOptions options) { - var customCertificatesDestination = options == null ? null : options.getCustomCertificatesDestination(); - var defaultCertificateBundlePaths = options == null ? null : options.getDefaultCertificateBundlePaths(); - var defaultCertificateDirectoryPaths = options == null ? null : options.getDefaultCertificateDirectoryPaths(); + public ContainerResource withContainerCertificatePaths(WithContainerCertificatePathsOptions optionsBag) { + var customCertificatesDestination = optionsBag == null ? null : optionsBag.getCustomCertificatesDestination(); + var defaultCertificateBundlePaths = optionsBag == null ? null : optionsBag.getDefaultCertificateBundlePaths(); + var defaultCertificateDirectoryPaths = optionsBag == null ? null : optionsBag.getDefaultCertificateDirectoryPaths(); return withContainerCertificatePathsImpl(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths); } @@ -5207,9 +5207,9 @@ public ContainerResource withDockerfileBuilder(String contextPath, AspireAction1 } /** Configures custom base images for generated Dockerfiles. */ - public ContainerResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { - var buildImage = options == null ? null : options.getBuildImage(); - var runtimeImage = options == null ? null : options.getRuntimeImage(); + public ContainerResource withDockerfileBaseImage(WithDockerfileBaseImageOptions optionsBag) { + var buildImage = optionsBag == null ? null : optionsBag.getBuildImage(); + var runtimeImage = optionsBag == null ? null : optionsBag.getRuntimeImage(); return withDockerfileBaseImageImpl(buildImage, runtimeImage); } @@ -5241,9 +5241,9 @@ public ContainerResource withContainerNetworkAlias(String alias) { } /** Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. */ - public ContainerResource withMcpServer(WithMcpServerOptions options) { - var path = options == null ? null : options.getPath(); - var endpointName = options == null ? null : options.getEndpointName(); + public ContainerResource withMcpServer(WithMcpServerOptions optionsBag) { + var path = optionsBag == null ? null : optionsBag.getPath(); + var endpointName = optionsBag == null ? null : optionsBag.getEndpointName(); return withMcpServerImpl(path, endpointName); } @@ -5467,10 +5467,10 @@ public ContainerResource withReference(String source) { } /** Adds a reference to another resource */ - public ContainerResource withReference(AspireUnion source, WithReferenceOptions options) { - var connectionName = options == null ? null : options.getConnectionName(); - var optional = options == null ? null : options.getOptional(); - var name = options == null ? null : options.getName(); + public ContainerResource withReference(AspireUnion source, WithReferenceOptions optionsBag) { + var connectionName = optionsBag == null ? null : optionsBag.getConnectionName(); + var optional = optionsBag == null ? null : optionsBag.getOptional(); + var name = optionsBag == null ? null : optionsBag.getName(); return withReferenceImpl(source, connectionName, optional, name); } @@ -5521,9 +5521,9 @@ public ContainerResource withEndpointCallback(String endpointName, AspireAction1 } /** Updates an HTTP endpoint via callback */ - public ContainerResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public ContainerResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -5554,9 +5554,9 @@ private ContainerResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public ContainerResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -5587,15 +5587,15 @@ private ContainerResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public ContainerResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -6292,9 +6292,9 @@ public ContainerResource withPipelineConfiguration(AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public DotnetToolResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -7931,9 +7931,9 @@ private DotnetToolResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public DotnetToolResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -7964,15 +7964,15 @@ private DotnetToolResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public DotnetToolResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -8765,9 +8765,9 @@ public IExecutionConfigurationBuilder createExecutionConfiguration() { } /** Adds an optional string parameter */ - public DotnetToolResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public DotnetToolResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -9016,9 +9016,9 @@ public DotnetToolResource withMergeEndpointScheme(String endpointName, double po } /** Configures resource logging */ - public DotnetToolResource withMergeLogging(String logLevel, WithMergeLoggingOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public DotnetToolResource withMergeLogging(String logLevel, WithMergeLoggingOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingImpl(logLevel, enableConsole, maxFiles); } @@ -9042,9 +9042,9 @@ private DotnetToolResource withMergeLoggingImpl(String logLevel, Boolean enableC } /** Configures resource logging with file path */ - public DotnetToolResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public DotnetToolResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingPathImpl(logLevel, logPath, enableConsole, maxFiles); } @@ -9799,9 +9799,9 @@ public ExecutableResource withContainerRegistry(ResourceBuilderBase registry) { } /** Configures custom base images for generated Dockerfiles. */ - public ExecutableResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { - var buildImage = options == null ? null : options.getBuildImage(); - var runtimeImage = options == null ? null : options.getRuntimeImage(); + public ExecutableResource withDockerfileBaseImage(WithDockerfileBaseImageOptions optionsBag) { + var buildImage = optionsBag == null ? null : optionsBag.getBuildImage(); + var runtimeImage = optionsBag == null ? null : optionsBag.getRuntimeImage(); return withDockerfileBaseImageImpl(buildImage, runtimeImage); } @@ -9858,9 +9858,9 @@ public ExecutableResource withWorkingDirectory(String workingDirectory) { } /** Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. */ - public ExecutableResource withMcpServer(WithMcpServerOptions options) { - var path = options == null ? null : options.getPath(); - var endpointName = options == null ? null : options.getEndpointName(); + public ExecutableResource withMcpServer(WithMcpServerOptions optionsBag) { + var path = optionsBag == null ? null : optionsBag.getPath(); + var endpointName = optionsBag == null ? null : optionsBag.getEndpointName(); return withMcpServerImpl(path, endpointName); } @@ -10076,10 +10076,10 @@ public ExecutableResource withReference(String source) { } /** Adds a reference to another resource */ - public ExecutableResource withReference(AspireUnion source, WithReferenceOptions options) { - var connectionName = options == null ? null : options.getConnectionName(); - var optional = options == null ? null : options.getOptional(); - var name = options == null ? null : options.getName(); + public ExecutableResource withReference(AspireUnion source, WithReferenceOptions optionsBag) { + var connectionName = optionsBag == null ? null : optionsBag.getConnectionName(); + var optional = optionsBag == null ? null : optionsBag.getOptional(); + var name = optionsBag == null ? null : optionsBag.getName(); return withReferenceImpl(source, connectionName, optional, name); } @@ -10130,9 +10130,9 @@ public ExecutableResource withEndpointCallback(String endpointName, AspireAction } /** Updates an HTTP endpoint via callback */ - public ExecutableResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public ExecutableResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -10163,9 +10163,9 @@ private ExecutableResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public ExecutableResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -10196,15 +10196,15 @@ private ExecutableResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public ExecutableResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -10997,9 +10997,9 @@ public IExecutionConfigurationBuilder createExecutionConfiguration() { } /** Adds an optional string parameter */ - public ExecutableResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public ExecutableResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -11248,9 +11248,9 @@ public ExecutableResource withMergeEndpointScheme(String endpointName, double po } /** Configures resource logging */ - public ExecutableResource withMergeLogging(String logLevel, WithMergeLoggingOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ExecutableResource withMergeLogging(String logLevel, WithMergeLoggingOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingImpl(logLevel, enableConsole, maxFiles); } @@ -11274,9 +11274,9 @@ private ExecutableResource withMergeLoggingImpl(String logLevel, Boolean enableC } /** Configures resource logging with file path */ - public ExecutableResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ExecutableResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingPathImpl(logLevel, logPath, enableConsole, maxFiles); } @@ -11492,9 +11492,9 @@ public ExternalServiceResource withContainerRegistry(ResourceBuilderBase registr } /** Configures custom base images for generated Dockerfiles. */ - public ExternalServiceResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { - var buildImage = options == null ? null : options.getBuildImage(); - var runtimeImage = options == null ? null : options.getRuntimeImage(); + public ExternalServiceResource withDockerfileBaseImage(WithDockerfileBaseImageOptions optionsBag) { + var buildImage = optionsBag == null ? null : optionsBag.getBuildImage(); + var runtimeImage = optionsBag == null ? null : optionsBag.getRuntimeImage(); return withDockerfileBaseImageImpl(buildImage, runtimeImage); } @@ -11517,10 +11517,10 @@ private ExternalServiceResource withDockerfileBaseImageImpl(String buildImage, S } /** Adds an HTTP health check to the external service for polyglot app hosts. */ - public ExternalServiceResource withHttpHealthCheck(WithHttpHealthCheckOptions options) { - var path = options == null ? null : options.getPath(); - var statusCode = options == null ? null : options.getStatusCode(); - var endpointName = options == null ? null : options.getEndpointName(); + public ExternalServiceResource withHttpHealthCheck(WithHttpHealthCheckOptions optionsBag) { + var path = optionsBag == null ? null : optionsBag.getPath(); + var statusCode = optionsBag == null ? null : optionsBag.getStatusCode(); + var endpointName = optionsBag == null ? null : optionsBag.getEndpointName(); return withHttpHealthCheckImpl(path, statusCode, endpointName); } @@ -11813,9 +11813,9 @@ public ExternalServiceResource withHidden() { } /** Hides the resource from default resource lists after successful completion */ - public ExternalServiceResource withHiddenOnCompletion(WithHiddenOnCompletionOptions options) { - var exitCode = options == null ? null : options.getExitCode(); - var exitCodes = options == null ? null : options.getExitCodes(); + public ExternalServiceResource withHiddenOnCompletion(WithHiddenOnCompletionOptions optionsBag) { + var exitCode = optionsBag == null ? null : optionsBag.getExitCode(); + var exitCodes = optionsBag == null ? null : optionsBag.getExitCodes(); return withHiddenOnCompletionImpl(exitCode, exitCodes); } @@ -11838,11 +11838,11 @@ private ExternalServiceResource withHiddenOnCompletionImpl(Double exitCode, doub } /** Adds a pipeline step to the resource that will be executed during deployment. */ - public ExternalServiceResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public ExternalServiceResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -11976,9 +11976,9 @@ public IExecutionConfigurationBuilder createExecutionConfiguration() { } /** Adds an optional string parameter */ - public ExternalServiceResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public ExternalServiceResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -12202,9 +12202,9 @@ public ExternalServiceResource withMergeEndpointScheme(String endpointName, doub } /** Configures resource logging */ - public ExternalServiceResource withMergeLogging(String logLevel, WithMergeLoggingOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ExternalServiceResource withMergeLogging(String logLevel, WithMergeLoggingOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingImpl(logLevel, enableConsole, maxFiles); } @@ -12228,9 +12228,9 @@ private ExternalServiceResource withMergeLoggingImpl(String logLevel, Boolean en } /** Configures resource logging with file path */ - public ExternalServiceResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ExternalServiceResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingPathImpl(logLevel, logPath, enableConsole, maxFiles); } @@ -12974,9 +12974,9 @@ public ContainerResource addContainer(String name, AspireUnion image) { } /** Adds a Dockerfile to the application model that can be treated like a container resource. */ - public ContainerResource addDockerfile(String name, String contextPath, AddDockerfileOptions options) { - var dockerfilePath = options == null ? null : options.getDockerfilePath(); - var stage = options == null ? null : options.getStage(); + public ContainerResource addDockerfile(String name, String contextPath, AddDockerfileOptions optionsBag) { + var dockerfilePath = optionsBag == null ? null : optionsBag.getDockerfilePath(); + var stage = optionsBag == null ? null : optionsBag.getStage(); return addDockerfileImpl(name, contextPath, dockerfilePath, stage); } @@ -13146,10 +13146,10 @@ public DistributedApplication build() { } /** Adds a parameter resource */ - public ParameterResource addParameter(String name, AddParameterOptions options) { - var value = options == null ? null : options.getValue(); - var publishValueAsDefault = options == null ? null : options.getPublishValueAsDefault(); - var secret = options == null ? null : options.getSecret(); + public ParameterResource addParameter(String name, AddParameterOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var publishValueAsDefault = optionsBag == null ? null : optionsBag.getPublishValueAsDefault(); + var secret = optionsBag == null ? null : optionsBag.getSecret(); return addParameterImpl(name, value, publishValueAsDefault, secret); } @@ -13193,9 +13193,9 @@ public ParameterResource addParameterFromConfiguration(String name, String confi } /** Adds a parameter with a generated default value */ - public ParameterResource addParameterWithGeneratedValue(String name, GenerateParameterDefault value, AddParameterWithGeneratedValueOptions options) { - var secret = options == null ? null : options.getSecret(); - var persist = options == null ? null : options.getPersist(); + public ParameterResource addParameterWithGeneratedValue(String name, GenerateParameterDefault value, AddParameterWithGeneratedValueOptions optionsBag) { + var secret = optionsBag == null ? null : optionsBag.getSecret(); + var persist = optionsBag == null ? null : optionsBag.getPersist(); return addParameterWithGeneratedValueImpl(name, value, secret, persist); } @@ -13477,9 +13477,9 @@ public IDistributedApplicationPipeline disableBuildOnlyContainerValidation() { } /** Adds an application-level pipeline step in a TypeScript-friendly shape. */ - public void addStep(String stepName, AspireAction1 callback, AddStepOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); + public void addStep(String stepName, AspireAction1 callback, AddStepOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); addStepImpl(stepName, callback, dependsOn, requiredBy); } @@ -13557,9 +13557,9 @@ public class IExecutionConfigurationBuilder extends HandleWrapperBase { } /** Builds the execution configuration for the specified builder. */ - public IExecutionConfigurationResult build(DistributedApplicationExecutionContext executionContext, BuildOptions options) { - var resourceLogger = options == null ? null : options.getResourceLogger(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public IExecutionConfigurationResult build(DistributedApplicationExecutionContext executionContext, BuildOptions optionsBag) { + var resourceLogger = optionsBag == null ? null : optionsBag.getResourceLogger(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); return buildImpl(executionContext, resourceLogger, cancellationToken); } @@ -13801,9 +13801,9 @@ public boolean isAvailable() { } /** Prompts the user for confirmation with an OK/Cancel dialog. */ - public BoolInteractionResult promptConfirmation(String title, String message, PromptConfirmationOptions options) { - var options = options == null ? null : options.getOptions(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public BoolInteractionResult promptConfirmation(String title, String message, PromptConfirmationOptions optionsBag) { + var options = optionsBag == null ? null : optionsBag.getOptions(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); return promptConfirmationImpl(title, message, options, cancellationToken); } @@ -13828,9 +13828,9 @@ private BoolInteractionResult promptConfirmationImpl(String title, String messag } /** Prompts the user with a message box dialog. */ - public BoolInteractionResult promptMessageBox(String title, String message, PromptMessageBoxOptions options) { - var options = options == null ? null : options.getOptions(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public BoolInteractionResult promptMessageBox(String title, String message, PromptMessageBoxOptions optionsBag) { + var options = optionsBag == null ? null : optionsBag.getOptions(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); return promptMessageBoxImpl(title, message, options, cancellationToken); } @@ -13855,9 +13855,9 @@ private BoolInteractionResult promptMessageBoxImpl(String title, String message, } /** Prompts the user with a notification. */ - public BoolInteractionResult promptNotification(String title, String message, PromptNotificationOptions options) { - var options = options == null ? null : options.getOptions(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public BoolInteractionResult promptNotification(String title, String message, PromptNotificationOptions optionsBag) { + var options = optionsBag == null ? null : optionsBag.getOptions(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); return promptNotificationImpl(title, message, options, cancellationToken); } @@ -13882,10 +13882,10 @@ private BoolInteractionResult promptNotificationImpl(String title, String messag } /** Prompts the user for a single input. */ - public InputInteractionResult promptInput(String title, String message, InteractionInputBuilder input, PromptInputOptions options) { - var options = options == null ? null : options.getOptions(); - var validationCallback = options == null ? null : options.getValidationCallback(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public InputInteractionResult promptInput(String title, String message, InteractionInputBuilder input, PromptInputOptions optionsBag) { + var options = optionsBag == null ? null : optionsBag.getOptions(); + var validationCallback = optionsBag == null ? null : optionsBag.getValidationCallback(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); return promptInputImpl(title, message, input, options, validationCallback, cancellationToken); } @@ -13927,10 +13927,10 @@ private InputInteractionResult promptInputImpl(String title, String message, Int } /** Prompts the user for multiple inputs. */ - public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs, PromptInputsOptions options) { - var options = options == null ? null : options.getOptions(); - var validationCallback = options == null ? null : options.getValidationCallback(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs, PromptInputsOptions optionsBag) { + var options = optionsBag == null ? null : optionsBag.getOptions(); + var validationCallback = optionsBag == null ? null : optionsBag.getValidationCallback(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); return promptInputsImpl(title, message, inputs, options, validationCallback, cancellationToken); } @@ -14028,9 +14028,9 @@ public InteractionInputBuilder createNumberInput(String name, CreateInteractionI } /** Creates a choice input that selects from a list of options. */ - public InteractionInputBuilder createChoiceInput(String name, CreateChoiceInputOptions options) { - var choices = options == null ? null : options.getChoices(); - var options = options == null ? null : options.getOptions(); + public InteractionInputBuilder createChoiceInput(String name, CreateChoiceInputOptions optionsBag) { + var choices = optionsBag == null ? null : optionsBag.getChoices(); + var options = optionsBag == null ? null : optionsBag.getOptions(); return createChoiceInputImpl(name, choices, options); } @@ -14202,9 +14202,9 @@ public void logStepMarkdown(String level, String markdownString) { } /** Completes the reporting step with plain-text completion text. */ - public void completeStep(String completionText, CompleteStepOptions options) { - var completionState = options == null ? null : options.getCompletionState(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public void completeStep(String completionText, CompleteStepOptions optionsBag) { + var completionState = optionsBag == null ? null : optionsBag.getCompletionState(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); completeStepImpl(completionText, completionState, cancellationToken); } @@ -14227,9 +14227,9 @@ private void completeStepImpl(String completionText, String completionState, Can } /** Completes the reporting step with Markdown-formatted completion text. */ - public void completeStepMarkdown(String markdownString, CompleteStepMarkdownOptions options) { - var completionState = options == null ? null : options.getCompletionState(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public void completeStepMarkdown(String markdownString, CompleteStepMarkdownOptions optionsBag) { + var completionState = optionsBag == null ? null : optionsBag.getCompletionState(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); completeStepMarkdownImpl(markdownString, completionState, cancellationToken); } @@ -14298,10 +14298,10 @@ public void updateTaskMarkdown(String markdownString, CancellationToken cancella } /** Completes the reporting task with plain-text completion text. */ - public void completeTask(CompleteTaskOptions options) { - var completionMessage = options == null ? null : options.getCompletionMessage(); - var completionState = options == null ? null : options.getCompletionState(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public void completeTask(CompleteTaskOptions optionsBag) { + var completionMessage = optionsBag == null ? null : optionsBag.getCompletionMessage(); + var completionState = optionsBag == null ? null : optionsBag.getCompletionState(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); completeTaskImpl(completionMessage, completionState, cancellationToken); } @@ -14326,9 +14326,9 @@ private void completeTaskImpl(String completionMessage, String completionState, } /** Completes the reporting task with Markdown-formatted completion text. */ - public void completeTaskMarkdown(String markdownString, CompleteTaskMarkdownOptions options) { - var completionState = options == null ? null : options.getCompletionState(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public void completeTaskMarkdown(String markdownString, CompleteTaskMarkdownOptions optionsBag) { + var completionState = optionsBag == null ? null : optionsBag.getCompletionState(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); completeTaskMarkdownImpl(markdownString, completionState, cancellationToken); } @@ -15630,9 +15630,9 @@ public ParameterResource withContainerRegistry(ResourceBuilderBase registry) { } /** Configures custom base images for generated Dockerfiles. */ - public ParameterResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { - var buildImage = options == null ? null : options.getBuildImage(); - var runtimeImage = options == null ? null : options.getRuntimeImage(); + public ParameterResource withDockerfileBaseImage(WithDockerfileBaseImageOptions optionsBag) { + var buildImage = optionsBag == null ? null : optionsBag.getBuildImage(); + var runtimeImage = optionsBag == null ? null : optionsBag.getRuntimeImage(); return withDockerfileBaseImageImpl(buildImage, runtimeImage); } @@ -15947,9 +15947,9 @@ public ParameterResource withHidden() { } /** Hides the resource from default resource lists after successful completion */ - public ParameterResource withHiddenOnCompletion(WithHiddenOnCompletionOptions options) { - var exitCode = options == null ? null : options.getExitCode(); - var exitCodes = options == null ? null : options.getExitCodes(); + public ParameterResource withHiddenOnCompletion(WithHiddenOnCompletionOptions optionsBag) { + var exitCode = optionsBag == null ? null : optionsBag.getExitCode(); + var exitCodes = optionsBag == null ? null : optionsBag.getExitCodes(); return withHiddenOnCompletionImpl(exitCode, exitCodes); } @@ -15972,11 +15972,11 @@ private ParameterResource withHiddenOnCompletionImpl(Double exitCode, double[] e } /** Adds a pipeline step to the resource that will be executed during deployment. */ - public ParameterResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public ParameterResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -16110,9 +16110,9 @@ public IExecutionConfigurationBuilder createExecutionConfiguration() { } /** Adds an optional string parameter */ - public ParameterResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public ParameterResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -16336,9 +16336,9 @@ public ParameterResource withMergeEndpointScheme(String endpointName, double por } /** Configures resource logging */ - public ParameterResource withMergeLogging(String logLevel, WithMergeLoggingOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ParameterResource withMergeLogging(String logLevel, WithMergeLoggingOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingImpl(logLevel, enableConsole, maxFiles); } @@ -16362,9 +16362,9 @@ private ParameterResource withMergeLoggingImpl(String logLevel, Boolean enableCo } /** Configures resource logging with file path */ - public ParameterResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ParameterResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingPathImpl(logLevel, logPath, enableConsole, maxFiles); } @@ -17055,9 +17055,9 @@ public ProjectResource withContainerRegistry(ResourceBuilderBase registry) { } /** Configures custom base images for generated Dockerfiles. */ - public ProjectResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { - var buildImage = options == null ? null : options.getBuildImage(); - var runtimeImage = options == null ? null : options.getRuntimeImage(); + public ProjectResource withDockerfileBaseImage(WithDockerfileBaseImageOptions optionsBag) { + var buildImage = optionsBag == null ? null : optionsBag.getBuildImage(); + var runtimeImage = optionsBag == null ? null : optionsBag.getRuntimeImage(); return withDockerfileBaseImageImpl(buildImage, runtimeImage); } @@ -17080,9 +17080,9 @@ private ProjectResource withDockerfileBaseImageImpl(String buildImage, String ru } /** Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. */ - public ProjectResource withMcpServer(WithMcpServerOptions options) { - var path = options == null ? null : options.getPath(); - var endpointName = options == null ? null : options.getEndpointName(); + public ProjectResource withMcpServer(WithMcpServerOptions optionsBag) { + var path = optionsBag == null ? null : optionsBag.getPath(); + var endpointName = optionsBag == null ? null : optionsBag.getEndpointName(); return withMcpServerImpl(path, endpointName); } @@ -17335,10 +17335,10 @@ public ProjectResource withReference(String source) { } /** Adds a reference to another resource */ - public ProjectResource withReference(AspireUnion source, WithReferenceOptions options) { - var connectionName = options == null ? null : options.getConnectionName(); - var optional = options == null ? null : options.getOptional(); - var name = options == null ? null : options.getName(); + public ProjectResource withReference(AspireUnion source, WithReferenceOptions optionsBag) { + var connectionName = optionsBag == null ? null : optionsBag.getConnectionName(); + var optional = optionsBag == null ? null : optionsBag.getOptional(); + var name = optionsBag == null ? null : optionsBag.getName(); return withReferenceImpl(source, connectionName, optional, name); } @@ -17389,9 +17389,9 @@ public ProjectResource withEndpointCallback(String endpointName, AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public ProjectResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -17422,9 +17422,9 @@ private ProjectResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public ProjectResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -17455,15 +17455,15 @@ private ProjectResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public ProjectResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -18270,9 +18270,9 @@ public IExecutionConfigurationBuilder createExecutionConfiguration() { } /** Adds an optional string parameter */ - public ProjectResource withOptionalString(WithOptionalStringOptions options) { - var value = options == null ? null : options.getValue(); - var enabled = options == null ? null : options.getEnabled(); + public ProjectResource withOptionalString(WithOptionalStringOptions optionsBag) { + var value = optionsBag == null ? null : optionsBag.getValue(); + var enabled = optionsBag == null ? null : optionsBag.getEnabled(); return withOptionalStringImpl(value, enabled); } @@ -18521,9 +18521,9 @@ public ProjectResource withMergeEndpointScheme(String endpointName, double port, } /** Configures resource logging */ - public ProjectResource withMergeLogging(String logLevel, WithMergeLoggingOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ProjectResource withMergeLogging(String logLevel, WithMergeLoggingOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingImpl(logLevel, enableConsole, maxFiles); } @@ -18547,9 +18547,9 @@ private ProjectResource withMergeLoggingImpl(String logLevel, Boolean enableCons } /** Configures resource logging with file path */ - public ProjectResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions options) { - var enableConsole = options == null ? null : options.getEnableConsole(); - var maxFiles = options == null ? null : options.getMaxFiles(); + public ProjectResource withMergeLoggingPath(String logLevel, String logPath, WithMergeLoggingPathOptions optionsBag) { + var enableConsole = optionsBag == null ? null : optionsBag.getEnableConsole(); + var maxFiles = optionsBag == null ? null : optionsBag.getMaxFiles(); return withMergeLoggingPathImpl(logLevel, logPath, enableConsole, maxFiles); } @@ -19192,9 +19192,9 @@ public ExecuteCommandResult executeCommandAsync(IResource resource, String comma } /** Executes a command for the specified resource. */ - public ExecuteCommandResult executeCommandAsync(AspireUnion resource, String commandName, ExecuteCommandAsyncOptions options) { - var arguments = options == null ? null : options.getArguments(); - var cancellationToken = options == null ? null : options.getCancellationToken(); + public ExecuteCommandResult executeCommandAsync(AspireUnion resource, String commandName, ExecuteCommandAsyncOptions optionsBag) { + var arguments = optionsBag == null ? null : optionsBag.getArguments(); + var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); return executeCommandAsyncImpl(resource, commandName, arguments, cancellationToken); } @@ -19478,9 +19478,9 @@ public ResourceEventDto tryGetResourceState(String resourceName) { } /** Publishes an update for a resource's state. */ - public void publishResourceUpdate(IResource resource, PublishResourceUpdateOptions options) { - var state = options == null ? null : options.getState(); - var stateStyle = options == null ? null : options.getStateStyle(); + public void publishResourceUpdate(IResource resource, PublishResourceUpdateOptions optionsBag) { + var state = optionsBag == null ? null : optionsBag.getState(); + var stateStyle = optionsBag == null ? null : optionsBag.getStateStyle(); publishResourceUpdateImpl(resource, state, stateStyle); } @@ -20061,9 +20061,9 @@ public TestDatabaseResource publishAsContainer() { } /** Causes Aspire to build the specified container image from a Dockerfile. */ - public TestDatabaseResource withDockerfile(String contextPath, WithDockerfileOptions options) { - var dockerfilePath = options == null ? null : options.getDockerfilePath(); - var stage = options == null ? null : options.getStage(); + public TestDatabaseResource withDockerfile(String contextPath, WithDockerfileOptions optionsBag) { + var dockerfilePath = optionsBag == null ? null : optionsBag.getDockerfilePath(); + var stage = optionsBag == null ? null : optionsBag.getStage(); return withDockerfileImpl(contextPath, dockerfilePath, stage); } @@ -20147,10 +20147,10 @@ public TestDatabaseResource withBuildSecret(String name, ParameterResource value } /** Adds container certificate path overrides used for certificate trust at run time. */ - public TestDatabaseResource withContainerCertificatePaths(WithContainerCertificatePathsOptions options) { - var customCertificatesDestination = options == null ? null : options.getCustomCertificatesDestination(); - var defaultCertificateBundlePaths = options == null ? null : options.getDefaultCertificateBundlePaths(); - var defaultCertificateDirectoryPaths = options == null ? null : options.getDefaultCertificateDirectoryPaths(); + public TestDatabaseResource withContainerCertificatePaths(WithContainerCertificatePathsOptions optionsBag) { + var customCertificatesDestination = optionsBag == null ? null : optionsBag.getCustomCertificatesDestination(); + var defaultCertificateBundlePaths = optionsBag == null ? null : optionsBag.getDefaultCertificateBundlePaths(); + var defaultCertificateDirectoryPaths = optionsBag == null ? null : optionsBag.getDefaultCertificateDirectoryPaths(); return withContainerCertificatePathsImpl(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths); } @@ -20217,9 +20217,9 @@ public TestDatabaseResource withDockerfileBuilder(String contextPath, AspireActi } /** Configures custom base images for generated Dockerfiles. */ - public TestDatabaseResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { - var buildImage = options == null ? null : options.getBuildImage(); - var runtimeImage = options == null ? null : options.getRuntimeImage(); + public TestDatabaseResource withDockerfileBaseImage(WithDockerfileBaseImageOptions optionsBag) { + var buildImage = optionsBag == null ? null : optionsBag.getBuildImage(); + var runtimeImage = optionsBag == null ? null : optionsBag.getRuntimeImage(); return withDockerfileBaseImageImpl(buildImage, runtimeImage); } @@ -20251,9 +20251,9 @@ public TestDatabaseResource withContainerNetworkAlias(String alias) { } /** Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. */ - public TestDatabaseResource withMcpServer(WithMcpServerOptions options) { - var path = options == null ? null : options.getPath(); - var endpointName = options == null ? null : options.getEndpointName(); + public TestDatabaseResource withMcpServer(WithMcpServerOptions optionsBag) { + var path = optionsBag == null ? null : optionsBag.getPath(); + var endpointName = optionsBag == null ? null : optionsBag.getEndpointName(); return withMcpServerImpl(path, endpointName); } @@ -20477,10 +20477,10 @@ public TestDatabaseResource withReference(String source) { } /** Adds a reference to another resource */ - public TestDatabaseResource withReference(AspireUnion source, WithReferenceOptions options) { - var connectionName = options == null ? null : options.getConnectionName(); - var optional = options == null ? null : options.getOptional(); - var name = options == null ? null : options.getName(); + public TestDatabaseResource withReference(AspireUnion source, WithReferenceOptions optionsBag) { + var connectionName = optionsBag == null ? null : optionsBag.getConnectionName(); + var optional = optionsBag == null ? null : optionsBag.getOptional(); + var name = optionsBag == null ? null : optionsBag.getName(); return withReferenceImpl(source, connectionName, optional, name); } @@ -20531,9 +20531,9 @@ public TestDatabaseResource withEndpointCallback(String endpointName, AspireActi } /** Updates an HTTP endpoint via callback */ - public TestDatabaseResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public TestDatabaseResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -20564,9 +20564,9 @@ private TestDatabaseResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public TestDatabaseResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -20597,15 +20597,15 @@ private TestDatabaseResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public TestDatabaseResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -21302,9 +21302,9 @@ public TestDatabaseResource withPipelineConfiguration(AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public TestRedisResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -22649,9 +22649,9 @@ private TestRedisResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public TestRedisResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -22682,15 +22682,15 @@ private TestRedisResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public TestRedisResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -23387,9 +23387,9 @@ public TestRedisResource withPipelineConfiguration(AspireAction1 callback, WithHttpEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public TestVaultResource withHttpEndpointCallback(AspireAction1 callback, WithHttpEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -24752,9 +24752,9 @@ private TestVaultResource withHttpEndpointCallbackImpl(AspireAction1 callback, WithHttpsEndpointCallbackOptions options) { - var name = options == null ? null : options.getName(); - var createIfNotExists = options == null ? null : options.getCreateIfNotExists(); + public TestVaultResource withHttpsEndpointCallback(AspireAction1 callback, WithHttpsEndpointCallbackOptions optionsBag) { + var name = optionsBag == null ? null : optionsBag.getName(); + var createIfNotExists = optionsBag == null ? null : optionsBag.getCreateIfNotExists(); return withHttpsEndpointCallbackImpl(callback, name, createIfNotExists); } @@ -24785,15 +24785,15 @@ private TestVaultResource withHttpsEndpointCallbackImpl(AspireAction1 callback, WithPipelineStepFactoryOptions options) { - var dependsOn = options == null ? null : options.getDependsOn(); - var requiredBy = options == null ? null : options.getRequiredBy(); - var tags = options == null ? null : options.getTags(); - var description = options == null ? null : options.getDescription(); + public TestVaultResource withPipelineStepFactory(String stepName, AspireAction1 callback, WithPipelineStepFactoryOptions optionsBag) { + var dependsOn = optionsBag == null ? null : optionsBag.getDependsOn(); + var requiredBy = optionsBag == null ? null : optionsBag.getRequiredBy(); + var tags = optionsBag == null ? null : optionsBag.getTags(); + var description = optionsBag == null ? null : optionsBag.getDescription(); return withPipelineStepFactoryImpl(stepName, callback, dependsOn, requiredBy, tags, description); } @@ -25490,9 +25490,9 @@ public TestVaultResource withPipelineConfiguration(AspireAction1 Date: Fri, 5 Jun 2026 11:57:45 -0700 Subject: [PATCH 09/20] Make nullable interaction option strings optional in ATS shape The interaction option DTOs declared their nullable string properties with plain setters. The ATS DTO scanner can only detect optionality for nullable value types (Nullable.GetUnderlyingType) or init-only properties; it cannot see nullable reference-type annotations through reflection. As a result PrimaryButtonText, SecondaryButtonText, LinkText, LinkUrl, Label, Description, Placeholder, Value and DependsOnInputs were emitted as required. Downstream generators then treated them as required: the Go and Rust SDKs serialized empty strings unconditionally, blanking server-side defaults when a caller only wanted to set a single option such as Intent. (TypeScript, Python and Java already rendered them optional.) Switch these option-bag properties to init-only, matching the existing HttpsCertificateInfo.Thumbprint convention, so the scanner marks them optional. Regenerated ats.txt and the Go/Rust snapshots (now Option/pointer with conditional serialization) and updated the Go validation apphost to use aspire.StringPtr for the now-optional fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 60 +++++----- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 26 ++--- ...TwoPassScanningGeneratedAspire.verified.go | 48 ++++---- ...TwoPassScanningGeneratedAspire.verified.rs | 104 +++++++++++------- .../Aspire.Hosting/Go/apphost.go | 26 ++--- 5 files changed, 145 insertions(+), 119 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index 46bc3bb5c21..b2e1ba05ada 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -449,47 +449,47 @@ internal sealed class CreateInteractionInputOptions /// /// Gets or sets the label for the input. Defaults to the input name when not specified. /// - public string? Label { get; set; } + public string? Label { get; init; } /// /// Gets or sets the description for the input. /// - public string? Description { get; set; } + public string? Description { get; init; } /// /// Gets or sets a value indicating whether the description is rendered as Markdown. /// - public bool? EnableDescriptionMarkdown { get; set; } + public bool? EnableDescriptionMarkdown { get; init; } /// /// Gets or sets a value indicating whether the input is required. /// - public bool? Required { get; set; } + public bool? Required { get; init; } /// /// Gets or sets the placeholder text for the input. /// - public string? Placeholder { get; set; } + public string? Placeholder { get; init; } /// /// Gets or sets the initial value of the input. /// - public string? Value { get; set; } + public string? Value { get; init; } /// /// Gets or sets a value indicating whether a custom choice is allowed. Only used by choice inputs. /// - public bool? AllowCustomChoice { get; set; } + public bool? AllowCustomChoice { get; init; } /// /// Gets or sets a value indicating whether the input is disabled. /// - public bool? Disabled { get; set; } + public bool? Disabled { get; init; } /// /// Gets or sets the maximum length for text inputs. /// - public int? MaxLength { get; set; } + public int? MaxLength { get; init; } } /// @@ -501,12 +501,12 @@ internal sealed class DynamicLoadingOptions /// /// Gets or sets a value indicating whether the callback always runs at the start of the prompt. /// - public bool? AlwaysLoadOnStart { get; set; } + public bool? AlwaysLoadOnStart { get; init; } /// /// Gets or sets the names of inputs this input depends on. The callback runs when any of them change. /// - public IReadOnlyList? DependsOnInputs { get; set; } + public IReadOnlyList? DependsOnInputs { get; init; } } /// @@ -518,32 +518,32 @@ internal sealed class InteractionMessageBoxOptions /// /// Gets or sets the primary button text. /// - public string? PrimaryButtonText { get; set; } + public string? PrimaryButtonText { get; init; } /// /// Gets or sets the secondary button text. /// - public string? SecondaryButtonText { get; set; } + public string? SecondaryButtonText { get; init; } /// /// Gets or sets a value indicating whether the secondary button is shown. /// - public bool? ShowSecondaryButton { get; set; } + public bool? ShowSecondaryButton { get; init; } /// /// Gets or sets a value indicating whether the dismiss button is shown. /// - public bool? ShowDismiss { get; set; } + public bool? ShowDismiss { get; init; } /// /// Gets or sets a value indicating whether Markdown in the message is rendered. /// - public bool? EnableMessageMarkdown { get; set; } + public bool? EnableMessageMarkdown { get; init; } /// /// Gets or sets the intent of the message box. /// - public MessageIntent? Intent { get; set; } + public MessageIntent? Intent { get; init; } internal MessageBoxInteractionOptions ToOptions() { @@ -568,42 +568,42 @@ internal sealed class InteractionNotificationOptions /// /// Gets or sets the primary button text. /// - public string? PrimaryButtonText { get; set; } + public string? PrimaryButtonText { get; init; } /// /// Gets or sets the secondary button text. /// - public string? SecondaryButtonText { get; set; } + public string? SecondaryButtonText { get; init; } /// /// Gets or sets a value indicating whether the secondary button is shown. /// - public bool? ShowSecondaryButton { get; set; } + public bool? ShowSecondaryButton { get; init; } /// /// Gets or sets a value indicating whether the dismiss button is shown. /// - public bool? ShowDismiss { get; set; } + public bool? ShowDismiss { get; init; } /// /// Gets or sets a value indicating whether Markdown in the message is rendered. /// - public bool? EnableMessageMarkdown { get; set; } + public bool? EnableMessageMarkdown { get; init; } /// /// Gets or sets the intent of the notification. /// - public MessageIntent? Intent { get; set; } + public MessageIntent? Intent { get; init; } /// /// Gets or sets the text for a link in the notification. /// - public string? LinkText { get; set; } + public string? LinkText { get; init; } /// /// Gets or sets the URL for the link in the notification. /// - public string? LinkUrl { get; set; } + public string? LinkUrl { get; init; } internal NotificationInteractionOptions ToOptions() { @@ -630,27 +630,27 @@ internal sealed class InteractionInputsDialogOptions /// /// Gets or sets the primary button text. /// - public string? PrimaryButtonText { get; set; } + public string? PrimaryButtonText { get; init; } /// /// Gets or sets the secondary button text. /// - public string? SecondaryButtonText { get; set; } + public string? SecondaryButtonText { get; init; } /// /// Gets or sets a value indicating whether the secondary button is shown. /// - public bool? ShowSecondaryButton { get; set; } + public bool? ShowSecondaryButton { get; init; } /// /// Gets or sets a value indicating whether the dismiss button is shown. /// - public bool? ShowDismiss { get; set; } + public bool? ShowDismiss { get; init; } /// /// Gets or sets a value indicating whether Markdown in the message is rendered. /// - public bool? EnableMessageMarkdown { get; set; } + public bool? EnableMessageMarkdown { get; init; } internal InputsDialogInteractionOptions ToOptions() { diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 77a6f48bc02..f18e71ee317 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -207,17 +207,17 @@ Aspire.Hosting/Aspire.Hosting.Ats.CreateBuilderOptions # Options for creating a ProjectDirectory: string # The directory containing the AppHost project file. Aspire.Hosting/Aspire.Hosting.Ats.CreateInteractionInputOptions # Optional configuration shared by interaction input factory capabilities. AllowCustomChoice?: boolean # Gets or sets a value indicating whether a custom choice is allowed. Only used by choice inputs. - Description: string # Gets or sets the description for the input. + Description?: string # Gets or sets the description for the input. Disabled?: boolean # Gets or sets a value indicating whether the input is disabled. EnableDescriptionMarkdown?: boolean # Gets or sets a value indicating whether the description is rendered as Markdown. - Label: string # Gets or sets the label for the input. Defaults to the input name when not specified. + Label?: string # Gets or sets the label for the input. Defaults to the input name when not specified. MaxLength?: number # Gets or sets the maximum length for text inputs. - Placeholder: string # Gets or sets the placeholder text for the input. + Placeholder?: string # Gets or sets the placeholder text for the input. Required?: boolean # Gets or sets a value indicating whether the input is required. - Value: string # Gets or sets the initial value of the input. + Value?: string # Gets or sets the initial value of the input. Aspire.Hosting/Aspire.Hosting.Ats.DynamicLoadingOptions # Options controlling when a dynamic-loading callback runs. AlwaysLoadOnStart?: boolean # Gets or sets a value indicating whether the callback always runs at the start of the prompt. - DependsOnInputs: string[] # Gets or sets the names of inputs this input depends on. The callback runs when any of them change. + DependsOnInputs?: string[] # Gets or sets the names of inputs this input depends on. The callback runs when any of them change. Aspire.Hosting/Aspire.Hosting.Ats.HttpsCertificateExecutionConfigurationExportData # ATS-friendly HTTPS certificate data returned from an execution-configuration result. IsKeyPathReferenced: boolean # Indicates whether the key path was referenced. IsPfxPathReferenced: boolean # Indicates whether the PFX path was referenced. @@ -241,24 +241,24 @@ Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption # A single selectable Value: string # Gets or sets the value submitted when this option is selected. Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions # Options for inputs dialog prompts. EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. - PrimaryButtonText: string # Gets or sets the primary button text. - SecondaryButtonText: string # Gets or sets the secondary button text. + PrimaryButtonText?: string # Gets or sets the primary button text. + SecondaryButtonText?: string # Gets or sets the secondary button text. ShowDismiss?: boolean # Gets or sets a value indicating whether the dismiss button is shown. ShowSecondaryButton?: boolean # Gets or sets a value indicating whether the secondary button is shown. Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions # Options for message box and confirmation prompts. EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. Intent?: enum:Aspire.Hosting.MessageIntent # Gets or sets the intent of the message box. - PrimaryButtonText: string # Gets or sets the primary button text. - SecondaryButtonText: string # Gets or sets the secondary button text. + PrimaryButtonText?: string # Gets or sets the primary button text. + SecondaryButtonText?: string # Gets or sets the secondary button text. ShowDismiss?: boolean # Gets or sets a value indicating whether the dismiss button is shown. ShowSecondaryButton?: boolean # Gets or sets a value indicating whether the secondary button is shown. Aspire.Hosting/Aspire.Hosting.Ats.InteractionNotificationOptions # Options for notification prompts. EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. Intent?: enum:Aspire.Hosting.MessageIntent # Gets or sets the intent of the notification. - LinkText: string # Gets or sets the text for a link in the notification. - LinkUrl: string # Gets or sets the URL for the link in the notification. - PrimaryButtonText: string # Gets or sets the primary button text. - SecondaryButtonText: string # Gets or sets the secondary button text. + LinkText?: string # Gets or sets the text for a link in the notification. + LinkUrl?: string # Gets or sets the URL for the link in the notification. + PrimaryButtonText?: string # Gets or sets the primary button text. + SecondaryButtonText?: string # Gets or sets the secondary button text. ShowDismiss?: boolean # Gets or sets a value indicating whether the dismiss button is shown. ShowSecondaryButton?: boolean # Gets or sets a value indicating whether the secondary button is shown. Aspire.Hosting/Aspire.Hosting.Ats.ParameterCustomInputOptions # Options for customizing parameter inputs from polyglot app hosts. diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 45b72a4f06c..7cb7d495fa1 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -408,12 +408,12 @@ func (d *InteractionChoiceOption) ToMap() map[string]any { // CreateInteractionInputOptions represents CreateInteractionInputOptions. type CreateInteractionInputOptions struct { - Label string `json:"Label,omitempty"` - Description string `json:"Description,omitempty"` + Label *string `json:"Label,omitempty"` + Description *string `json:"Description,omitempty"` EnableDescriptionMarkdown *bool `json:"EnableDescriptionMarkdown,omitempty"` Required *bool `json:"Required,omitempty"` - Placeholder string `json:"Placeholder,omitempty"` - Value string `json:"Value,omitempty"` + Placeholder *string `json:"Placeholder,omitempty"` + Value *string `json:"Value,omitempty"` AllowCustomChoice *bool `json:"AllowCustomChoice,omitempty"` Disabled *bool `json:"Disabled,omitempty"` MaxLength *float64 `json:"MaxLength,omitempty"` @@ -422,12 +422,12 @@ type CreateInteractionInputOptions struct { // ToMap converts the DTO to a map for JSON serialization. func (d *CreateInteractionInputOptions) ToMap() map[string]any { m := map[string]any{} - m["Label"] = serializeValue(d.Label) - m["Description"] = serializeValue(d.Description) + if d.Label != nil { m["Label"] = serializeValue(d.Label) } + if d.Description != nil { m["Description"] = serializeValue(d.Description) } if d.EnableDescriptionMarkdown != nil { m["EnableDescriptionMarkdown"] = serializeValue(d.EnableDescriptionMarkdown) } if d.Required != nil { m["Required"] = serializeValue(d.Required) } - m["Placeholder"] = serializeValue(d.Placeholder) - m["Value"] = serializeValue(d.Value) + if d.Placeholder != nil { m["Placeholder"] = serializeValue(d.Placeholder) } + if d.Value != nil { m["Value"] = serializeValue(d.Value) } if d.AllowCustomChoice != nil { m["AllowCustomChoice"] = serializeValue(d.AllowCustomChoice) } if d.Disabled != nil { m["Disabled"] = serializeValue(d.Disabled) } if d.MaxLength != nil { m["MaxLength"] = serializeValue(d.MaxLength) } @@ -450,8 +450,8 @@ func (d *DynamicLoadingOptions) ToMap() map[string]any { // InteractionMessageBoxOptions represents InteractionMessageBoxOptions. type InteractionMessageBoxOptions struct { - PrimaryButtonText string `json:"PrimaryButtonText,omitempty"` - SecondaryButtonText string `json:"SecondaryButtonText,omitempty"` + PrimaryButtonText *string `json:"PrimaryButtonText,omitempty"` + SecondaryButtonText *string `json:"SecondaryButtonText,omitempty"` ShowSecondaryButton *bool `json:"ShowSecondaryButton,omitempty"` ShowDismiss *bool `json:"ShowDismiss,omitempty"` EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"` @@ -461,8 +461,8 @@ type InteractionMessageBoxOptions struct { // ToMap converts the DTO to a map for JSON serialization. func (d *InteractionMessageBoxOptions) ToMap() map[string]any { m := map[string]any{} - m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) - m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) + if d.PrimaryButtonText != nil { m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) } + if d.SecondaryButtonText != nil { m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) } if d.ShowSecondaryButton != nil { m["ShowSecondaryButton"] = serializeValue(d.ShowSecondaryButton) } if d.ShowDismiss != nil { m["ShowDismiss"] = serializeValue(d.ShowDismiss) } if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) } @@ -472,34 +472,34 @@ func (d *InteractionMessageBoxOptions) ToMap() map[string]any { // InteractionNotificationOptions represents InteractionNotificationOptions. type InteractionNotificationOptions struct { - PrimaryButtonText string `json:"PrimaryButtonText,omitempty"` - SecondaryButtonText string `json:"SecondaryButtonText,omitempty"` + PrimaryButtonText *string `json:"PrimaryButtonText,omitempty"` + SecondaryButtonText *string `json:"SecondaryButtonText,omitempty"` ShowSecondaryButton *bool `json:"ShowSecondaryButton,omitempty"` ShowDismiss *bool `json:"ShowDismiss,omitempty"` EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"` Intent *MessageIntent `json:"Intent,omitempty"` - LinkText string `json:"LinkText,omitempty"` - LinkUrl string `json:"LinkUrl,omitempty"` + LinkText *string `json:"LinkText,omitempty"` + LinkUrl *string `json:"LinkUrl,omitempty"` } // ToMap converts the DTO to a map for JSON serialization. func (d *InteractionNotificationOptions) ToMap() map[string]any { m := map[string]any{} - m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) - m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) + if d.PrimaryButtonText != nil { m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) } + if d.SecondaryButtonText != nil { m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) } if d.ShowSecondaryButton != nil { m["ShowSecondaryButton"] = serializeValue(d.ShowSecondaryButton) } if d.ShowDismiss != nil { m["ShowDismiss"] = serializeValue(d.ShowDismiss) } if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) } if d.Intent != nil { m["Intent"] = serializeValue(d.Intent) } - m["LinkText"] = serializeValue(d.LinkText) - m["LinkUrl"] = serializeValue(d.LinkUrl) + if d.LinkText != nil { m["LinkText"] = serializeValue(d.LinkText) } + if d.LinkUrl != nil { m["LinkUrl"] = serializeValue(d.LinkUrl) } return m } // InteractionInputsDialogOptions represents InteractionInputsDialogOptions. type InteractionInputsDialogOptions struct { - PrimaryButtonText string `json:"PrimaryButtonText,omitempty"` - SecondaryButtonText string `json:"SecondaryButtonText,omitempty"` + PrimaryButtonText *string `json:"PrimaryButtonText,omitempty"` + SecondaryButtonText *string `json:"SecondaryButtonText,omitempty"` ShowSecondaryButton *bool `json:"ShowSecondaryButton,omitempty"` ShowDismiss *bool `json:"ShowDismiss,omitempty"` EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"` @@ -508,8 +508,8 @@ type InteractionInputsDialogOptions struct { // ToMap converts the DTO to a map for JSON serialization. func (d *InteractionInputsDialogOptions) ToMap() map[string]any { m := map[string]any{} - m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) - m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) + if d.PrimaryButtonText != nil { m["PrimaryButtonText"] = serializeValue(d.PrimaryButtonText) } + if d.SecondaryButtonText != nil { m["SecondaryButtonText"] = serializeValue(d.SecondaryButtonText) } if d.ShowSecondaryButton != nil { m["ShowSecondaryButton"] = serializeValue(d.ShowSecondaryButton) } if d.ShowDismiss != nil { m["ShowDismiss"] = serializeValue(d.ShowDismiss) } if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) } diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 30bdf280646..109c618a1eb 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -823,18 +823,18 @@ impl InteractionChoiceOption { /// CreateInteractionInputOptions #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CreateInteractionInputOptions { - #[serde(rename = "Label")] - pub label: String, - #[serde(rename = "Description")] - pub description: String, + #[serde(rename = "Label", skip_serializing_if = "Option::is_none")] + pub label: Option, + #[serde(rename = "Description", skip_serializing_if = "Option::is_none")] + pub description: Option, #[serde(rename = "EnableDescriptionMarkdown", skip_serializing_if = "Option::is_none")] pub enable_description_markdown: Option, #[serde(rename = "Required", skip_serializing_if = "Option::is_none")] pub required: Option, - #[serde(rename = "Placeholder")] - pub placeholder: String, - #[serde(rename = "Value")] - pub value: String, + #[serde(rename = "Placeholder", skip_serializing_if = "Option::is_none")] + pub placeholder: Option, + #[serde(rename = "Value", skip_serializing_if = "Option::is_none")] + pub value: Option, #[serde(rename = "AllowCustomChoice", skip_serializing_if = "Option::is_none")] pub allow_custom_choice: Option, #[serde(rename = "Disabled", skip_serializing_if = "Option::is_none")] @@ -846,16 +846,24 @@ pub struct CreateInteractionInputOptions { impl CreateInteractionInputOptions { pub fn to_map(&self) -> HashMap { let mut map = HashMap::new(); - map.insert("Label".to_string(), serde_json::to_value(&self.label).unwrap_or(Value::Null)); - map.insert("Description".to_string(), serde_json::to_value(&self.description).unwrap_or(Value::Null)); + if let Some(ref v) = self.label { + map.insert("Label".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.description { + map.insert("Description".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } if let Some(ref v) = self.enable_description_markdown { map.insert("EnableDescriptionMarkdown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } if let Some(ref v) = self.required { map.insert("Required".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - map.insert("Placeholder".to_string(), serde_json::to_value(&self.placeholder).unwrap_or(Value::Null)); - map.insert("Value".to_string(), serde_json::to_value(&self.value).unwrap_or(Value::Null)); + if let Some(ref v) = self.placeholder { + map.insert("Placeholder".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.value { + map.insert("Value".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } if let Some(ref v) = self.allow_custom_choice { map.insert("AllowCustomChoice".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } @@ -874,8 +882,8 @@ impl CreateInteractionInputOptions { pub struct DynamicLoadingOptions { #[serde(rename = "AlwaysLoadOnStart", skip_serializing_if = "Option::is_none")] pub always_load_on_start: Option, - #[serde(rename = "DependsOnInputs")] - pub depends_on_inputs: Vec, + #[serde(rename = "DependsOnInputs", skip_serializing_if = "Option::is_none")] + pub depends_on_inputs: Option>, } impl DynamicLoadingOptions { @@ -884,7 +892,9 @@ impl DynamicLoadingOptions { if let Some(ref v) = self.always_load_on_start { map.insert("AlwaysLoadOnStart".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - map.insert("DependsOnInputs".to_string(), serde_json::to_value(&self.depends_on_inputs).unwrap_or(Value::Null)); + if let Some(ref v) = self.depends_on_inputs { + map.insert("DependsOnInputs".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } map } } @@ -892,10 +902,10 @@ impl DynamicLoadingOptions { /// InteractionMessageBoxOptions #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct InteractionMessageBoxOptions { - #[serde(rename = "PrimaryButtonText")] - pub primary_button_text: String, - #[serde(rename = "SecondaryButtonText")] - pub secondary_button_text: String, + #[serde(rename = "PrimaryButtonText", skip_serializing_if = "Option::is_none")] + pub primary_button_text: Option, + #[serde(rename = "SecondaryButtonText", skip_serializing_if = "Option::is_none")] + pub secondary_button_text: Option, #[serde(rename = "ShowSecondaryButton", skip_serializing_if = "Option::is_none")] pub show_secondary_button: Option, #[serde(rename = "ShowDismiss", skip_serializing_if = "Option::is_none")] @@ -909,8 +919,12 @@ pub struct InteractionMessageBoxOptions { impl InteractionMessageBoxOptions { pub fn to_map(&self) -> HashMap { let mut map = HashMap::new(); - map.insert("PrimaryButtonText".to_string(), serde_json::to_value(&self.primary_button_text).unwrap_or(Value::Null)); - map.insert("SecondaryButtonText".to_string(), serde_json::to_value(&self.secondary_button_text).unwrap_or(Value::Null)); + if let Some(ref v) = self.primary_button_text { + map.insert("PrimaryButtonText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.secondary_button_text { + map.insert("SecondaryButtonText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } if let Some(ref v) = self.show_secondary_button { map.insert("ShowSecondaryButton".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } @@ -930,10 +944,10 @@ impl InteractionMessageBoxOptions { /// InteractionNotificationOptions #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct InteractionNotificationOptions { - #[serde(rename = "PrimaryButtonText")] - pub primary_button_text: String, - #[serde(rename = "SecondaryButtonText")] - pub secondary_button_text: String, + #[serde(rename = "PrimaryButtonText", skip_serializing_if = "Option::is_none")] + pub primary_button_text: Option, + #[serde(rename = "SecondaryButtonText", skip_serializing_if = "Option::is_none")] + pub secondary_button_text: Option, #[serde(rename = "ShowSecondaryButton", skip_serializing_if = "Option::is_none")] pub show_secondary_button: Option, #[serde(rename = "ShowDismiss", skip_serializing_if = "Option::is_none")] @@ -942,17 +956,21 @@ pub struct InteractionNotificationOptions { pub enable_message_markdown: Option, #[serde(rename = "Intent", skip_serializing_if = "Option::is_none")] pub intent: Option, - #[serde(rename = "LinkText")] - pub link_text: String, - #[serde(rename = "LinkUrl")] - pub link_url: String, + #[serde(rename = "LinkText", skip_serializing_if = "Option::is_none")] + pub link_text: Option, + #[serde(rename = "LinkUrl", skip_serializing_if = "Option::is_none")] + pub link_url: Option, } impl InteractionNotificationOptions { pub fn to_map(&self) -> HashMap { let mut map = HashMap::new(); - map.insert("PrimaryButtonText".to_string(), serde_json::to_value(&self.primary_button_text).unwrap_or(Value::Null)); - map.insert("SecondaryButtonText".to_string(), serde_json::to_value(&self.secondary_button_text).unwrap_or(Value::Null)); + if let Some(ref v) = self.primary_button_text { + map.insert("PrimaryButtonText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.secondary_button_text { + map.insert("SecondaryButtonText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } if let Some(ref v) = self.show_secondary_button { map.insert("ShowSecondaryButton".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } @@ -965,8 +983,12 @@ impl InteractionNotificationOptions { if let Some(ref v) = self.intent { map.insert("Intent".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - map.insert("LinkText".to_string(), serde_json::to_value(&self.link_text).unwrap_or(Value::Null)); - map.insert("LinkUrl".to_string(), serde_json::to_value(&self.link_url).unwrap_or(Value::Null)); + if let Some(ref v) = self.link_text { + map.insert("LinkText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.link_url { + map.insert("LinkUrl".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } map } } @@ -974,10 +996,10 @@ impl InteractionNotificationOptions { /// InteractionInputsDialogOptions #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct InteractionInputsDialogOptions { - #[serde(rename = "PrimaryButtonText")] - pub primary_button_text: String, - #[serde(rename = "SecondaryButtonText")] - pub secondary_button_text: String, + #[serde(rename = "PrimaryButtonText", skip_serializing_if = "Option::is_none")] + pub primary_button_text: Option, + #[serde(rename = "SecondaryButtonText", skip_serializing_if = "Option::is_none")] + pub secondary_button_text: Option, #[serde(rename = "ShowSecondaryButton", skip_serializing_if = "Option::is_none")] pub show_secondary_button: Option, #[serde(rename = "ShowDismiss", skip_serializing_if = "Option::is_none")] @@ -989,8 +1011,12 @@ pub struct InteractionInputsDialogOptions { impl InteractionInputsDialogOptions { pub fn to_map(&self) -> HashMap { let mut map = HashMap::new(); - map.insert("PrimaryButtonText".to_string(), serde_json::to_value(&self.primary_button_text).unwrap_or(Value::Null)); - map.insert("SecondaryButtonText".to_string(), serde_json::to_value(&self.secondary_button_text).unwrap_or(Value::Null)); + if let Some(ref v) = self.primary_button_text { + map.insert("PrimaryButtonText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = self.secondary_button_text { + map.insert("SecondaryButtonText".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } if let Some(ref v) = self.show_secondary_button { map.insert("ShowSecondaryButton".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index edb08bed441..84236f9b865 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -639,8 +639,8 @@ ENTRYPOINT ["dotnet", "App.dll"] confirmIntent := aspire.MessageIntentConfirmation confirmation, err := interactionService.PromptConfirmation("Confirm", "Proceed?", &aspire.PromptConfirmationOptions{ Options: &aspire.InteractionMessageBoxOptions{ - PrimaryButtonText: "Yes", - SecondaryButtonText: "No", + PrimaryButtonText: aspire.StringPtr("Yes"), + SecondaryButtonText: aspire.StringPtr("No"), ShowSecondaryButton: aspire.BoolPtr(true), ShowDismiss: aspire.BoolPtr(true), EnableMessageMarkdown: aspire.BoolPtr(true), @@ -653,7 +653,7 @@ ENTRYPOINT ["dotnet", "App.dll"] infoIntent := aspire.MessageIntentInformation messageBox, err := interactionService.PromptMessageBox("Notice", "Read this.", &aspire.PromptMessageBoxOptions{ - Options: &aspire.InteractionMessageBoxOptions{PrimaryButtonText: "OK", Intent: &infoIntent}, + Options: &aspire.InteractionMessageBoxOptions{PrimaryButtonText: aspire.StringPtr("OK"), Intent: &infoIntent}, }) if err != nil { return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} @@ -663,8 +663,8 @@ ENTRYPOINT ["dotnet", "App.dll"] notification, err := interactionService.PromptNotification("Heads up", "Something happened.", &aspire.PromptNotificationOptions{ Options: &aspire.InteractionNotificationOptions{ Intent: &warnIntent, - LinkText: "Learn more", - LinkUrl: "https://aspire.dev", + LinkText: aspire.StringPtr("Learn more"), + LinkUrl: aspire.StringPtr("https://aspire.dev"), ShowDismiss: aspire.BoolPtr(true), }, }) @@ -674,12 +674,12 @@ ENTRYPOINT ["dotnet", "App.dll"] textInput := interactionService.CreateTextInput("name", &aspire.CreateTextInputOptions{ Options: &aspire.CreateInteractionInputOptions{ - Label: "Name", - Description: "Your **name**", + Label: aspire.StringPtr("Name"), + Description: aspire.StringPtr("Your **name**"), EnableDescriptionMarkdown: aspire.BoolPtr(true), Required: aspire.BoolPtr(true), - Placeholder: "Jane Doe", - Value: "Jane", + Placeholder: aspire.StringPtr("Jane Doe"), + Value: aspire.StringPtr("Jane"), MaxLength: aspire.Float64Ptr(64), Disabled: aspire.BoolPtr(false), }, @@ -688,10 +688,10 @@ ENTRYPOINT ["dotnet", "App.dll"] Options: &aspire.CreateInteractionInputOptions{Required: aspire.BoolPtr(true)}, }) booleanInput := interactionService.CreateBooleanInput("enabled", &aspire.CreateBooleanInputOptions{ - Options: &aspire.CreateInteractionInputOptions{Value: "true"}, + Options: &aspire.CreateInteractionInputOptions{Value: aspire.StringPtr("true")}, }) numberInput := interactionService.CreateNumberInput("count", &aspire.CreateNumberInputOptions{ - Options: &aspire.CreateInteractionInputOptions{Value: "1"}, + Options: &aspire.CreateInteractionInputOptions{Value: aspire.StringPtr("1")}, }) choiceInput := interactionService.CreateChoiceInput("color", &aspire.CreateChoiceInputOptions{ Choices: []*aspire.InteractionChoiceOption{{Value: "r", Label: "Red"}, {Value: "g", Label: "Green"}}, @@ -713,7 +713,7 @@ ENTRYPOINT ["dotnet", "App.dll"] }) single, err := interactionService.PromptInput("Single input", "Enter a value.", interactionService.CreateTextInput("solo"), &aspire.PromptInputOptions{ - Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: "Save"}, + Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: aspire.StringPtr("Save")}, ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { inputs, _ := validationContext.Inputs().ToArray() for _, input := range inputs { @@ -730,7 +730,7 @@ ENTRYPOINT ["dotnet", "App.dll"] multi, err := interactionService.PromptInputs("Multiple inputs", "Fill out the form.", []aspire.InteractionInputBuilder{textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput}, &aspire.PromptInputsOptions{ - Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: "Submit", EnableMessageMarkdown: aspire.BoolPtr(true)}, + Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: aspire.StringPtr("Submit"), EnableMessageMarkdown: aspire.BoolPtr(true)}, ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { inputs, _ := validationContext.Inputs().ToArray() for _, input := range inputs { From 6862a0995b62e2a8b0c6e11718b1aebee7606117 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Jun 2026 12:12:47 -0700 Subject: [PATCH 10/20] Harden polyglot interaction inputs: guard null elements, non-null GetInputValue Address PR review feedback on the interaction-service exports: - PromptInputs now validates each array element and throws a clear ArgumentException naming the parameter and index instead of letting a null element (possible over JSON-RPC) surface as a NullReferenceException. - GetInputValue returns an empty string instead of null when an input has no value or no input with that name exists. The ATS code-generation system has no support for nullable reference return types, so every generated SDK already types this as a non-nullable string; returning null caused Go/Rust clients to fail decoding and made the TS contract unsound. Updated the TypeScript snapshot for the revised returns doc text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 9 +++++---- .../Snapshots/TwoPassScanningGeneratedAspire.verified.ts | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index b2e1ba05ada..1093eeba406 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -141,7 +141,8 @@ public static async Task PromptInputs( var interactionInputs = new InteractionInput[inputs.Length]; for (var i = 0; i < inputs.Length; i++) { - interactionInputs[i] = inputs[i].Input; + var input = inputs[i] ?? throw new ArgumentException($"The input at index {i} cannot be null.", nameof(inputs)); + interactionInputs[i] = input.Input; } var result = await interactionService.PromptInputsAsync(title, message, interactionInputs, BuildDialogOptions(options, validationCallback), cancellationToken).ConfigureAwait(false); @@ -388,13 +389,13 @@ public string GetInputName() /// Gets the current value of an input in the prompt by name. /// /// The name of the input to read. - /// The input value, or when the input has no value or does not exist. + /// The input value, or an empty string when the input has no value or no input with that name exists. [AspireExport] - public string? GetInputValue(string inputName) + public string GetInputValue(string inputName) { ArgumentNullException.ThrowIfNull(inputName); - return _inner.AllInputs.TryGetByName(inputName, out var input) ? input.Value : null; + return _inner.AllInputs.TryGetByName(inputName, out var input) ? input.Value ?? string.Empty : string.Empty; } /// diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index c58cfe6217f..1753bab4f90 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -5678,7 +5678,7 @@ export interface InteractionInputLoadContext { /** * Gets the current value of an input in the prompt by name. * @param inputName The name of the input to read. - * @returns The input value, or `null` when the input has no value or does not exist. + * @returns The input value, or an empty string when the input has no value or no input with that name exists. */ getInputValue(inputName: string): Promise; /** @@ -5702,7 +5702,7 @@ export interface InteractionInputLoadContextPromise extends PromiseLike; /** @@ -5743,7 +5743,7 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { /** * Gets the current value of an input in the prompt by name. * @param inputName The name of the input to read. - * @returns The input value, or `null` when the input has no value or does not exist. + * @returns The input value, or an empty string when the input has no value or no input with that name exists. */ async getInputValue(inputName: string): Promise { const rpcArgs: Record = { context: this._handle, inputName }; From a03dab99b37ddedc8439d859165cb1263bc01038 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 09:46:58 -0700 Subject: [PATCH 11/20] Move validationCallback onto InteractionInputsDialogOptions DTO; emit strong DTO callbacks for Go/Java/Python Relocate the inputs-dialog validation callback from a method parameter of promptInput/promptInputs onto the InteractionInputsDialogOptions DTO property, matching the C# IInteractionService usage and leveraging DTO-callback support. DTO-property callbacks previously rendered weakly typed (Object / func(...any) any) in Go/Java/Python while TypeScript already rendered them strongly typed. Teach the Go, Java, and Python generators to render DTO callback properties with full strong typing using the same metadata TS already consumes (no runtime-template changes: each language's recursive transport marshaller already registers funcs/closures embedded in serialized DTO maps). - Go: DTO callback fields use RenderCallbackType; ToMap embeds a typed shim via a shared EmitGoCallbackShimBody factored out of EmitCallbackRegistration. - Java: strong field/getter/setter; toMap wraps AspireActionN/AspireFuncN in a java.util.function.Function with typed arg conversion (guarded to <=1 param to match the single-arg runtime wrapper); fromMap skips client->host callbacks. Lambda parameter renamed to transportArg to avoid shadowing the converted local. - Python: DTO callback properties use typing.Callable[[T], R]. - Rust: DTO callbacks remain Option (serde derive cannot serialize Box); expanded comment documents the known follow-up, consistent with existing PrepareRequest/CreateProcessSpec handling. Regenerated ats.txt and the 5 codegen snapshots, and updated the Go/Java/Python/TS polyglot apphosts to set validationCallback inside InteractionInputsDialogOptions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsGoCodeGenerator.cs | 77 ++++++++++--- .../AtsJavaCodeGenerator.cs | 74 +++++++++++-- .../AtsPythonCodeGenerator.cs | 9 +- .../AtsRustCodeGenerator.cs | 11 +- src/Aspire.Hosting/Ats/InteractionExports.cs | 41 ++----- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 5 +- ...TwoPassScanningGeneratedAspire.verified.go | 63 ++++++----- ...oPassScanningGeneratedAspire.verified.java | 101 +++++++----------- ...TwoPassScanningGeneratedAspire.verified.py | 17 ++- ...TwoPassScanningGeneratedAspire.verified.rs | 13 +-- ...TwoPassScanningGeneratedAspire.verified.ts | 48 +++++---- .../Aspire.Hosting/Go/apphost.go | 33 +++--- .../Aspire.Hosting/Java/AppHost.java | 30 +++--- .../Aspire.Hosting/Python/apphost.py | 6 +- .../Aspire.Hosting/TypeScript/apphost.mts | 29 ++--- 15 files changed, 337 insertions(+), 220 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs index 6c06d4f4e71..ef2d29d712f 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs @@ -540,7 +540,12 @@ private void GenerateDtoTypes(IReadOnlyList dtoTypes) foreach (var property in dto.Properties) { var propertyName = ToPascalCase(property.Name); - var propertyType = MapDtoPropertyTypeToGo(property.Type, property.IsOptional); + // Callback-typed DTO properties carry the same metadata as method parameters, so + // render the strongly-typed func signature (e.g. func(ctx InputsDialogValidationContext)) + // instead of the weak func(...any) any fallback. + var propertyType = property.IsCallback + ? RenderCallbackType(DtoPropertyToParameterInfo(property)) + : MapDtoPropertyTypeToGo(property.Type, property.IsOptional); var jsonTag = $"`json:\"{property.Name},omitempty\"`"; WriteLine($"\t{propertyName} {propertyType} {jsonTag}"); } @@ -553,6 +558,11 @@ private void GenerateDtoTypes(IReadOnlyList dtoTypes) foreach (var property in dto.Properties) { var propertyName = ToPascalCase(property.Name); + if (property.IsCallback) + { + EmitDtoCallbackToMap(property, propertyName); + continue; + } var propertyType = MapDtoPropertyTypeToGo(property.Type, property.IsOptional); if (IsNilableGoType(propertyType)) { @@ -1370,6 +1380,47 @@ private void EmitArgsConstruction( /// the user-supplied callback's parameter types. /// private void EmitCallbackRegistration(string indent, AtsParameterInfo p, string callbackExpr) + { + WriteLine($"{indent}if {callbackExpr} != nil {{"); + WriteLine($"{indent}\tcb := {callbackExpr}"); + WriteLine($"{indent}\tshim := func(args ...any) any {{"); + EmitGoCallbackShimBody($"{indent}\t\t", p); + WriteLine($"{indent}\t}}"); + WriteLine($"{indent}\treqArgs[\"{p.Name}\"] = s.client.registerCallback(shim)"); + WriteLine($"{indent}}}"); + } + + // Emits a strongly-typed DTO callback property into the DTO's ToMap output. Method parameters + // pre-register their callback (putting a string id into reqArgs), but DTO properties instead + // embed the shim func directly in the map; the client's marshalTransportValue walks the + // serialized args, finds the func(...any) any value, and registers it. This keeps the public + // DTO field strongly typed (e.g. func(ctx InputsDialogValidationContext)) while reusing the + // exact arg-decoding/return/writeback behavior of method-parameter callbacks. + private void EmitDtoCallbackToMap(AtsDtoPropertyInfo property, string propertyName) + { + var p = DtoPropertyToParameterInfo(property); + WriteLine($"\tif d.{propertyName} != nil {{"); + WriteLine($"\t\tcb := d.{propertyName}"); + WriteLine($"\t\tm[\"{property.Name}\"] = func(args ...any) any {{"); + EmitGoCallbackShimBody("\t\t\t", p); + WriteLine($"\t\t}}"); + WriteLine($"\t}}"); + } + + private static AtsParameterInfo DtoPropertyToParameterInfo(AtsDtoPropertyInfo property) + => new() + { + Name = property.Name, + Type = property.Type, + IsOptional = property.IsOptional, + IsCallback = property.IsCallback, + CallbackParameters = property.CallbackParameters, + CallbackReturnType = property.CallbackReturnType, + }; + + // Writes the body of a `func(args ...any) any` callback shim, assuming the user's typed + // callback is in scope as `cb`. Shared by method-parameter and DTO-property callbacks. + private void EmitGoCallbackShimBody(string indent, AtsParameterInfo p) { var hasReturn = p.CallbackReturnType is not null && p.CallbackReturnType.TypeId != AtsConstants.Void; @@ -1390,17 +1441,14 @@ private void EmitCallbackRegistration(string indent, AtsParameterInfo p, string } callExpr.Append(')'); - WriteLine($"{indent}if {callbackExpr} != nil {{"); - WriteLine($"{indent}\tcb := {callbackExpr}"); - WriteLine($"{indent}\tshim := func(args ...any) any {{"); if (hasReturn) { - WriteLine($"{indent}\t\treturn {callExpr}"); + WriteLine($"{indent}return {callExpr}"); } else if (p.CallbackParameters is null) { // Legacy untyped callback returning any — preserve return value. - WriteLine($"{indent}\t\treturn {callExpr}"); + WriteLine($"{indent}return {callExpr}"); } else if (p.CallbackParameters is { Count: > 0 } callbackParameters && callbackParameters.Any(cp => cp.Type.Category == AtsTypeCategory.Dto)) { @@ -1410,28 +1458,25 @@ private void EmitCallbackRegistration(string indent, AtsParameterInfo p, string var argName = $"arg{i}"; argNames.Add(argName); var goType = MapTypeRefToGo(callbackParameters[i].Type, false); - WriteLine($"{indent}\t\t{argName} := callbackArg[{goType}](args, {i})"); + WriteLine($"{indent}{argName} := callbackArg[{goType}](args, {i})"); } - WriteLine($"{indent}\t\tcb({string.Join(", ", argNames)})"); - WriteLine($"{indent}\t\treturn map[string]any{{"); + WriteLine($"{indent}cb({string.Join(", ", argNames)})"); + WriteLine($"{indent}return map[string]any{{"); for (var i = 0; i < callbackParameters.Count; i++) { if (callbackParameters[i].Type.Category == AtsTypeCategory.Dto) { - WriteLine($"{indent}\t\t\t\"p{i}\": serializeValue({argNames[i]}),"); + WriteLine($"{indent}\t\"p{i}\": serializeValue({argNames[i]}),"); } } - WriteLine($"{indent}\t\t}}"); + WriteLine($"{indent}}}"); } else { - WriteLine($"{indent}\t\t{callExpr}"); - WriteLine($"{indent}\t\treturn nil"); + WriteLine($"{indent}{callExpr}"); + WriteLine($"{indent}return nil"); } - WriteLine($"{indent}\t}}"); - WriteLine($"{indent}\treqArgs[\"{p.Name}\"] = s.client.registerCallback(shim)"); - WriteLine($"{indent}}}"); } // ── List / Dict accessor methods ───────────────────────────────────────── diff --git a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs index 9023345692e..3a88aab2d42 100644 --- a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs @@ -534,7 +534,7 @@ private void GenerateDtoTypes(IReadOnlyList dtoTypes) foreach (var property in dto.Properties) { var fieldName = ToCamelCase(property.Name); - var fieldType = MapDtoPropertyTypeToJava(property.Type, property.IsOptional); + var fieldType = MapDtoFieldTypeToJava(property); WriteLine($" private {fieldType} {fieldName};"); } WriteLine(); @@ -544,7 +544,7 @@ private void GenerateDtoTypes(IReadOnlyList dtoTypes) { var fieldName = ToCamelCase(property.Name); var methodName = ToPascalCase(property.Name); - var fieldType = MapDtoPropertyTypeToJava(property.Type, property.IsOptional); + var fieldType = MapDtoFieldTypeToJava(property); WriteLine($" public {fieldType} get{methodName}() {{ return {fieldName}; }}"); WriteLine($" public void set{methodName}({fieldType} value) {{ this.{fieldName} = value; }}"); @@ -556,6 +556,13 @@ private void GenerateDtoTypes(IReadOnlyList dtoTypes) WriteLine($" var value = new {dtoName}();"); foreach (var property in dto.Properties) { + // Strongly-typed callback properties cannot be reconstructed from transport data: + // callbacks only flow from client to host, never back. Skip them in fromMap so the + // generated code does not pass a raw transport value to the typed setter. + if (IsStronglyTypedDtoCallback(property)) + { + continue; + } var fieldName = ToCamelCase(property.Name); var methodName = ToPascalCase(property.Name); var transportValueName = $"{fieldName}Value"; @@ -572,7 +579,14 @@ private void GenerateDtoTypes(IReadOnlyList dtoTypes) foreach (var property in dto.Properties) { var fieldName = ToCamelCase(property.Name); - WriteLine($" map.put(\"{property.Name}\", AspireClient.serializeValue({fieldName}));"); + if (IsStronglyTypedDtoCallback(property)) + { + EmitJavaDtoCallbackToMap(property); + } + else + { + WriteLine($" map.put(\"{property.Name}\", AspireClient.serializeValue({fieldName}));"); + } } WriteLine(" return map;"); WriteLine(" }"); @@ -1672,19 +1686,67 @@ private void GenerateCallbackBody(string callbackName, AtsParameterInfo callback } } + // A DTO callback property is rendered with a strong functional-interface type only when it has + // at most one parameter. The runtime marshaller registers DTO-embedded callbacks as a single-arg + // Function (args[0] only), so multi-parameter DTO callbacks must keep the weak Object fallback to + // avoid generating a strongly-typed API that silently drops arguments. All current DTO callbacks + // (e.g. validation/prepare-request contexts) are single-parameter. + private static bool IsStronglyTypedDtoCallback(AtsDtoPropertyInfo property) + => property.IsCallback && (property.CallbackParameters?.Count ?? 0) <= 1; + + private string MapDtoFieldTypeToJava(AtsDtoPropertyInfo property) + => IsStronglyTypedDtoCallback(property) + ? GenerateCallbackTypeSignature(property.CallbackParameters, property.CallbackReturnType) + : MapDtoPropertyTypeToJava(property.Type, property.IsOptional); + + // Serializes a strongly-typed DTO callback property by wrapping the user's AspireAction/AspireFunc + // in a java.util.function.Function. The client's marshalTransportValue detects Function values in + // the serialized DTO map and registers them, invoking the Function with the unwrapped first + // argument. This mirrors the typed arg-conversion used for method-parameter callbacks. + private void EmitJavaDtoCallbackToMap(AtsDtoPropertyInfo property) + { + var fieldName = ToCamelCase(property.Name); + var hasReturnType = property.CallbackReturnType != null && property.CallbackReturnType.TypeId != AtsConstants.Void; + var callbackParameter = property.CallbackParameters is { Count: 1 } ? property.CallbackParameters[0] : null; + + WriteLine($" map.put(\"{property.Name}\", {fieldName} == null ? null : (java.util.function.Function) (transportArg -> {{"); + var invocationArgument = string.Empty; + if (callbackParameter is not null) + { + var callbackParameterName = ToCamelCase(callbackParameter.Name); + WriteLine($" var {callbackParameterName} = {GetCallbackArgumentExpression(callbackParameter, "transportArg")};"); + invocationArgument = callbackParameterName; + } + + var invocation = $"{fieldName}.invoke({invocationArgument})"; + if (hasReturnType) + { + WriteLine($" return AspireClient.awaitValue({invocation});"); + } + else + { + WriteLine($" {invocation};"); + WriteLine(" return null;"); + } + WriteLine(" }));"); + } + private string GetCallbackArgumentExpression(AtsCallbackParameterInfo callbackParameter, int index) + => GetCallbackArgumentExpression(callbackParameter, $"args[{index}]"); + + private string GetCallbackArgumentExpression(AtsCallbackParameterInfo callbackParameter, string argumentExpression) { if (callbackParameter.Type?.TypeId == AtsConstants.CancellationToken) { - return $"CancellationToken.fromValue(args[{index}])"; + return $"CancellationToken.fromValue({argumentExpression})"; } if (IsUnionType(callbackParameter.Type)) { - return $"AspireUnion.of(args[{index}])"; + return $"AspireUnion.of({argumentExpression})"; } - return RenderJavaTransportValueConversion(callbackParameter.Type, $"args[{index}]", callbackParameter.Type?.IsNullable == true); + return RenderJavaTransportValueConversion(callbackParameter.Type, argumentExpression, callbackParameter.Type?.IsNullable == true); } private string RenderJavaTransportValueConversion(AtsTypeRef? typeRef, string valueExpression, bool isOptional, int depth = 0) diff --git a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs index 48d1447bbbf..6d7479ee062 100644 --- a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs @@ -832,7 +832,14 @@ private void GenerateDtoClasses(IReadOnlyList dtoTypes) sb.AppendLine(CultureInfo.InvariantCulture, $"class {className}(typing.TypedDict, total=False):"); foreach (var prop in dtoType.Properties) { - var propType = MapDtoPropertyTypeToPython(prop.Type); + // Callback-typed DTO properties carry the same CallbackParameters/CallbackReturnType + // metadata as method parameters, so render the strongly-typed Callable signature + // (e.g. typing.Callable[[InputsDialogValidationContext], None]) instead of a bare + // typing.Callable. The runtime marshaller already registers callables embedded in + // DTO dicts, so no serialization change is needed. + var propType = prop.IsCallback + ? GenerateCallbackTypeSignature(prop.CallbackParameters, prop.CallbackReturnType) + : MapDtoPropertyTypeToPython(prop.Type); sb.AppendLine(CultureInfo.InvariantCulture, $" {prop.Name}: {propType}"); } sb.AppendLine(); diff --git a/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs index 4acdeac6666..caa831065a1 100644 --- a/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs @@ -945,7 +945,16 @@ private string MapTypeRefToRustForDto(AtsTypeRef? typeRef, bool isOptional) // Use Handle directly for handle types in DTOs since Handle implements Serialize/Deserialize AtsTypeCategory.Handle => "Handle", AtsTypeCategory.Dto => MapDtoType(typeRef.TypeId), - AtsTypeCategory.Callback => "Value", // Callbacks can't be serialized in DTOs + // DTO callback properties remain weakly typed (a serde_json::Value) in Rust. Unlike Go, Java, + // Python, and TypeScript -- whose generators emit strongly-typed DTO callbacks and whose + // runtimes auto-register closures embedded in serialized DTO maps -- Rust DTOs are built on + // serde derive, and a Box closure is not serde-serializable. Supporting it would + // require serde-skipping the field plus a hand-written to_map that registers the closure via + // the global register_callback. This matches the existing behavior for all other DTO + // callbacks (e.g. HttpCommandExportOptions.PrepareRequest from PR #17950), and there is no + // Rust polyglot bench to validate a runtime implementation, so Rust DTO callbacks are left + // weakly typed as a known follow-up. + AtsTypeCategory.Callback => "Value", AtsTypeCategory.Array => $"Vec<{MapTypeRefToRustForDto(typeRef.ElementType, false)}>", AtsTypeCategory.List => $"Vec<{MapTypeRefToRustForDto(typeRef.ElementType, false)}>", AtsTypeCategory.Dict => $"HashMap<{MapTypeRefToRustForDto(typeRef.KeyType, false)}, {MapTypeRefToRustForDto(typeRef.ValueType, false)}>", diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index 1093eeba406..78dab226bb0 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -101,8 +101,8 @@ public static async Task PromptNotification( /// /// Prompts the user for a single input. /// - // Prompts can invoke dynamic-loading callbacks that re-enter the remote host through ATS, so the synchronous - // invocation path must run on a background thread to keep the JSON-RPC loop processing nested callbacks. + // Prompts can invoke dynamic-loading and validation callbacks that re-enter the remote host through ATS, so the + // synchronous invocation path must run on a background thread to keep the JSON-RPC loop processing nested callbacks. [AspireExport(RunSyncOnBackgroundThread = true)] public static async Task PromptInput( this IInteractionService interactionService, @@ -110,13 +110,12 @@ public static async Task PromptInput( string? message, InteractionInputBuilder input, InteractionInputsDialogOptions? options = null, - Func? validationCallback = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(interactionService); ArgumentNullException.ThrowIfNull(input); - var result = await interactionService.PromptInputAsync(title, message, input.Input, BuildDialogOptions(options, validationCallback), cancellationToken).ConfigureAwait(false); + var result = await interactionService.PromptInputAsync(title, message, input.Input, options?.ToOptions(), cancellationToken).ConfigureAwait(false); return InputInteractionResult.From(result); } @@ -132,7 +131,6 @@ public static async Task PromptInputs( string? message, InteractionInputBuilder[] inputs, InteractionInputsDialogOptions? options = null, - Func? validationCallback = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(interactionService); @@ -145,34 +143,10 @@ public static async Task PromptInputs( interactionInputs[i] = input.Input; } - var result = await interactionService.PromptInputsAsync(title, message, interactionInputs, BuildDialogOptions(options, validationCallback), cancellationToken).ConfigureAwait(false); + var result = await interactionService.PromptInputsAsync(title, message, interactionInputs, options?.ToOptions(), cancellationToken).ConfigureAwait(false); return InputsInteractionResult.From(result); } - // Bridges the polyglot dialog options DTO and an optional validation callback to the native dialog options. - // The validation callback is supplied as a method argument (not on the DTO) because delegates cannot be - // serialized across the ATS boundary. The native InputsDialogValidationContext is already an exported, curated - // handle (its IServiceProvider is [AspireExportIgnore]'d), so it can be handed to the polyglot callback directly. - private static InputsDialogInteractionOptions? BuildDialogOptions( - InteractionInputsDialogOptions? options, - Func? validationCallback) - { - if (options is null && validationCallback is null) - { - return null; - } - - // Never mutate the shared InputsDialogInteractionOptions.Default singleton; ToOptions() already returns a - // fresh instance, so only allocate a new one when no DTO options were provided. - var nativeOptions = options?.ToOptions() ?? new InputsDialogInteractionOptions(); - if (validationCallback is not null) - { - nativeOptions.ValidationCallback = validationCallback; - } - - return nativeOptions; - } - // The input factories hang off IInteractionService so the ATS scanner treats the service handle as the // receiver (polyglot: interactionService.createTextInput(...)). The receiver itself is unused because inputs // are independent of the service, so suppress the unused-parameter analyzer for the factory block. @@ -653,6 +627,12 @@ internal sealed class InteractionInputsDialogOptions /// public bool? EnableMessageMarkdown { get; init; } + /// + /// Gets or sets a callback invoked to validate the inputs before the dialog is accepted. The callback + /// receives a validation context that exposes the current inputs and can record validation errors. + /// + public Func? ValidationCallback { get; init; } + internal InputsDialogInteractionOptions ToOptions() { return new InputsDialogInteractionOptions @@ -662,6 +642,7 @@ internal InputsDialogInteractionOptions ToOptions() ShowSecondaryButton = ShowSecondaryButton, ShowDismiss = ShowDismiss, EnableMessageMarkdown = EnableMessageMarkdown, + ValidationCallback = ValidationCallback, }; } } diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index a991fd7b6a5..8a1cfc50e41 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -258,6 +258,7 @@ Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions # Options for i SecondaryButtonText?: string # Gets or sets the secondary button text. ShowDismiss?: boolean # Gets or sets a value indicating whether the dismiss button is shown. ShowSecondaryButton?: boolean # Gets or sets a value indicating whether the secondary button is shown. + ValidationCallback?: callback # Gets or sets a callback invoked to validate the inputs before the dialog is accepted. The callback receives a validation context that exposes the current inputs and can record validation errors. Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions # Options for message box and confirmation prompts. EnableMessageMarkdown?: boolean # Gets or sets a value indicating whether Markdown in the message is rendered. Intent?: enum:Aspire.Hosting.MessageIntent # Gets or sets the intent of the message box. @@ -668,8 +669,8 @@ Aspire.Hosting/ProjectResourceOptions.setExcludeKestrelEndpoints(context: Aspire Aspire.Hosting/ProjectResourceOptions.setExcludeLaunchProfile(context: Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions, value: boolean) -> Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions Aspire.Hosting/ProjectResourceOptions.setLaunchProfileName(context: Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions, value: string) -> Aspire.Hosting/Aspire.Hosting.ProjectResourceOptions Aspire.Hosting/promptConfirmation(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult -Aspire.Hosting/promptInput(title: string, message: string, input: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, validationCallback?: callback, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult -Aspire.Hosting/promptInputs(title: string, message: string, inputs: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder[], options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, validationCallback?: callback, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult +Aspire.Hosting/promptInput(title: string, message: string, input: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult +Aspire.Hosting/promptInputs(title: string, message: string, inputs: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder[], options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputsDialogOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult Aspire.Hosting/promptMessageBox(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionMessageBoxOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult Aspire.Hosting/promptNotification(title: string, message: string, options?: Aspire.Hosting/Aspire.Hosting.Ats.InteractionNotificationOptions, cancellationToken?: cancellationToken) -> Aspire.Hosting/Aspire.Hosting.Ats.BoolInteractionResult Aspire.Hosting/publishAsConnectionString() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index ef07385e2f3..932432d017d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -519,6 +519,7 @@ type InteractionInputsDialogOptions struct { ShowSecondaryButton *bool `json:"ShowSecondaryButton,omitempty"` ShowDismiss *bool `json:"ShowDismiss,omitempty"` EnableMessageMarkdown *bool `json:"EnableMessageMarkdown,omitempty"` + ValidationCallback func(arg InputsDialogValidationContext) `json:"ValidationCallback,omitempty"` } // ToMap converts the DTO to a map for JSON serialization. @@ -529,6 +530,13 @@ func (d *InteractionInputsDialogOptions) ToMap() map[string]any { if d.ShowSecondaryButton != nil { m["ShowSecondaryButton"] = serializeValue(d.ShowSecondaryButton) } if d.ShowDismiss != nil { m["ShowDismiss"] = serializeValue(d.ShowDismiss) } if d.EnableMessageMarkdown != nil { m["EnableMessageMarkdown"] = serializeValue(d.EnableMessageMarkdown) } + if d.ValidationCallback != nil { + cb := d.ValidationCallback + m["ValidationCallback"] = func(args ...any) any { + cb(callbackArg[InputsDialogValidationContext](args, 0)) + return nil + } + } return m } @@ -667,13 +675,13 @@ type CommandOptions struct { Description string `json:"Description,omitempty"` Parameter any `json:"Parameter,omitempty"` Arguments []*InteractionInput `json:"Arguments,omitempty"` - ValidateArguments func(...any) any `json:"ValidateArguments,omitempty"` + ValidateArguments func(arg InputsDialogValidationContext) `json:"ValidateArguments,omitempty"` Visibility ResourceCommandVisibility `json:"Visibility,omitempty"` ConfirmationMessage string `json:"ConfirmationMessage,omitempty"` IconName string `json:"IconName,omitempty"` IconVariant *IconVariant `json:"IconVariant,omitempty"` IsHighlighted bool `json:"IsHighlighted,omitempty"` - UpdateState func(...any) any `json:"UpdateState,omitempty"` + UpdateState func(arg UpdateCommandStateContext) ResourceCommandState `json:"UpdateState,omitempty"` } // ToMap converts the DTO to a map for JSON serialization. @@ -682,13 +690,24 @@ func (d *CommandOptions) ToMap() map[string]any { m["Description"] = serializeValue(d.Description) if d.Parameter != nil { m["Parameter"] = serializeValue(d.Parameter) } if d.Arguments != nil { m["Arguments"] = serializeValue(d.Arguments) } - if d.ValidateArguments != nil { m["ValidateArguments"] = serializeValue(d.ValidateArguments) } + if d.ValidateArguments != nil { + cb := d.ValidateArguments + m["ValidateArguments"] = func(args ...any) any { + cb(callbackArg[InputsDialogValidationContext](args, 0)) + return nil + } + } m["Visibility"] = serializeValue(d.Visibility) m["ConfirmationMessage"] = serializeValue(d.ConfirmationMessage) m["IconName"] = serializeValue(d.IconName) if d.IconVariant != nil { m["IconVariant"] = serializeValue(d.IconVariant) } m["IsHighlighted"] = serializeValue(d.IsHighlighted) - if d.UpdateState != nil { m["UpdateState"] = serializeValue(d.UpdateState) } + if d.UpdateState != nil { + cb := d.UpdateState + m["UpdateState"] = func(args ...any) any { + return cb(callbackArg[UpdateCommandStateContext](args, 0)) + } + } return m } @@ -703,7 +722,7 @@ type HttpCommandExportOptions struct { CommandName string `json:"CommandName,omitempty"` EndpointName string `json:"EndpointName,omitempty"` MethodName string `json:"MethodName,omitempty"` - PrepareRequest func(...any) any `json:"PrepareRequest,omitempty"` + PrepareRequest func(arg HttpCommandPrepareRequestContext) *HttpCommandRequestExportData `json:"PrepareRequest,omitempty"` ResultMode HttpCommandResultMode `json:"ResultMode,omitempty"` } @@ -719,7 +738,12 @@ func (d *HttpCommandExportOptions) ToMap() map[string]any { m["CommandName"] = serializeValue(d.CommandName) m["EndpointName"] = serializeValue(d.EndpointName) m["MethodName"] = serializeValue(d.MethodName) - if d.PrepareRequest != nil { m["PrepareRequest"] = serializeValue(d.PrepareRequest) } + if d.PrepareRequest != nil { + cb := d.PrepareRequest + m["PrepareRequest"] = func(args ...any) any { + return cb(callbackArg[HttpCommandPrepareRequestContext](args, 0)) + } + } m["ResultMode"] = serializeValue(d.ResultMode) return m } @@ -795,7 +819,7 @@ type ProcessCommandExportOptions struct { InheritEnvironmentVariables *bool `json:"InheritEnvironmentVariables,omitempty"` StandardInputContent string `json:"StandardInputContent,omitempty"` KillEntireProcessTree *bool `json:"KillEntireProcessTree,omitempty"` - CreateProcessSpec func(...any) any `json:"CreateProcessSpec,omitempty"` + CreateProcessSpec func(arg ExecuteCommandContext) *ProcessCommandSpecExportData `json:"CreateProcessSpec,omitempty"` CommandOptions *CommandOptions `json:"CommandOptions,omitempty"` MaxOutputLineCount *float64 `json:"MaxOutputLineCount,omitempty"` DisplayImmediately *bool `json:"DisplayImmediately,omitempty"` @@ -812,7 +836,12 @@ func (d *ProcessCommandExportOptions) ToMap() map[string]any { if d.InheritEnvironmentVariables != nil { m["InheritEnvironmentVariables"] = serializeValue(d.InheritEnvironmentVariables) } m["StandardInputContent"] = serializeValue(d.StandardInputContent) if d.KillEntireProcessTree != nil { m["KillEntireProcessTree"] = serializeValue(d.KillEntireProcessTree) } - if d.CreateProcessSpec != nil { m["CreateProcessSpec"] = serializeValue(d.CreateProcessSpec) } + if d.CreateProcessSpec != nil { + cb := d.CreateProcessSpec + m["CreateProcessSpec"] = func(args ...any) any { + return cb(callbackArg[ExecuteCommandContext](args, 0)) + } + } if d.CommandOptions != nil { m["CommandOptions"] = serializeValue(d.CommandOptions) } if d.MaxOutputLineCount != nil { m["MaxOutputLineCount"] = serializeValue(d.MaxOutputLineCount) } if d.DisplayImmediately != nil { m["DisplayImmediately"] = serializeValue(d.DisplayImmediately) } @@ -16545,14 +16574,6 @@ func (s *interactionService) PromptInput(title string, message string, input Int if opt != nil { merged = deepUpdate(merged, opt) } } for k, v := range merged.ToMap() { reqArgs[k] = v } - if merged.ValidationCallback != nil { - cb := merged.ValidationCallback - shim := func(args ...any) any { - cb(callbackArg[InputsDialogValidationContext](args, 0)) - return nil - } - reqArgs["validationCallback"] = s.client.registerCallback(shim) - } if merged.CancellationToken != nil { ctx = merged.CancellationToken.Context() if id := s.client.registerCancellation(merged.CancellationToken); id != "" { @@ -16584,14 +16605,6 @@ func (s *interactionService) PromptInputs(title string, message string, inputs [ if opt != nil { merged = deepUpdate(merged, opt) } } for k, v := range merged.ToMap() { reqArgs[k] = v } - if merged.ValidationCallback != nil { - cb := merged.ValidationCallback - shim := func(args ...any) any { - cb(callbackArg[InputsDialogValidationContext](args, 0)) - return nil - } - reqArgs["validationCallback"] = s.client.registerCallback(shim) - } if merged.CancellationToken != nil { ctx = merged.CancellationToken.Context() if id := s.client.registerCancellation(merged.CancellationToken); id != "" { @@ -26908,7 +26921,6 @@ func (o *PromptNotificationOptions) ToMap() map[string]any { // PromptInputOptions carries optional parameters for PromptInput. type PromptInputOptions struct { Options *InteractionInputsDialogOptions `json:"options,omitempty"` - ValidationCallback func(arg InputsDialogValidationContext) `json:"-"` CancellationToken *CancellationToken `json:"-"` } @@ -26922,7 +26934,6 @@ func (o *PromptInputOptions) ToMap() map[string]any { // PromptInputsOptions carries optional parameters for PromptInputs. type PromptInputsOptions struct { Options *InteractionInputsDialogOptions `json:"options,omitempty"` - ValidationCallback func(arg InputsDialogValidationContext) `json:"-"` CancellationToken *CancellationToken `json:"-"` } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index ea9d2fb262a..b8c6b137788 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -3549,13 +3549,13 @@ public class CommandOptions implements JsonSerializable { private String description; private Object parameter; private InteractionInput[] arguments; - private Object validateArguments; + private AspireAction1 validateArguments; private ResourceCommandVisibility visibility; private String confirmationMessage; private String iconName; private IconVariant iconVariant; private boolean isHighlighted; - private Object updateState; + private AspireFunc1 updateState; public String getDescription() { return description; } public void setDescription(String value) { this.description = value; } @@ -3563,8 +3563,8 @@ public class CommandOptions implements JsonSerializable { public void setParameter(Object value) { this.parameter = value; } public InteractionInput[] getArguments() { return arguments; } public void setArguments(InteractionInput[] value) { this.arguments = value; } - public Object getValidateArguments() { return validateArguments; } - public void setValidateArguments(Object value) { this.validateArguments = value; } + public AspireAction1 getValidateArguments() { return validateArguments; } + public void setValidateArguments(AspireAction1 value) { this.validateArguments = value; } public ResourceCommandVisibility getVisibility() { return visibility; } public void setVisibility(ResourceCommandVisibility value) { this.visibility = value; } public String getConfirmationMessage() { return confirmationMessage; } @@ -3575,8 +3575,8 @@ public class CommandOptions implements JsonSerializable { public void setIconVariant(IconVariant value) { this.iconVariant = value; } public boolean getIsHighlighted() { return isHighlighted; } public void setIsHighlighted(boolean value) { this.isHighlighted = value; } - public Object getUpdateState() { return updateState; } - public void setUpdateState(Object value) { this.updateState = value; } + public AspireFunc1 getUpdateState() { return updateState; } + public void setUpdateState(AspireFunc1 value) { this.updateState = value; } @SuppressWarnings("unchecked") public static CommandOptions fromMap(Map map) { @@ -3587,8 +3587,6 @@ public static CommandOptions fromMap(Map map) { value.setParameter(parameterValue); var argumentsValue = map.get("Arguments"); value.setArguments((InteractionInput[]) argumentsValue); - var validateArgumentsValue = map.get("ValidateArguments"); - value.setValidateArguments(validateArgumentsValue); var visibilityValue = map.get("Visibility"); value.setVisibility(ResourceCommandVisibility.fromValue((String) visibilityValue)); var confirmationMessageValue = map.get("ConfirmationMessage"); @@ -3599,8 +3597,6 @@ public static CommandOptions fromMap(Map map) { value.setIconVariant(iconVariantValue == null ? null : IconVariant.fromValue((String) iconVariantValue)); var isHighlightedValue = map.get("IsHighlighted"); value.setIsHighlighted((Boolean) isHighlightedValue); - var updateStateValue = map.get("UpdateState"); - value.setUpdateState(updateStateValue); return value; } @@ -3609,13 +3605,20 @@ public Map toMap() { map.put("Description", AspireClient.serializeValue(description)); map.put("Parameter", AspireClient.serializeValue(parameter)); map.put("Arguments", AspireClient.serializeValue(arguments)); - map.put("ValidateArguments", AspireClient.serializeValue(validateArguments)); + map.put("ValidateArguments", validateArguments == null ? null : (java.util.function.Function) (transportArg -> { + var arg = (InputsDialogValidationContext) transportArg; + validateArguments.invoke(arg); + return null; + })); map.put("Visibility", AspireClient.serializeValue(visibility)); map.put("ConfirmationMessage", AspireClient.serializeValue(confirmationMessage)); map.put("IconName", AspireClient.serializeValue(iconName)); map.put("IconVariant", AspireClient.serializeValue(iconVariant)); map.put("IsHighlighted", AspireClient.serializeValue(isHighlighted)); - map.put("UpdateState", AspireClient.serializeValue(updateState)); + map.put("UpdateState", updateState == null ? null : (java.util.function.Function) (transportArg -> { + var arg = (UpdateCommandStateContext) transportArg; + return AspireClient.awaitValue(updateState.invoke(arg)); + })); return map; } } @@ -12529,7 +12532,7 @@ public class HttpCommandExportOptions implements JsonSerializable { private String commandName; private String endpointName; private String methodName; - private Object prepareRequest; + private AspireFunc1 prepareRequest; private HttpCommandResultMode resultMode; public CommandOptions getCommandOptions() { return commandOptions; } @@ -12550,8 +12553,8 @@ public class HttpCommandExportOptions implements JsonSerializable { public void setEndpointName(String value) { this.endpointName = value; } public String getMethodName() { return methodName; } public void setMethodName(String value) { this.methodName = value; } - public Object getPrepareRequest() { return prepareRequest; } - public void setPrepareRequest(Object value) { this.prepareRequest = value; } + public AspireFunc1 getPrepareRequest() { return prepareRequest; } + public void setPrepareRequest(AspireFunc1 value) { this.prepareRequest = value; } public HttpCommandResultMode getResultMode() { return resultMode; } public void setResultMode(HttpCommandResultMode value) { this.resultMode = value; } @@ -12576,8 +12579,6 @@ public static HttpCommandExportOptions fromMap(Map map) { value.setEndpointName(endpointNameValue == null ? null : (String) endpointNameValue); var methodNameValue = map.get("MethodName"); value.setMethodName(methodNameValue == null ? null : (String) methodNameValue); - var prepareRequestValue = map.get("PrepareRequest"); - value.setPrepareRequest(prepareRequestValue); var resultModeValue = map.get("ResultMode"); value.setResultMode(HttpCommandResultMode.fromValue((String) resultModeValue)); return value; @@ -12594,7 +12595,10 @@ public Map toMap() { map.put("CommandName", AspireClient.serializeValue(commandName)); map.put("EndpointName", AspireClient.serializeValue(endpointName)); map.put("MethodName", AspireClient.serializeValue(methodName)); - map.put("PrepareRequest", AspireClient.serializeValue(prepareRequest)); + map.put("PrepareRequest", prepareRequest == null ? null : (java.util.function.Function) (transportArg -> { + var arg = (HttpCommandPrepareRequestContext) transportArg; + return AspireClient.awaitValue(prepareRequest.invoke(arg)); + })); map.put("ResultMode", AspireClient.serializeValue(resultMode)); return map; } @@ -14058,9 +14062,8 @@ private BoolInteractionResult promptNotificationImpl(String title, String messag /** Prompts the user for a single input. */ public InputInteractionResult promptInput(String title, String message, InteractionInputBuilder input, PromptInputOptions optionsBag) { var options = optionsBag == null ? null : optionsBag.getOptions(); - var validationCallback = optionsBag == null ? null : optionsBag.getValidationCallback(); var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); - return promptInputImpl(title, message, input, options, validationCallback, cancellationToken); + return promptInputImpl(title, message, input, options, cancellationToken); } public InputInteractionResult promptInput(String title, String message, HandleWrapperBase input, PromptInputOptions options) { @@ -14076,7 +14079,7 @@ public InputInteractionResult promptInput(String title, String message, HandleWr } /** Prompts the user for a single input. */ - private InputInteractionResult promptInputImpl(String title, String message, InteractionInputBuilder input, InteractionInputsDialogOptions options, AspireAction1 validationCallback, CancellationToken cancellationToken) { + private InputInteractionResult promptInputImpl(String title, String message, InteractionInputBuilder input, InteractionInputsDialogOptions options, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); reqArgs.put("title", AspireClient.serializeValue(title)); @@ -14085,14 +14088,6 @@ private InputInteractionResult promptInputImpl(String title, String message, Int if (options != null) { reqArgs.put("options", AspireClient.serializeValue(options)); } - var validationCallbackId = validationCallback == null ? null : getClient().registerCallback(args -> { - var arg = (InputsDialogValidationContext) args[0]; - validationCallback.invoke(arg); - return null; - }); - if (validationCallbackId != null) { - reqArgs.put("validationCallback", validationCallbackId); - } if (cancellationToken != null) { reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); } @@ -14103,9 +14098,8 @@ private InputInteractionResult promptInputImpl(String title, String message, Int /** Prompts the user for multiple inputs. */ public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs, PromptInputsOptions optionsBag) { var options = optionsBag == null ? null : optionsBag.getOptions(); - var validationCallback = optionsBag == null ? null : optionsBag.getValidationCallback(); var cancellationToken = optionsBag == null ? null : optionsBag.getCancellationToken(); - return promptInputsImpl(title, message, inputs, options, validationCallback, cancellationToken); + return promptInputsImpl(title, message, inputs, options, cancellationToken); } public InputsInteractionResult promptInputs(String title, String message, InteractionInputBuilder[] inputs) { @@ -14113,7 +14107,7 @@ public InputsInteractionResult promptInputs(String title, String message, Intera } /** Prompts the user for multiple inputs. */ - private InputsInteractionResult promptInputsImpl(String title, String message, InteractionInputBuilder[] inputs, InteractionInputsDialogOptions options, AspireAction1 validationCallback, CancellationToken cancellationToken) { + private InputsInteractionResult promptInputsImpl(String title, String message, InteractionInputBuilder[] inputs, InteractionInputsDialogOptions options, CancellationToken cancellationToken) { Map reqArgs = new HashMap<>(); reqArgs.put("interactionService", AspireClient.serializeValue(getHandle())); reqArgs.put("title", AspireClient.serializeValue(title)); @@ -14122,14 +14116,6 @@ private InputsInteractionResult promptInputsImpl(String title, String message, I if (options != null) { reqArgs.put("options", AspireClient.serializeValue(options)); } - var validationCallbackId = validationCallback == null ? null : getClient().registerCallback(args -> { - var arg = (InputsDialogValidationContext) args[0]; - validationCallback.invoke(arg); - return null; - }); - if (validationCallbackId != null) { - reqArgs.put("validationCallback", validationCallbackId); - } if (cancellationToken != null) { reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); } @@ -15393,6 +15379,7 @@ public class InteractionInputsDialogOptions implements JsonSerializable { private Boolean showSecondaryButton; private Boolean showDismiss; private Boolean enableMessageMarkdown; + private AspireAction1 validationCallback; public String getPrimaryButtonText() { return primaryButtonText; } public void setPrimaryButtonText(String value) { this.primaryButtonText = value; } @@ -15404,6 +15391,8 @@ public class InteractionInputsDialogOptions implements JsonSerializable { public void setShowDismiss(Boolean value) { this.showDismiss = value; } public Boolean getEnableMessageMarkdown() { return enableMessageMarkdown; } public void setEnableMessageMarkdown(Boolean value) { this.enableMessageMarkdown = value; } + public AspireAction1 getValidationCallback() { return validationCallback; } + public void setValidationCallback(AspireAction1 value) { this.validationCallback = value; } @SuppressWarnings("unchecked") public static InteractionInputsDialogOptions fromMap(Map map) { @@ -15428,6 +15417,11 @@ public Map toMap() { map.put("ShowSecondaryButton", AspireClient.serializeValue(showSecondaryButton)); map.put("ShowDismiss", AspireClient.serializeValue(showDismiss)); map.put("EnableMessageMarkdown", AspireClient.serializeValue(enableMessageMarkdown)); + map.put("ValidationCallback", validationCallback == null ? null : (java.util.function.Function) (transportArg -> { + var arg = (InputsDialogValidationContext) transportArg; + validationCallback.invoke(arg); + return null; + })); return map; } } @@ -17014,7 +17008,7 @@ public class ProcessCommandExportOptions implements JsonSerializable { private Boolean inheritEnvironmentVariables; private String standardInputContent; private Boolean killEntireProcessTree; - private Object createProcessSpec; + private AspireFunc1 createProcessSpec; private CommandOptions commandOptions; private Double maxOutputLineCount; private Boolean displayImmediately; @@ -17034,8 +17028,8 @@ public class ProcessCommandExportOptions implements JsonSerializable { public void setStandardInputContent(String value) { this.standardInputContent = value; } public Boolean getKillEntireProcessTree() { return killEntireProcessTree; } public void setKillEntireProcessTree(Boolean value) { this.killEntireProcessTree = value; } - public Object getCreateProcessSpec() { return createProcessSpec; } - public void setCreateProcessSpec(Object value) { this.createProcessSpec = value; } + public AspireFunc1 getCreateProcessSpec() { return createProcessSpec; } + public void setCreateProcessSpec(AspireFunc1 value) { this.createProcessSpec = value; } public CommandOptions getCommandOptions() { return commandOptions; } public void setCommandOptions(CommandOptions value) { this.commandOptions = value; } public Double getMaxOutputLineCount() { return maxOutputLineCount; } @@ -17062,8 +17056,6 @@ public static ProcessCommandExportOptions fromMap(Map map) { value.setStandardInputContent(standardInputContentValue == null ? null : (String) standardInputContentValue); var killEntireProcessTreeValue = map.get("KillEntireProcessTree"); value.setKillEntireProcessTree(killEntireProcessTreeValue == null ? null : (Boolean) killEntireProcessTreeValue); - var createProcessSpecValue = map.get("CreateProcessSpec"); - value.setCreateProcessSpec(createProcessSpecValue); var commandOptionsValue = map.get("CommandOptions"); value.setCommandOptions(commandOptionsValue == null ? null : CommandOptions.fromMap((Map) commandOptionsValue)); var maxOutputLineCountValue = map.get("MaxOutputLineCount"); @@ -17084,7 +17076,10 @@ public Map toMap() { map.put("InheritEnvironmentVariables", AspireClient.serializeValue(inheritEnvironmentVariables)); map.put("StandardInputContent", AspireClient.serializeValue(standardInputContent)); map.put("KillEntireProcessTree", AspireClient.serializeValue(killEntireProcessTree)); - map.put("CreateProcessSpec", AspireClient.serializeValue(createProcessSpec)); + map.put("CreateProcessSpec", createProcessSpec == null ? null : (java.util.function.Function) (transportArg -> { + var arg = (ExecuteCommandContext) transportArg; + return AspireClient.awaitValue(createProcessSpec.invoke(arg)); + })); map.put("CommandOptions", AspireClient.serializeValue(commandOptions)); map.put("MaxOutputLineCount", AspireClient.serializeValue(maxOutputLineCount)); map.put("DisplayImmediately", AspireClient.serializeValue(displayImmediately)); @@ -18894,7 +18889,6 @@ public PromptConfirmationOptions cancellationToken(CancellationToken value) { /** Options for PromptInput. */ public final class PromptInputOptions { private InteractionInputsDialogOptions options; - private AspireAction1 validationCallback; private CancellationToken cancellationToken; public InteractionInputsDialogOptions getOptions() { return options; } @@ -18903,12 +18897,6 @@ public PromptInputOptions options(InteractionInputsDialogOptions value) { return this; } - public AspireAction1 getValidationCallback() { return validationCallback; } - public PromptInputOptions validationCallback(AspireAction1 value) { - this.validationCallback = value; - return this; - } - public CancellationToken getCancellationToken() { return cancellationToken; } public PromptInputOptions cancellationToken(CancellationToken value) { this.cancellationToken = value; @@ -18928,7 +18916,6 @@ public PromptInputOptions cancellationToken(CancellationToken value) { /** Options for PromptInputs. */ public final class PromptInputsOptions { private InteractionInputsDialogOptions options; - private AspireAction1 validationCallback; private CancellationToken cancellationToken; public InteractionInputsDialogOptions getOptions() { return options; } @@ -18937,12 +18924,6 @@ public PromptInputsOptions options(InteractionInputsDialogOptions value) { return this; } - public AspireAction1 getValidationCallback() { return validationCallback; } - public PromptInputsOptions validationCallback(AspireAction1 value) { - this.validationCallback = value; - return this; - } - public CancellationToken getCancellationToken() { return cancellationToken; } public PromptInputsOptions cancellationToken(CancellationToken value) { this.cancellationToken = value; diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index ec3ca398c30..1f621ab737d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1764,13 +1764,13 @@ class CommandOptions(typing.TypedDict, total=False): Description: str | None Parameter: typing.Any Arguments: typing.Iterable[InteractionInput] - ValidateArguments: typing.Callable + ValidateArguments: typing.Callable[[InputsDialogValidationContext], None] Visibility: ResourceCommandVisibility ConfirmationMessage: str | None IconName: str | None IconVariant: IconVariant | None IsHighlighted: bool - UpdateState: typing.Callable + UpdateState: typing.Callable[[UpdateCommandStateContext], ResourceCommandState] class CommandResultData(typing.TypedDict, total=False): Value: str @@ -1840,7 +1840,7 @@ class HttpCommandExportOptions(typing.TypedDict, total=False): CommandName: str | None EndpointName: str | None MethodName: str | None - PrepareRequest: typing.Callable + PrepareRequest: typing.Callable[[HttpCommandPrepareRequestContext], HttpCommandRequestExportData] ResultMode: HttpCommandResultMode class HttpCommandRequestExportData(typing.TypedDict, total=False): @@ -1900,6 +1900,7 @@ class InteractionInputsDialogOptions(typing.TypedDict, total=False): ShowSecondaryButton: bool | None ShowDismiss: bool | None EnableMessageMarkdown: bool | None + ValidationCallback: typing.Callable[[InputsDialogValidationContext], None] class InteractionMessageBoxOptions(typing.TypedDict, total=False): PrimaryButtonText: str | None @@ -1939,7 +1940,7 @@ class ProcessCommandExportOptions(typing.TypedDict, total=False): InheritEnvironmentVariables: bool | None StandardInputContent: str | None KillEntireProcessTree: bool | None - CreateProcessSpec: typing.Callable + CreateProcessSpec: typing.Callable[[ExecuteCommandContext], ProcessCommandSpecExportData] CommandOptions: CommandOptions MaxOutputLineCount: int | None DisplayImmediately: bool | None @@ -2978,7 +2979,7 @@ def prompt_notification(self, title: str, message: str, *, options: InteractionN ) return typing.cast(BoolInteractionResult, result) - def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, *, options: InteractionInputsDialogOptions | None = None, validation_callback: typing.Callable[[InputsDialogValidationContext], None] | None = None, timeout: int | None = None) -> InputInteractionResult: + def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, *, options: InteractionInputsDialogOptions | None = None, timeout: int | None = None) -> InputInteractionResult: """Prompts the user for a single input.""" rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} rpc_args['title'] = title @@ -2986,8 +2987,6 @@ def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, rpc_args['input'] = input if options is not None: rpc_args['options'] = options - if validation_callback is not None: - rpc_args['validationCallback'] = self._client.register_callback(validation_callback) if timeout is not None: rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) result = self._client.invoke_capability( @@ -2996,7 +2995,7 @@ def prompt_input(self, title: str, message: str, input: InteractionInputBuilder, ) return typing.cast(InputInteractionResult, result) - def prompt_inputs(self, title: str, message: str, inputs: typing.Iterable[InteractionInputBuilder], *, options: InteractionInputsDialogOptions | None = None, validation_callback: typing.Callable[[InputsDialogValidationContext], None] | None = None, timeout: int | None = None) -> InputsInteractionResult: + def prompt_inputs(self, title: str, message: str, inputs: typing.Iterable[InteractionInputBuilder], *, options: InteractionInputsDialogOptions | None = None, timeout: int | None = None) -> InputsInteractionResult: """Prompts the user for multiple inputs.""" rpc_args: dict[str, typing.Any] = {'interactionService': self._handle} rpc_args['title'] = title @@ -3004,8 +3003,6 @@ def prompt_inputs(self, title: str, message: str, inputs: typing.Iterable[Intera rpc_args['inputs'] = inputs if options is not None: rpc_args['options'] = options - if validation_callback is not None: - rpc_args['validationCallback'] = self._client.register_callback(validation_callback) if timeout is not None: rpc_args['cancellationToken'] = self._client.register_cancellation_token(timeout) result = self._client.invoke_capability( diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 6b8c957d8d8..37e196a780d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1031,6 +1031,8 @@ pub struct InteractionInputsDialogOptions { pub show_dismiss: Option, #[serde(rename = "EnableMessageMarkdown", skip_serializing_if = "Option::is_none")] pub enable_message_markdown: Option, + #[serde(rename = "ValidationCallback", skip_serializing_if = "Option::is_none")] + pub validation_callback: Option, } impl InteractionInputsDialogOptions { @@ -1051,6 +1053,9 @@ impl InteractionInputsDialogOptions { if let Some(ref v) = self.enable_message_markdown { map.insert("EnableMessageMarkdown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } + if let Some(ref v) = self.validation_callback { + map.insert("ValidationCallback".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } map } } @@ -11174,7 +11179,7 @@ impl IInteractionService { } /// Prompts the user for a single input. - pub fn prompt_input(&self, title: &str, message: &str, input: &InteractionInputBuilder, options: Option, validation_callback: impl Fn(Vec) -> Value + Send + Sync + 'static, cancellation_token: Option<&CancellationToken>) -> Result> { + pub fn prompt_input(&self, title: &str, message: &str, input: &InteractionInputBuilder, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("interactionService".to_string(), self.handle.to_json()); args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); @@ -11183,8 +11188,6 @@ impl IInteractionService { if let Some(ref v) = options { args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let callback_id = register_callback(validation_callback); - args.insert("validationCallback".to_string(), Value::String(callback_id)); if let Some(token) = cancellation_token { let token_id = register_cancellation(token, self.client.clone()); args.insert("cancellationToken".to_string(), Value::String(token_id)); @@ -11194,7 +11197,7 @@ impl IInteractionService { } /// Prompts the user for multiple inputs. - pub fn prompt_inputs(&self, title: &str, message: &str, inputs: Vec, options: Option, validation_callback: impl Fn(Vec) -> Value + Send + Sync + 'static, cancellation_token: Option<&CancellationToken>) -> Result> { + pub fn prompt_inputs(&self, title: &str, message: &str, inputs: Vec, options: Option, cancellation_token: Option<&CancellationToken>) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("interactionService".to_string(), self.handle.to_json()); args.insert("title".to_string(), serde_json::to_value(&title).unwrap_or(Value::Null)); @@ -11204,8 +11207,6 @@ impl IInteractionService { if let Some(ref v) = options { args.insert("options".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let callback_id = register_callback(validation_callback); - args.insert("validationCallback".to_string(), Value::String(callback_id)); if let Some(token) = cancellation_token { let token_id = register_cancellation(token, self.client.clone()); args.insert("cancellationToken".to_string(), Value::String(token_id)); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index d2e4bb7a2f9..6624b52cb0f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -1009,6 +1009,8 @@ export interface InteractionInputsDialogOptions { showDismiss?: boolean | null; /** Gets or sets a value indicating whether Markdown in the message is rendered. */ enableMessageMarkdown?: boolean | null; + /** Gets or sets a callback invoked to validate the inputs before the dialog is accepted. The callback receives a validation context that exposes the current inputs and can record validation errors. */ + validationCallback?: (arg: InputsDialogValidationContext) => Promise; } /** Options for message box and confirmation prompts. */ @@ -1471,13 +1473,11 @@ export interface PromptConfirmationOptions { export interface PromptInputOptions { options?: InteractionInputsDialogOptions; - validationCallback?: (arg: InputsDialogValidationContext) => Promise; cancellationToken?: AbortSignal | CancellationToken; } export interface PromptInputsOptions { options?: InteractionInputsDialogOptions; - validationCallback?: (arg: InputsDialogValidationContext) => Promise; cancellationToken?: AbortSignal | CancellationToken; } @@ -11434,17 +11434,23 @@ class InteractionServiceImpl implements InteractionService { */ async promptInput(title: string, message: string, input: Awaitable, optionsBag?: PromptInputOptions): Promise { const options = optionsBag?.options; - const validationCallback = optionsBag?.validationCallback; const cancellationToken = optionsBag?.cancellationToken; - const validationCallbackId = validationCallback ? registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as InputsDialogValidationContextHandle; - const arg = new InputsDialogValidationContextImpl(argHandle, this._client); - await validationCallback(arg); - }) : undefined; input = isPromiseLike(input) ? await input : input; + const __optionsForRpc = options === undefined || options === null ? options : { ...options }; + if (__optionsForRpc !== undefined && __optionsForRpc !== null) { + const __optionsForRpcData = __optionsForRpc as Record; + const ____optionsForRpcValidationCallback = __optionsForRpc.validationCallback; + if (____optionsForRpcValidationCallback !== undefined) { + const ____optionsForRpcValidationCallbackId = ____optionsForRpcValidationCallback ? registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as InputsDialogValidationContextHandle; + const arg = new InputsDialogValidationContextImpl(argHandle, this._client); + await ____optionsForRpcValidationCallback(arg); + }) : undefined; + __optionsForRpcData["validationCallback"] = ____optionsForRpcValidationCallbackId; + } + } const rpcArgs: Record = { interactionService: this._handle, title, message, input }; - if (options !== undefined) rpcArgs.options = options; - if (validationCallback !== undefined) rpcArgs.validationCallback = validationCallbackId; + if (options !== undefined) rpcArgs.options = __optionsForRpc; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); return await this._client.invokeCapability( 'Aspire.Hosting/promptInput', @@ -11458,16 +11464,22 @@ class InteractionServiceImpl implements InteractionService { */ async promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], optionsBag?: PromptInputsOptions): Promise { const options = optionsBag?.options; - const validationCallback = optionsBag?.validationCallback; const cancellationToken = optionsBag?.cancellationToken; - const validationCallbackId = validationCallback ? registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as InputsDialogValidationContextHandle; - const arg = new InputsDialogValidationContextImpl(argHandle, this._client); - await validationCallback(arg); - }) : undefined; + const __optionsForRpc = options === undefined || options === null ? options : { ...options }; + if (__optionsForRpc !== undefined && __optionsForRpc !== null) { + const __optionsForRpcData = __optionsForRpc as Record; + const ____optionsForRpcValidationCallback = __optionsForRpc.validationCallback; + if (____optionsForRpcValidationCallback !== undefined) { + const ____optionsForRpcValidationCallbackId = ____optionsForRpcValidationCallback ? registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as InputsDialogValidationContextHandle; + const arg = new InputsDialogValidationContextImpl(argHandle, this._client); + await ____optionsForRpcValidationCallback(arg); + }) : undefined; + __optionsForRpcData["validationCallback"] = ____optionsForRpcValidationCallbackId; + } + } const rpcArgs: Record = { interactionService: this._handle, title, message, inputs }; - if (options !== undefined) rpcArgs.options = options; - if (validationCallback !== undefined) rpcArgs.validationCallback = validationCallbackId; + if (options !== undefined) rpcArgs.options = __optionsForRpc; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); return await this._client.invokeCapability( 'Aspire.Hosting/promptInputs', diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 9dc25a4fa6c..467e6c2e95b 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -724,14 +724,16 @@ ENTRYPOINT ["dotnet", "App.dll"] }) single, err := interactionService.PromptInput("Single input", "Enter a value.", interactionService.CreateTextInput("solo"), &aspire.PromptInputOptions{ - Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: aspire.StringPtr("Save")}, - ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { - inputs, _ := validationContext.Inputs().ToArray() - for _, input := range inputs { - if input.Name == "solo" && input.Value == "" { - _ = validationContext.AddValidationError("solo", "A value is required.") + Options: &aspire.InteractionInputsDialogOptions{ + PrimaryButtonText: aspire.StringPtr("Save"), + ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { + inputs, _ := validationContext.Inputs().ToArray() + for _, input := range inputs { + if input.Name == "solo" && input.Value == "" { + _ = validationContext.AddValidationError("solo", "A value is required.") + } } - } + }, }, }) if err != nil { @@ -741,14 +743,17 @@ ENTRYPOINT ["dotnet", "App.dll"] multi, err := interactionService.PromptInputs("Multiple inputs", "Fill out the form.", []aspire.InteractionInputBuilder{textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput}, &aspire.PromptInputsOptions{ - Options: &aspire.InteractionInputsDialogOptions{PrimaryButtonText: aspire.StringPtr("Submit"), EnableMessageMarkdown: aspire.BoolPtr(true)}, - ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { - inputs, _ := validationContext.Inputs().ToArray() - for _, input := range inputs { - if input.Name == "name" && input.Value == "bad" { - _ = validationContext.AddValidationError("name", "Name cannot be 'bad'.") + Options: &aspire.InteractionInputsDialogOptions{ + PrimaryButtonText: aspire.StringPtr("Submit"), + EnableMessageMarkdown: aspire.BoolPtr(true), + ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { + inputs, _ := validationContext.Inputs().ToArray() + for _, input := range inputs { + if input.Name == "name" && input.Value == "bad" { + _ = validationContext.AddValidationError("name", "Name cannot be 'bad'.") + } } - } + }, }, }) if err != nil { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index 672fb919872..1c6a0f58156 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -394,28 +394,30 @@ void main() throws Exception { var singleDialogOptions = new InteractionInputsDialogOptions(); singleDialogOptions.setPrimaryButtonText("Save"); + singleDialogOptions.setValidationCallback((validationContext) -> { + for (var input : validationContext.inputs().toArray()) { + if ("solo".equals(input.getName()) && (input.getValue() == null || input.getValue().isEmpty())) { + validationContext.addValidationError("solo", "A value is required."); + } + } + }); var single = interactionService.promptInput("Single input", "Enter a value.", interactionService.createTextInput("solo"), - new PromptInputOptions().options(singleDialogOptions).validationCallback((validationContext) -> { - for (var input : validationContext.inputs().toArray()) { - if ("solo".equals(input.getName()) && (input.getValue() == null || input.getValue().isEmpty())) { - validationContext.addValidationError("solo", "A value is required."); - } - } - })); + new PromptInputOptions().options(singleDialogOptions)); var multiDialogOptions = new InteractionInputsDialogOptions(); multiDialogOptions.setPrimaryButtonText("Submit"); multiDialogOptions.setEnableMessageMarkdown(true); + multiDialogOptions.setValidationCallback((validationContext) -> { + for (var input : validationContext.inputs().toArray()) { + if ("name".equals(input.getName()) && "bad".equals(input.getValue())) { + validationContext.addValidationError("name", "Name cannot be 'bad'."); + } + } + }); var multi = interactionService.promptInputs("Multiple inputs", "Fill out the form.", new InteractionInputBuilder[] { textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput }, - new PromptInputsOptions().options(multiDialogOptions).validationCallback((validationContext) -> { - for (var input : validationContext.inputs().toArray()) { - if ("name".equals(input.getName()) && "bad".equals(input.getValue())) { - validationContext.addValidationError("name", "Name cannot be 'bad'."); - } - } - })); + new PromptInputsOptions().options(multiDialogOptions)); String selectedColor = ""; if (multi.getInputs() != null) { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index 7e1760eaa17..e727a06c470 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -567,8 +567,7 @@ def validate_solo(validation_context): "Single input", "Enter a value.", interaction_service.create_text_input("solo"), - options={"PrimaryButtonText": "Save"}, - validation_callback=validate_solo, + options={"PrimaryButtonText": "Save", "ValidationCallback": validate_solo}, ) def validate_form(validation_context): @@ -581,8 +580,7 @@ def validate_form(validation_context): "Multiple inputs", "Fill out the form.", [text_input, secret_input, boolean_input, number_input, choice_input, preset_input, size_input, dependent_input], - options={"PrimaryButtonText": "Submit", "EnableMessageMarkdown": True}, - validation_callback=validate_form, + options={"PrimaryButtonText": "Submit", "EnableMessageMarkdown": True, "ValidationCallback": validate_form}, ) selected_color = next((i.get("Value") for i in (multi.get("Inputs") or []) if i.get("Name") == "color"), None) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index c3413544a8b..c6e072f52cc 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -869,12 +869,14 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn "Enter a value.", interactionService.createTextInput("solo"), { - options: { primaryButtonText: "Save" }, - validationCallback: async (validationContext) => { - const inputs = await (await validationContext.inputs()).toArray(); - const solo = inputs.find(input => input.name === "solo"); - if (!solo?.value) { - await validationContext.addValidationError("solo", "A value is required."); + options: { + primaryButtonText: "Save", + validationCallback: async (validationContext) => { + const inputs = await (await validationContext.inputs()).toArray(); + const solo = inputs.find(input => input.name === "solo"); + if (!solo?.value) { + await validationContext.addValidationError("solo", "A value is required."); + } } } }); @@ -884,12 +886,15 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn "Fill out the form.", [textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput], { - options: { primaryButtonText: "Submit", enableMessageMarkdown: true }, - validationCallback: async (validationContext) => { - const inputs = await (await validationContext.inputs()).toArray(); - const name = inputs.find(input => input.name === "name"); - if (name?.value === "bad") { - await validationContext.addValidationError("name", "Name cannot be 'bad'."); + options: { + primaryButtonText: "Submit", + enableMessageMarkdown: true, + validationCallback: async (validationContext) => { + const inputs = await (await validationContext.inputs()).toArray(); + const name = inputs.find(input => input.name === "name"); + if (name?.value === "bad") { + await validationContext.addValidationError("name", "Name cannot be 'bad'."); + } } } }); From dfe3dcbfb6b1d394deb546b8976465b7c2185d5d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 09:57:05 -0700 Subject: [PATCH 12/20] Update Go/Java benches for strongly-typed withCommand UpdateState callback Local `aspire restore` + compile (go build / javac) validation surfaced that the withCommand UpdateState DTO callback now renders strongly typed in Go and Java (a side effect of emitting strong DTO callbacks), so the benches' weak forms no longer compiled. - Go: updateCommandState is now func(ctx UpdateCommandStateContext) ResourceCommandState (drops the manual args[0] type assertion since ctx is strongly typed). - Java: setUpdateState takes a bare lambda matching AspireFunc1 instead of a java.util.function.Function cast. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go | 9 +-------- tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 467e6c2e95b..1d860942fbe 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -543,14 +543,7 @@ ENTRYPOINT ["dotnet", "App.dll"] _ = container.WithHealthCheck("custom_check") _ = container.WithHttpHealthCheck() _ = container.WithHttpHealthCheck() - updateCommandState := func(args ...any) any { - if len(args) == 0 { - return aspire.ResourceCommandStateDisabled - } - ctx, ok := args[0].(aspire.UpdateCommandStateContext) - if !ok { - return aspire.ResourceCommandStateDisabled - } + updateCommandState := func(ctx aspire.UpdateCommandStateContext) aspire.ResourceCommandState { snapshot, err := ctx.ResourceSnapshot() if err != nil || snapshot.HealthStatus == nil { return aspire.ResourceCommandStateDisabled diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index 1c6a0f58156..c9c1461bee6 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -246,7 +246,7 @@ void main() throws Exception { container.withHttpHealthCheck(); container.withHttpHealthCheck(); var commandOptions = new CommandOptions(); - commandOptions.setUpdateState((Function) (ctx) -> { + commandOptions.setUpdateState((ctx) -> { var snapshot = ctx.resourceSnapshot(); return snapshot.getHealthStatus() == HealthStatus.HEALTHY ? ResourceCommandState.ENABLED : ResourceCommandState.DISABLED; }); From 92f2944b8b7c747aa53b199ddb3d6083660d0fc6 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 10:33:58 -0700 Subject: [PATCH 13/20] Use existing by-name input accessor in TS bench; add prompt round-trip test Address JamesNK's PR feedback on clunky by-name input lookups. The TypeScript InteractionInputCollection already ships client-side by-name accessors (get/value/required/requiredValue), so the showcase bench now uses value(name) in validation callbacks and the echo command instead of toArray().find(). Add a round-trip test proving PromptInputs returns submitted values keyed by input name, documenting that polyglot callers receive values via the result rather than by mutating the server-side input handles they pass in. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Aspire.Hosting.RemoteHost.Tests.csproj | 4 ++ .../AtsExportsTests.cs | 42 +++++++++++++++++++ .../Aspire.Hosting/TypeScript/apphost.mts | 14 +++---- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj b/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj index da1fe269407..d8eaeba348c 100644 --- a/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj +++ b/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj @@ -5,6 +5,10 @@ net10.0 + + + + diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs index 34aa103945e..d9f2e2dd225 100644 --- a/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs +++ b/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Aspire.Hosting.Ats; +using Aspire.Hosting.Tests; using Xunit; namespace Aspire.Hosting.RemoteHost.Tests; @@ -70,6 +71,47 @@ public void ParseLogLevel_StrictModeThrowsForUnknownLevel() Assert.Equal("level", exception.ParamName); } + // Polyglot callers cannot mutate the input handles they pass to PromptInputs (those handles live on the + // server and only the data fields cross the wire), so submitted values must travel back through the result. + // This round-trips a prompt to prove the result carries the values keyed by input name. +#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + [Fact] + public async Task PromptInputs_ResultCarriesSubmittedValuesByName() + { + var interactionService = new TestInteractionService(); + + var promptTask = InteractionExports.PromptInputs( + interactionService, + "Configure", + "Fill out the form.", + [ + InteractionExports.CreateTextInput(interactionService, "region"), + InteractionExports.CreateTextInput(interactionService, "zone"), + ]); + + var data = await interactionService.Interactions.Reader.ReadAsync(); + data.Inputs["region"].Value = "westus"; + data.Inputs["zone"].Value = "a"; + data.CompletionTcs.SetResult(InteractionResult.Ok(data.Inputs)); + + var result = await promptTask; + + Assert.False(result.Canceled); + Assert.Collection( + result.Inputs, + input => + { + Assert.Equal("region", input.Name); + Assert.Equal("westus", input.Value); + }, + input => + { + Assert.Equal("zone", input.Name); + Assert.Equal("a", input.Value); + }); + } +#pragma warning restore ASPIREINTERACTION001 + private sealed class TestHostEnvironment : IHostEnvironment { public string EnvironmentName { get; set; } = Environments.Production; diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index c6e072f52cc..8f3a90a2a86 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -738,9 +738,9 @@ await container.withCommand("noop", "Noop", async () => { }); await container.withCommand("echo", "Echo", async (ctx) => { const commandInputs = await ctx.arguments(); - const commandArguments = await commandInputs.toArray(); + const message = await commandInputs.value("message"); - return { success: commandArguments[0]?.value === "hello" }; + return { success: message === "hello" }; }, { commandOptions: { arguments: [ @@ -872,9 +872,8 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn options: { primaryButtonText: "Save", validationCallback: async (validationContext) => { - const inputs = await (await validationContext.inputs()).toArray(); - const solo = inputs.find(input => input.name === "solo"); - if (!solo?.value) { + const solo = await (await validationContext.inputs()).value("solo"); + if (!solo) { await validationContext.addValidationError("solo", "A value is required."); } } @@ -890,9 +889,8 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn primaryButtonText: "Submit", enableMessageMarkdown: true, validationCallback: async (validationContext) => { - const inputs = await (await validationContext.inputs()).toArray(); - const name = inputs.find(input => input.name === "name"); - if (name?.value === "bad") { + const name = await (await validationContext.inputs()).value("name"); + if (name === "bad") { await validationContext.addValidationError("name", "Name cannot be 'bad'."); } } From 0c5539d4242ff5d1bc42f5f8db68314e452b75c0 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 11:08:42 -0700 Subject: [PATCH 14/20] Add by-name input accessors to Go/Java/Python codegen for parity with TS Bring the Go, Java, and Python polyglot code generators to parity with TypeScript by injecting hand-authored by-name accessor methods (get/required/value/requiredValue) onto the auto-generated InteractionInputCollection wrapper. These delegate to the generated toArray capability and match names case-insensitively, mirroring the .NET InteractionInputCollection indexer and the TS get/value helpers. Update the Go/Java/Python test-bench apphosts to use value("name") instead of toArray() loops, and regenerate the affected snapshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsGoCodeGenerator.cs | 64 +++++++++++++++++++ .../AtsJavaCodeGenerator.cs | 45 +++++++++++++ .../AtsPythonCodeGenerator.cs | 43 +++++++++++++ .../Snapshots/AtsGeneratedAspire.verified.go | 2 + ...TwoPassScanningGeneratedAspire.verified.go | 40 ++++++++++++ ...oPassScanningGeneratedAspire.verified.java | 30 +++++++++ ...TwoPassScanningGeneratedAspire.verified.py | 27 ++++++++ .../Aspire.Hosting/Go/apphost.go | 20 +++--- .../Aspire.Hosting/Java/AppHost.java | 15 ++--- .../Aspire.Hosting/Python/apphost.py | 11 +--- 10 files changed, 267 insertions(+), 30 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs index ef2d29d712f..2e95a2c2da7 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs @@ -457,6 +457,7 @@ package aspire "context" "fmt" "os" + "strings" "time" ) @@ -464,6 +465,7 @@ package aspire var _ = context.Background var _ = fmt.Errorf var _ = os.Getenv + var _ = strings.EqualFold var _ = time.Second """); WriteLine(); @@ -824,6 +826,11 @@ private void GenerateConcreteHandleTypes(Dictionary _capabilityOptionsClassMap = new(StringComparer.Ordinal); private readonly HashSet _resourceBuilderHandleClasses = new(StringComparer.Ordinal); + private const string InteractionInputCollectionTypeId = "Aspire.Hosting/Aspire.Hosting.InteractionInputCollection"; + /// public string Language => "Java"; @@ -1062,6 +1064,11 @@ private void GenerateHandleTypes( } } + if (string.Equals(handleType.TypeId, InteractionInputCollectionTypeId, StringComparison.Ordinal)) + { + GenerateInteractionInputCollectionAccessors(); + } + if (string.Equals(handleType.ClassName, "DistributedApplication", StringComparison.Ordinal)) { GenerateDistributedApplicationBuilderHelpers(); @@ -1072,6 +1079,44 @@ private void GenerateHandleTypes( } } + private void GenerateInteractionInputCollectionAccessors() + { + // These accessors are hand-authored on top of the generated toArray capability for parity with .NET and TypeScript. + WriteLine(" /** Gets the input with the specified name, or null if no input matches. */"); + WriteLine(" public InteractionInput get(String name) {"); + WriteLine(" for (var input : toArray()) {"); + WriteLine(" if (input.getName() != null && input.getName().equalsIgnoreCase(name)) {"); + WriteLine(" return input;"); + WriteLine(" }"); + WriteLine(" }"); + WriteLine(" return null;"); + WriteLine(" }"); + WriteLine(); + + WriteLine(" /** Gets the input with the specified name, or throws if no input matches. */"); + WriteLine(" public InteractionInput required(String name) {"); + WriteLine(" var input = get(name);"); + WriteLine(" if (input == null) {"); + WriteLine(" throw new IllegalArgumentException(\"no input with name '\" + name + \"' was found\");"); + WriteLine(" }"); + WriteLine(" return input;"); + WriteLine(" }"); + WriteLine(); + + WriteLine(" /** Gets the value of the input with the specified name, or an empty string if no input matches or it has no value. */"); + WriteLine(" public String value(String name) {"); + WriteLine(" var input = get(name);"); + WriteLine(" return input == null || input.getValue() == null ? \"\" : input.getValue();"); + WriteLine(" }"); + WriteLine(); + + WriteLine(" /** Gets the value of the input with the specified name, or throws if no input matches. */"); + WriteLine(" public String requiredValue(String name) {"); + WriteLine(" return required(name).getValue();"); + WriteLine(" }"); + WriteLine(); + } + private void GenerateDistributedApplicationBuilderHelpers() { var builderClassName = _classNames.TryGetValue(AtsConstants.BuilderTypeId, out var name) diff --git a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs index 6d7479ee062..56c9b16c682 100644 --- a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs @@ -125,6 +125,10 @@ private sealed record OptionVariation( private sealed record MergedCapabilityDispatch(string AlternateCapabilityId, string DiscriminatingParamName); private readonly Dictionary _mergedCapabilityDispatches = new(StringComparer.Ordinal); + // Type ID of InteractionInputCollection. The by-name accessors below are hand-authored on top of the + // generated to_array capability so Python matches the .NET indexer and TypeScript get/value helpers. + private const string InteractionInputCollectionTypeId = "Aspire.Hosting/Aspire.Hosting.InteractionInputCollection"; + private PythonModuleBuilder _moduleBuilder = null!; // Mapping of typeId -> wrapper class name for all generated wrapper types @@ -1061,6 +1065,45 @@ private void GenerateTypeClass(BuilderModel model) { GenerateTypeClassMethod(sb, method); } + + if (string.Equals(model.TypeId, InteractionInputCollectionTypeId, StringComparison.Ordinal)) + { + EmitInteractionInputCollectionAccessors(sb); + } + } + + private static void EmitInteractionInputCollectionAccessors(System.Text.StringBuilder sb) + { + sb.AppendLine(" def get(self, name: str) -> InteractionInput | None:"); + sb.AppendLine(" \"\"\"Get the input with the specified name, or None if no input matches.\"\"\""); + sb.AppendLine(" lookup_name = name.lower()"); + sb.AppendLine(" for interaction_input in self.to_array():"); + sb.AppendLine(" input_name = interaction_input.get(\"Name\")"); + sb.AppendLine(" if input_name is not None and input_name.lower() == lookup_name:"); + sb.AppendLine(" return interaction_input"); + sb.AppendLine(" return None"); + sb.AppendLine(); + + sb.AppendLine(" def required(self, name: str) -> InteractionInput:"); + sb.AppendLine(" \"\"\"Get the input with the specified name, or raise ValueError if no input matches.\"\"\""); + sb.AppendLine(" interaction_input = self.get(name)"); + sb.AppendLine(" if interaction_input is None:"); + sb.AppendLine(" raise ValueError(f\"no input with name '{name}' was found\")"); + sb.AppendLine(" return interaction_input"); + sb.AppendLine(); + + sb.AppendLine(" def value(self, name: str) -> str:"); + sb.AppendLine(" \"\"\"Get the input value with the specified name, or an empty string if no input matches.\"\"\""); + sb.AppendLine(" interaction_input = self.get(name)"); + sb.AppendLine(" if interaction_input is None:"); + sb.AppendLine(" return \"\""); + sb.AppendLine(" return interaction_input.get(\"Value\") or \"\""); + sb.AppendLine(); + + sb.AppendLine(" def required_value(self, name: str) -> str:"); + sb.AppendLine(" \"\"\"Get the input value with the specified name, or raise ValueError if no input matches.\"\"\""); + sb.AppendLine(" return self.required(name).get(\"Value\") or \"\""); + sb.AppendLine(); } /// diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go index 31853835349..54ea3548b95 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go @@ -10,6 +10,7 @@ import ( "context" "fmt" "os" + "strings" "time" ) @@ -17,6 +18,7 @@ import ( var _ = context.Background var _ = fmt.Errorf var _ = os.Getenv +var _ = strings.EqualFold var _ = time.Second // ============================================================================ diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 932432d017d..de1f35ae62f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -10,6 +10,7 @@ import ( "context" "fmt" "os" + "strings" "time" ) @@ -17,6 +18,7 @@ import ( var _ = context.Background var _ = fmt.Errorf var _ = os.Getenv +var _ = strings.EqualFold var _ = time.Second // ============================================================================ @@ -16247,6 +16249,10 @@ func (s *interactionInputBuilder) WithValue(value string) InteractionInputBuilde type InteractionInputCollection interface { handleReference ToArray() ([]*InteractionInput, error) + Get(name string) (*InteractionInput, error) + Required(name string) (*InteractionInput, error) + Value(name string) (string, error) + RequiredValue(name string) (string, error) Err() error } @@ -16275,6 +16281,40 @@ func (s *interactionInputCollection) ToArray() ([]*InteractionInput, error) { return decodeAs[[]*InteractionInput](result) } +// Get returns the input with the specified name, or nil if no input matches. +func (s *interactionInputCollection) Get(name string) (*InteractionInput, error) { + if s.err != nil { return nil, s.err } + inputs, err := s.ToArray() + if err != nil { return nil, err } + for _, input := range inputs { + if strings.EqualFold(input.Name, name) { return input, nil } + } + return nil, nil +} + +// Required returns the input with the specified name, or an error if no input matches. +func (s *interactionInputCollection) Required(name string) (*InteractionInput, error) { + input, err := s.Get(name) + if err != nil { return nil, err } + if input == nil { return nil, fmt.Errorf("no input with name '%s' was found", name) } + return input, nil +} + +// Value returns the value of the input with the specified name, or an empty string if no input matches or it has no value. +func (s *interactionInputCollection) Value(name string) (string, error) { + input, err := s.Get(name) + if err != nil { return "", err } + if input == nil { return "", nil } + return input.Value, nil +} + +// RequiredValue returns the value of the input with the specified name, or an error if no input matches. +func (s *interactionInputCollection) RequiredValue(name string) (string, error) { + input, err := s.Required(name) + if err != nil { return "", err } + return input.Value, nil +} + // InteractionInputLoadContext is the public interface for handle type InteractionInputLoadContext. type InteractionInputLoadContext interface { handleReference diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index b8c6b137788..0cc921b3b45 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -15313,6 +15313,36 @@ public InteractionInput[] toArray() { return (InteractionInput[]) result; } + /** Gets the input with the specified name, or null if no input matches. */ + public InteractionInput get(String name) { + for (var input : toArray()) { + if (input.getName() != null && input.getName().equalsIgnoreCase(name)) { + return input; + } + } + return null; + } + + /** Gets the input with the specified name, or throws if no input matches. */ + public InteractionInput required(String name) { + var input = get(name); + if (input == null) { + throw new IllegalArgumentException("no input with name '" + name + "' was found"); + } + return input; + } + + /** Gets the value of the input with the specified name, or an empty string if no input matches or it has no value. */ + public String value(String name) { + var input = get(name); + return input == null || input.getValue() == null ? "" : input.getValue(); + } + + /** Gets the value of the input with the specified name, or throws if no input matches. */ + public String requiredValue(String name) { + return required(name).getValue(); + } + } // ===== InteractionInputLoadContext.java ===== diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 1f621ab737d..425e4a5ae99 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -5294,6 +5294,33 @@ def to_array(self) -> typing.Iterable[InteractionInput]: ) return result + def get(self, name: str) -> InteractionInput | None: + """Get the input with the specified name, or None if no input matches.""" + lookup_name = name.lower() + for interaction_input in self.to_array(): + input_name = interaction_input.get("Name") + if input_name is not None and input_name.lower() == lookup_name: + return interaction_input + return None + + def required(self, name: str) -> InteractionInput: + """Get the input with the specified name, or raise ValueError if no input matches.""" + interaction_input = self.get(name) + if interaction_input is None: + raise ValueError(f"no input with name '{name}' was found") + return interaction_input + + def value(self, name: str) -> str: + """Get the input value with the specified name, or an empty string if no input matches.""" + interaction_input = self.get(name) + if interaction_input is None: + return "" + return interaction_input.get("Value") or "" + + def required_value(self, name: str) -> str: + """Get the input value with the specified name, or raise ValueError if no input matches.""" + return self.required(name).get("Value") or "" + class InteractionInputLoadContext: """Type class for InteractionInputLoadContext.""" diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 1d860942fbe..0042b2467b1 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -561,11 +561,11 @@ ENTRYPOINT ["dotnet", "App.dll"] }, }) _ = container.WithCommand("echo", "Echo", func(ctx aspire.ExecuteCommandContext) *aspire.ExecuteCommandResult { - commandArguments, err := ctx.Arguments().ToArray() + message, err := ctx.Arguments().Value("message") if err != nil { return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} } - return &aspire.ExecuteCommandResult{Success: commandArguments[0].Value == "hello"} + return &aspire.ExecuteCommandResult{Success: message == "hello"} }, &aspire.WithCommandOptions{ CommandOptions: &aspire.CommandOptions{ Arguments: []*aspire.InteractionInput{ @@ -720,11 +720,9 @@ ENTRYPOINT ["dotnet", "App.dll"] Options: &aspire.InteractionInputsDialogOptions{ PrimaryButtonText: aspire.StringPtr("Save"), ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { - inputs, _ := validationContext.Inputs().ToArray() - for _, input := range inputs { - if input.Name == "solo" && input.Value == "" { - _ = validationContext.AddValidationError("solo", "A value is required.") - } + solo, _ := validationContext.Inputs().Value("solo") + if solo == "" { + _ = validationContext.AddValidationError("solo", "A value is required.") } }, }, @@ -740,11 +738,9 @@ ENTRYPOINT ["dotnet", "App.dll"] PrimaryButtonText: aspire.StringPtr("Submit"), EnableMessageMarkdown: aspire.BoolPtr(true), ValidationCallback: func(validationContext aspire.InputsDialogValidationContext) { - inputs, _ := validationContext.Inputs().ToArray() - for _, input := range inputs { - if input.Name == "name" && input.Value == "bad" { - _ = validationContext.AddValidationError("name", "Name cannot be 'bad'.") - } + name, _ := validationContext.Inputs().Value("name") + if name == "bad" { + _ = validationContext.AddValidationError("name", "Name cannot be 'bad'.") } }, }, diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index c9c1461bee6..f110e5a7c24 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -262,9 +262,8 @@ void main() throws Exception { messageArgument.setRequired(true); echoCommandOptions.setArguments(new InteractionInput[] { messageArgument }); container.withCommand("echo", "Echo", (ctx) -> { - var commandArguments = ctx.arguments().toArray(); var result = new ExecuteCommandResult(); - result.setSuccess("hello".equals(commandArguments[0].getValue())); + result.setSuccess("hello".equals(ctx.arguments().value("message"))); return result; }, echoCommandOptions); container.withCommand("restart", "Restart", (ctx) -> { @@ -395,10 +394,8 @@ void main() throws Exception { var singleDialogOptions = new InteractionInputsDialogOptions(); singleDialogOptions.setPrimaryButtonText("Save"); singleDialogOptions.setValidationCallback((validationContext) -> { - for (var input : validationContext.inputs().toArray()) { - if ("solo".equals(input.getName()) && (input.getValue() == null || input.getValue().isEmpty())) { - validationContext.addValidationError("solo", "A value is required."); - } + if (validationContext.inputs().value("solo").isEmpty()) { + validationContext.addValidationError("solo", "A value is required."); } }); var single = interactionService.promptInput("Single input", "Enter a value.", @@ -409,10 +406,8 @@ void main() throws Exception { multiDialogOptions.setPrimaryButtonText("Submit"); multiDialogOptions.setEnableMessageMarkdown(true); multiDialogOptions.setValidationCallback((validationContext) -> { - for (var input : validationContext.inputs().toArray()) { - if ("name".equals(input.getName()) && "bad".equals(input.getValue())) { - validationContext.addValidationError("name", "Name cannot be 'bad'."); - } + if ("bad".equals(validationContext.inputs().value("name"))) { + validationContext.addValidationError("name", "Name cannot be 'bad'."); } }); var multi = interactionService.promptInputs("Multiple inputs", "Fill out the form.", diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index e727a06c470..c2636bcad92 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -429,8 +429,7 @@ def update_command_state(ctx): return "Enabled" if snapshot.get("HealthStatus") == "Healthy" else "Disabled" def echo_command(ctx): - command_arguments = list(ctx.arguments.to_array()) - return {"success": command_arguments[0]["Value"] == "hello"} + return {"success": ctx.arguments.value("message") == "hello"} container.with_command( "noop", @@ -558,9 +557,7 @@ def load_shade(load_context): ) def validate_solo(validation_context): - inputs = list(validation_context.inputs().to_array()) - solo = next((i for i in inputs if i.get("Name") == "solo"), None) - if not (solo or {}).get("Value"): + if not validation_context.inputs().value("solo"): validation_context.add_validation_error("solo", "A value is required.") single = interaction_service.prompt_input( @@ -571,9 +568,7 @@ def validate_solo(validation_context): ) def validate_form(validation_context): - inputs = list(validation_context.inputs().to_array()) - name = next((i for i in inputs if i.get("Name") == "name"), None) - if (name or {}).get("Value") == "bad": + if validation_context.inputs().value("name") == "bad": validation_context.add_validation_error("name", "Name cannot be 'bad'.") multi = interaction_service.prompt_inputs( From 6303035f81bed8d4347b5ea0317dda6ad85f93fb Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 15:13:02 -0700 Subject: [PATCH 15/20] Model multi-input prompt result as a handle for by-name access Change InputsInteractionResult from a by-value DTO to an [AspireExport(ExposeProperties = true)] handle so its inputs are surfaced as the InteractionInputCollection handle. Polyglot callers can now read submitted values by name (for example result.inputs().value("color")) using the same accessors the validation and command-argument collections expose, instead of scanning a serialized array by hand. canceled and inputs become cheap RPC-backed accessors. From() wraps callback-free input copies (ToResultInput) in a fresh collection so the handle can be enumerated/serialized safely after the prompt completes. Regenerated all 5 language snapshots and the ats.txt baseline, updated the Go/Java/Python/TypeScript polyglot benches to use the by-name accessor, and extended the round-trip test to assert by-name access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 21 ++- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 6 +- ...TwoPassScanningGeneratedAspire.verified.go | 85 ++++++++--- ...oPassScanningGeneratedAspire.verified.java | 43 +++--- ...TwoPassScanningGeneratedAspire.verified.py | 39 ++++- ...TwoPassScanningGeneratedAspire.verified.rs | 66 ++++++--- ...TwoPassScanningGeneratedAspire.verified.ts | 135 +++++++++++++++--- .../AtsExportsTests.cs | 4 + .../Aspire.Hosting/Go/apphost.go | 22 +-- .../Aspire.Hosting/Java/AppHost.java | 19 +-- .../Aspire.Hosting/Python/apphost.py | 10 +- .../Aspire.Hosting/TypeScript/apphost.mts | 10 +- 12 files changed, 335 insertions(+), 125 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index 78dab226bb0..9d79ae4e1fd 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -702,7 +702,13 @@ internal static InputInteractionResult From(InteractionResult /// /// The result of a multi-input interaction prompt. /// -[AspireDto] +/// +/// Modeled as a handle (not a by-value DTO) so the returned inputs are surfaced as the +/// handle. That lets polyglot callers reuse the same name-based +/// accessors (for example result.inputs().value("color")) that the validation and command-argument +/// collections already expose, instead of having to scan a serialized array by hand. +/// +[AspireExport(ExposeProperties = true)] internal sealed class InputsInteractionResult { /// @@ -713,16 +719,21 @@ internal sealed class InputsInteractionResult /// /// Gets the inputs returned from the interaction. Empty when is . /// - public IReadOnlyList Inputs { get; init; } = []; + public required InteractionInputCollection Inputs { get; init; } internal static InputsInteractionResult From(InteractionResult result) { + // The engine returns the live input instances, which still carry the non-serializable dynamic-loading + // callback on DynamicLoading. Project onto callback-free copies (ToResultInput) before wrapping them in a + // fresh collection so the handle can be enumerated/serialized safely after the prompt completes. + var inputs = result.Canceled || result.Data is null + ? new InteractionInputCollection([]) + : new InteractionInputCollection(result.Data.Select(InteractionExports.ToResultInput).ToArray()); + return new InputsInteractionResult { Canceled = result.Canceled, - Inputs = result.Canceled || result.Data is null - ? [] - : result.Data.Select(InteractionExports.ToResultInput).ToArray(), + Inputs = inputs, }; } } diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 8a1cfc50e41..76e31852c12 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -58,6 +58,7 @@ Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceUrlsCallbackContext Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceUrlsEditor Aspire.Hosting/Aspire.Hosting.ApplicationModel.UpdateCommandStateContext [ExposeProperties] Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext [ExposeProperties] +Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult [ExposeProperties] Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext Aspire.Hosting/Aspire.Hosting.DistributedApplication @@ -246,9 +247,6 @@ Aspire.Hosting/Aspire.Hosting.Ats.HttpsCertificateInfo # ATS-friendly certificat Aspire.Hosting/Aspire.Hosting.Ats.InputInteractionResult # The result of a single-input interaction prompt. Canceled: boolean # Gets a value indicating whether the interaction was canceled by the user. Input?: Aspire.Hosting/Aspire.Hosting.InteractionInput # Gets the input returned from the interaction. Not present when `Canceled` is `true`. -Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult # The result of a multi-input interaction prompt. - Canceled: boolean # Gets a value indicating whether the interaction was canceled by the user. - Inputs?: Aspire.Hosting/Aspire.Hosting.InteractionInput[] # Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption # A single selectable option for a choice input. Options are presented in the order supplied. Label: string # Gets or sets the label displayed for this option. Value: string # Gets or sets the value submitted when this option is selected. @@ -479,6 +477,8 @@ Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.cancellationToken(conte Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.executionContext(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext Aspire.Hosting.Ats/getInputName(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext) -> string Aspire.Hosting.Ats/getInputValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, inputName: string) -> string +Aspire.Hosting.Ats/InputsInteractionResult.canceled(context: Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult) -> boolean +Aspire.Hosting.Ats/InputsInteractionResult.inputs(context: Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult) -> Aspire.Hosting/Aspire.Hosting.InteractionInputCollection Aspire.Hosting.Ats/setChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> void Aspire.Hosting.Ats/setValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, value: string) -> void Aspire.Hosting.Ats/withChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index de1f35ae62f..759d950167e 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -570,20 +570,6 @@ func (d *InputInteractionResult) ToMap() map[string]any { return m } -// InputsInteractionResult represents InputsInteractionResult. -type InputsInteractionResult struct { - Canceled bool `json:"Canceled,omitempty"` - Inputs []*InteractionInput `json:"Inputs,omitempty"` -} - -// ToMap converts the DTO to a map for JSON serialization. -func (d *InputsInteractionResult) ToMap() map[string]any { - m := map[string]any{} - m["Canceled"] = serializeValue(d.Canceled) - if d.Inputs != nil { m["Inputs"] = serializeValue(d.Inputs) } - return m -} - // ResourceEventDto represents ResourceEventDto. type ResourceEventDto struct { ResourceName string `json:"ResourceName,omitempty"` @@ -16176,6 +16162,58 @@ func (s *inputsDialogValidationContext) Inputs() InteractionInputCollection { return &interactionInputCollection{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} } +// InputsInteractionResult is the public interface for handle type InputsInteractionResult. +type InputsInteractionResult interface { + handleReference + Canceled() (bool, error) + Inputs() InteractionInputCollection + Err() error +} + +// inputsInteractionResult is the unexported impl of InputsInteractionResult. +type inputsInteractionResult struct { + *resourceBuilderBase +} + +// newInputsInteractionResultFromHandle wraps an existing handle as InputsInteractionResult. +func newInputsInteractionResultFromHandle(h *handle, c *client) InputsInteractionResult { + return &inputsInteractionResult{resourceBuilderBase: newResourceBuilderBase(h, c)} +} + +// Canceled gets a value indicating whether the interaction was canceled by the user. +func (s *inputsInteractionResult) Canceled() (bool, error) { + if s.err != nil { var zero bool; return zero, s.err } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/InputsInteractionResult.canceled", reqArgs) + if err != nil { + var zero bool + return zero, err + } + return decodeAs[bool](result) +} + +// Inputs gets the inputs returned from the interaction. Empty when `Canceled` is `true`. +func (s *inputsInteractionResult) Inputs() InteractionInputCollection { + if s.err != nil { return &interactionInputCollection{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/InputsInteractionResult.inputs", reqArgs) + if err != nil { + return &interactionInputCollection{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting.Ats/InputsInteractionResult.inputs returned unexpected type %T", result) + return &interactionInputCollection{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionInputCollection{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + // InteractionInputBuilder is the public interface for handle type InteractionInputBuilder. type InteractionInputBuilder interface { handleReference @@ -16401,7 +16439,7 @@ type InteractionService interface { IsAvailable() (bool, error) PromptConfirmation(title string, message string, options ...*PromptConfirmationOptions) (*BoolInteractionResult, error) PromptInput(title string, message string, input InteractionInputBuilder, options ...*PromptInputOptions) (*InputInteractionResult, error) - PromptInputs(title string, message string, inputs []InteractionInputBuilder, options ...*PromptInputsOptions) (*InputsInteractionResult, error) + PromptInputs(title string, message string, inputs []InteractionInputBuilder, options ...*PromptInputsOptions) InputsInteractionResult PromptMessageBox(title string, message string, options ...*PromptMessageBoxOptions) (*BoolInteractionResult, error) PromptNotification(title string, message string, options ...*PromptNotificationOptions) (*BoolInteractionResult, error) Err() error @@ -16630,8 +16668,8 @@ func (s *interactionService) PromptInput(title string, message string, input Int } // PromptInputs prompts the user for multiple inputs. -func (s *interactionService) PromptInputs(title string, message string, inputs []InteractionInputBuilder, options ...*PromptInputsOptions) (*InputsInteractionResult, error) { - if s.err != nil { var zero *InputsInteractionResult; return zero, s.err } +func (s *interactionService) PromptInputs(title string, message string, inputs []InteractionInputBuilder, options ...*PromptInputsOptions) InputsInteractionResult { + if s.err != nil { return &inputsInteractionResult{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } ctx := context.Background() reqArgs := map[string]any{ "interactionService": s.handle.ToJSON(), @@ -16654,10 +16692,14 @@ func (s *interactionService) PromptInputs(title string, message string, inputs [ } result, err := s.client.invokeCapability(ctx, "Aspire.Hosting/promptInputs", reqArgs) if err != nil { - var zero *InputsInteractionResult - return zero, err + return &inputsInteractionResult{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting/promptInputs returned unexpected type %T", result) + return &inputsInteractionResult{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} } - return decodeAs[*InputsInteractionResult](result) + return &inputsInteractionResult{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} } // PromptMessageBox prompts the user with a message box dialog. @@ -27565,6 +27607,9 @@ func registerWrappers(c *client) { c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext", func(h *handle, c *client) any { return newInputsDialogValidationContextFromHandle(h, c) }) + c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult", func(h *handle, c *client) any { + return newInputsInteractionResultFromHandle(h, c) + }) c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", func(h *handle, c *client) any { return newInteractionInputBuilderFromHandle(h, c) }) diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 0cc921b3b45..cbc76a3c0dd 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1374,6 +1374,7 @@ public class AspireRegistrations { AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext", (h, c) -> new EventingSubscriberRegistrationContext(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", (h, c) -> new InteractionInputBuilder(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", (h, c) -> new InteractionInputLoadContext(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult", (h, c) -> new InputsInteractionResult(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.AfterResourcesCreatedEvent", (h, c) -> new AfterResourcesCreatedEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.BeforeResourceStartedEvent", (h, c) -> new BeforeResourceStartedEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.BeforeStartEvent", (h, c) -> new BeforeStartEvent(h, c)); @@ -14120,7 +14121,7 @@ private InputsInteractionResult promptInputsImpl(String title, String message, I reqArgs.put("cancellationToken", getClient().registerCancellation(cancellationToken)); } var result = getClient().invokeCapability("Aspire.Hosting/promptInputs", reqArgs); - return InputsInteractionResult.fromMap((Map) result); + return (InputsInteractionResult) result; } public InteractionInputBuilder createTextInput(String name) { @@ -15074,32 +15075,28 @@ public void addValidationError(String inputName, String errorMessage) { import java.util.*; import java.util.function.*; -/** InputsInteractionResult DTO. */ -public class InputsInteractionResult implements JsonSerializable { - private boolean canceled; - private InteractionInput[] inputs; - - public boolean getCanceled() { return canceled; } - public void setCanceled(boolean value) { this.canceled = value; } - public InteractionInput[] getInputs() { return inputs; } - public void setInputs(InteractionInput[] value) { this.inputs = value; } +/** Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult. */ +public class InputsInteractionResult extends HandleWrapperBase { + InputsInteractionResult(Handle handle, AspireClient client) { + super(handle, client); + } - @SuppressWarnings("unchecked") - public static InputsInteractionResult fromMap(Map map) { - var value = new InputsInteractionResult(); - var canceledValue = map.get("Canceled"); - value.setCanceled((Boolean) canceledValue); - var inputsValue = map.get("Inputs"); - value.setInputs((InteractionInput[]) inputsValue); - return value; + /** Gets a value indicating whether the interaction was canceled by the user. */ + public boolean canceled() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + var result = getClient().invokeCapability("Aspire.Hosting.Ats/InputsInteractionResult.canceled", reqArgs); + return (Boolean) result; } - public Map toMap() { - Map map = new HashMap<>(); - map.put("Canceled", AspireClient.serializeValue(canceled)); - map.put("Inputs", AspireClient.serializeValue(inputs)); - return map; + /** Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. */ + public InteractionInputCollection inputs() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + var result = getClient().invokeCapability("Aspire.Hosting.Ats/InputsInteractionResult.inputs", reqArgs); + return (InteractionInputCollection) result; } + } // ===== InteractionChoiceOption.java ===== diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 425e4a5ae99..8807a0aa5ab 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1872,10 +1872,6 @@ class InputInteractionResult(typing.TypedDict, total=False): Canceled: bool Input: InteractionInput -class InputsInteractionResult(typing.TypedDict, total=False): - Canceled: bool - Inputs: typing.Iterable[InteractionInput] - class InteractionChoiceOption(typing.TypedDict, total=False): Value: str Label: str @@ -5222,6 +5218,40 @@ def add_validation_error(self, input_name: str, error_message: str) -> None: ) +class InputsInteractionResult: + """Type class for InputsInteractionResult.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"InputsInteractionResult(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + @_cached_property + def canceled(self) -> bool: + """Gets a value indicating whether the interaction was canceled by the user.""" + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/InputsInteractionResult.canceled', + {'context': self._handle} + ) + return typing.cast(bool, result) + + @_cached_property + def inputs(self) -> InteractionInputCollection: + """Gets the inputs returned from the interaction. Empty when `Canceled` is `true`.""" + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/InputsInteractionResult.inputs', + {'context': self._handle} + ) + return typing.cast(InteractionInputCollection, result) + + class InteractionInputBuilder: """Type class for InteractionInputBuilder.""" @@ -12000,6 +12030,7 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.HttpCommandPrepareRequestContext", HttpCommandPrepareRequestContext) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.InitializeResourceEvent", InitializeResourceEvent) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext", InputsDialogValidationContext) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult", InputsInteractionResult) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", InteractionInputBuilder) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.InteractionInputCollection", InteractionInputCollection) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", InteractionInputLoadContext) diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 37e196a780d..db3c3a743f3 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1100,26 +1100,6 @@ impl InputInteractionResult { } } -/// InputsInteractionResult -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct InputsInteractionResult { - #[serde(rename = "Canceled")] - pub canceled: bool, - #[serde(rename = "Inputs", skip_serializing_if = "Option::is_none")] - pub inputs: Option>, -} - -impl InputsInteractionResult { - pub fn to_map(&self) -> HashMap { - let mut map = HashMap::new(); - map.insert("Canceled".to_string(), serde_json::to_value(&self.canceled).unwrap_or(Value::Null)); - if let Some(ref v) = self.inputs { - map.insert("Inputs".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); - } - map - } -} - /// ResourceEventDto #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ResourceEventDto { @@ -11212,7 +11192,8 @@ impl IInteractionService { args.insert("cancellationToken".to_string(), Value::String(token_id)); } let result = self.client.invoke_capability("Aspire.Hosting/promptInputs", args)?; - Ok(serde_json::from_value(result)?) + let handle: Handle = serde_json::from_value(result)?; + Ok(InputsInteractionResult::new(handle, self.client.clone())) } /// Creates a single-line text input. @@ -12154,6 +12135,49 @@ impl InputsDialogValidationContext { } } +/// Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult +pub struct InputsInteractionResult { + handle: Handle, + client: Arc, +} + +impl HasHandle for InputsInteractionResult { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl InputsInteractionResult { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Gets a value indicating whether the interaction was canceled by the user. + pub fn canceled(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/InputsInteractionResult.canceled", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. + pub fn inputs(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/InputsInteractionResult.inputs", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionInputCollection::new(handle, self.client.clone())) + } +} + /// Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder pub struct InteractionInputBuilder { handle: Handle, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 6624b52cb0f..98bc1478ccc 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -312,6 +312,16 @@ type UpdateCommandStateContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.App /** Context passed to ATS-friendly eventing subscriber registrations. */ type EventingSubscriberRegistrationContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext'>; +/** + * The result of a multi-input interaction prompt. + * + * Modeled as a handle (not a by-value DTO) so the returned inputs are surfaced as the + * `InteractionInputCollection` handle. That lets polyglot callers reuse the same name-based + * accessors (for example `result.inputs().value("color")`) that the validation and command-argument + * collections already expose, instead of having to scan a serialized array by hand. + */ +type InputsInteractionResultHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult'>; + /** * An opaque, server-side builder for an `InteractionInput` used by polyglot app hosts. * @@ -981,14 +991,6 @@ export interface InputInteractionResult { input?: InteractionInput; } -/** The result of a multi-input interaction prompt. */ -export interface InputsInteractionResult { - /** Gets a value indicating whether the interaction was canceled by the user. */ - canceled?: boolean; - /** Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. */ - inputs?: InteractionInput[]; -} - /** A single selectable option for a choice input. Options are presented in the order supplied. */ export interface InteractionChoiceOption { /** Gets or sets the value submitted when this option is selected. */ @@ -5633,6 +5635,92 @@ class InputsDialogValidationContextPromiseImpl implements InputsDialogValidation } +// ============================================================================ +// InputsInteractionResult +// ============================================================================ + +/** + * The result of a multi-input interaction prompt. + * + * Modeled as a handle (not a by-value DTO) so the returned inputs are surfaced as the + * `InteractionInputCollection` handle. That lets polyglot callers reuse the same name-based + * accessors (for example `result.inputs().value("color")`) that the validation and command-argument + * collections already expose, instead of having to scan a serialized array by hand. + */ +export interface InputsInteractionResult { + toJSON(): MarshalledHandle; + /** Gets a value indicating whether the interaction was canceled by the user. */ + canceled(): Promise; + /** Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. */ + inputs(): Promise; +} + +export interface InputsInteractionResultPromise extends PromiseLike { + /** Gets a value indicating whether the interaction was canceled by the user. */ + canceled(): Promise; + /** Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. */ + inputs(): Promise; +} + +// ============================================================================ +// InputsInteractionResultImpl +// ============================================================================ + +/** + * The result of a multi-input interaction prompt. + * + * Modeled as a handle (not a by-value DTO) so the returned inputs are surfaced as the + * `InteractionInputCollection` handle. That lets polyglot callers reuse the same name-based + * accessors (for example `result.inputs().value("color")`) that the validation and command-argument + * collections already expose, instead of having to scan a serialized array by hand. + */ +class InputsInteractionResultImpl implements InputsInteractionResult { + constructor(private _handle: InputsInteractionResultHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + async canceled(): Promise { + return await this._client.invokeCapability( + 'Aspire.Hosting.Ats/InputsInteractionResult.canceled', + { context: this._handle } + ); + } + + async inputs(): Promise { + return await this._client.invokeCapability( + 'Aspire.Hosting.Ats/InputsInteractionResult.inputs', + { context: this._handle } + ); + } + +} + +/** + * Thenable wrapper for InputsInteractionResult that enables fluent chaining. + */ +class InputsInteractionResultPromiseImpl implements InputsInteractionResultPromise { + constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { + if (track) { _client.trackPromise(_promise); } + } + + then( + onfulfilled?: ((value: InputsInteractionResult) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + canceled(): Promise { + return this._promise.then(obj => obj.canceled()); + } + + inputs(): Promise { + return this._promise.then(obj => obj.inputs()); + } + +} + // ============================================================================ // InteractionInputBuilder // ============================================================================ @@ -11269,7 +11357,7 @@ export interface InteractionService { * Prompts the user for multiple inputs. * @param options Additional options. */ - promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): Promise; + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): InputsInteractionResultPromise; /** * Creates a single-line text input. * @param options Additional options. @@ -11328,7 +11416,7 @@ export interface InteractionServicePromise extends PromiseLike; + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): InputsInteractionResultPromise; /** * Creates a single-line text input. * @param options Additional options. @@ -11458,13 +11546,8 @@ class InteractionServiceImpl implements InteractionService { ); } - /** - * Prompts the user for multiple inputs. - * @param optionsBag Additional options. - */ - async promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], optionsBag?: PromptInputsOptions): Promise { - const options = optionsBag?.options; - const cancellationToken = optionsBag?.cancellationToken; + /** @internal */ + async _promptInputsInternal(title: string, message: string, inputs: InteractionInputBuilder[], options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { const __optionsForRpc = options === undefined || options === null ? options : { ...options }; if (__optionsForRpc !== undefined && __optionsForRpc !== null) { const __optionsForRpcData = __optionsForRpc as Record; @@ -11481,10 +11564,21 @@ class InteractionServiceImpl implements InteractionService { const rpcArgs: Record = { interactionService: this._handle, title, message, inputs }; if (options !== undefined) rpcArgs.options = __optionsForRpc; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); - return await this._client.invokeCapability( + const result = await this._client.invokeCapability( 'Aspire.Hosting/promptInputs', rpcArgs ); + return new InputsInteractionResultImpl(result, this._client); + } + + /** + * Prompts the user for multiple inputs. + * @param optionsBag Additional options. + */ + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], optionsBag?: PromptInputsOptions): InputsInteractionResultPromise { + const options = optionsBag?.options; + const cancellationToken = optionsBag?.cancellationToken; + return new InputsInteractionResultPromiseImpl(this._promptInputsInternal(title, message, inputs, options, cancellationToken), this._client); } /** @internal */ @@ -11623,8 +11717,8 @@ class InteractionServicePromiseImpl implements InteractionServicePromise { return this._promise.then(obj => obj.promptInput(title, message, input, options)); } - promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): Promise { - return this._promise.then(obj => obj.promptInputs(title, message, inputs, options)); + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): InputsInteractionResultPromise { + return new InputsInteractionResultPromiseImpl(this._promise.then(obj => obj.promptInputs(title, message, inputs, options)), this._client); } createTextInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { @@ -56399,6 +56493,7 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecuteCom registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.HttpCommandPrepareRequestContext', (handle, client) => new HttpCommandPrepareRequestContextImpl(handle as HttpCommandPrepareRequestContextHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.InitializeResourceEvent', (handle, client) => new InitializeResourceEventImpl(handle as InitializeResourceEventHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.InputsDialogValidationContext', (handle, client) => new InputsDialogValidationContextImpl(handle as InputsDialogValidationContextHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult', (handle, client) => new InputsInteractionResultImpl(handle as InputsInteractionResultHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder', (handle, client) => new InteractionInputBuilderImpl(handle as InteractionInputBuilderHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext', (handle, client) => new InteractionInputLoadContextImpl(handle as InteractionInputLoadContextHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade', (handle, client) => new LogFacadeImpl(handle as LogFacadeHandle, client)); diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs index d9f2e2dd225..6ccd25ea6f7 100644 --- a/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs +++ b/tests/Aspire.Hosting.RemoteHost.Tests/AtsExportsTests.cs @@ -97,6 +97,10 @@ public async Task PromptInputs_ResultCarriesSubmittedValuesByName() var result = await promptTask; Assert.False(result.Canceled); + // Inputs is now the InteractionInputCollection handle, so callers can read submitted values by name + // (mirroring the .NET indexer) in addition to enumerating them. + Assert.Equal("westus", result.Inputs["region"].Value); + Assert.Equal("a", result.Inputs["zone"].Value); Assert.Collection( result.Inputs, input => diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 0042b2467b1..3f82634ec36 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -623,7 +623,11 @@ ENTRYPOINT ["dotnet", "App.dll"] return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} } - return &aspire.ExecuteCommandResult{Success: !result.Canceled, Canceled: aspire.BoolPtr(result.Canceled)} + canceled, err := result.Canceled() + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + return &aspire.ExecuteCommandResult{Success: !canceled, Canceled: aspire.BoolPtr(canceled)} }) // Exhaustive coverage of the remaining IInteractionService surface so every newly added member is @@ -749,27 +753,27 @@ ENTRYPOINT ["dotnet", "App.dll"] return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} } - selectedColor := "" - for _, input := range multi.Inputs { - if input.Name == "color" { - selectedColor = input.Value - } - } + selectedColor, _ := multi.Inputs().Value("color") soloValue := "" if single.Input != nil { soloValue = single.Input.Value } + multiCanceled, err := multi.Canceled() + if err != nil { + return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} + } + success := !confirmation.Canceled && confirmation.Value != nil && *confirmation.Value && !messageBox.Canceled && !notification.Canceled && !single.Canceled && - !multi.Canceled + !multiCanceled return &aspire.ExecuteCommandResult{ Success: success, - Canceled: aspire.BoolPtr(multi.Canceled), + Canceled: aspire.BoolPtr(multiCanceled), Message: aspire.StringPtr(fmt.Sprintf("color=%s solo=%s", selectedColor, soloValue)), } }) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index f110e5a7c24..99d10654a6c 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -305,9 +305,10 @@ void main() throws Exception { "Choose a region, then pick a zone from the dynamically loaded options.", new InteractionInputBuilder[] { regionInput, zoneInput }); + var canceled = result.canceled(); var commandResult = new ExecuteCommandResult(); - commandResult.setSuccess(!result.getCanceled()); - commandResult.setCanceled(result.getCanceled()); + commandResult.setSuccess(!canceled); + commandResult.setCanceled(canceled); return commandResult; }); // Exhaustive coverage of the remaining IInteractionService surface so every newly added member is @@ -414,26 +415,20 @@ void main() throws Exception { new InteractionInputBuilder[] { textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput }, new PromptInputsOptions().options(multiDialogOptions)); - String selectedColor = ""; - if (multi.getInputs() != null) { - for (var input : multi.getInputs()) { - if ("color".equals(input.getName())) { - selectedColor = input.getValue(); - } - } - } + String selectedColor = multi.inputs().value("color"); String soloValue = single.getInput() != null ? single.getInput().getValue() : ""; + var multiCanceled = multi.canceled(); var success = !confirmation.getCanceled() && Boolean.TRUE.equals(confirmation.getValue()) && !messageBox.getCanceled() && !notification.getCanceled() && !single.getCanceled() - && !multi.getCanceled(); + && !multiCanceled; var commandResult = new ExecuteCommandResult(); commandResult.setSuccess(success); - commandResult.setCanceled(multi.getCanceled()); + commandResult.setCanceled(multiCanceled); commandResult.setMessage("color=" + (selectedColor == null ? "" : selectedColor) + " solo=" + (soloValue == null ? "" : soloValue)); return commandResult; diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index c2636bcad92..43c53705480 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -476,7 +476,8 @@ def load_zones(load_context): "Choose a region, then pick a zone from the dynamically loaded options.", [region_input, zone_input] ) - return {"success": not result.get("Canceled", False), "canceled": result.get("Canceled", False)} + canceled = result.canceled() + return {"success": not canceled, "canceled": canceled} container.with_command("pick-zone", "Pick Zone", pick_zone_command) # Exhaustive coverage of the remaining IInteractionService surface so every newly added member is @@ -578,19 +579,20 @@ def validate_form(validation_context): options={"PrimaryButtonText": "Submit", "EnableMessageMarkdown": True, "ValidationCallback": validate_form}, ) - selected_color = next((i.get("Value") for i in (multi.get("Inputs") or []) if i.get("Name") == "color"), None) + selected_color = multi.inputs().value("color") solo_value = (single.get("Input") or {}).get("Value") + multi_canceled = multi.canceled() success = (not confirmation.get("Canceled", False) and confirmation.get("Value", False) and not message_box.get("Canceled", False) and not notification.get("Canceled", False) and not single.get("Canceled", False) - and not multi.get("Canceled", False)) + and not multi_canceled) return { "success": bool(success), - "canceled": multi.get("Canceled", False), + "canceled": multi_canceled, "message": f"color={selected_color or ''} solo={solo_value or ''}", } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index 8f3a90a2a86..a66a9430e49 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -792,7 +792,8 @@ await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { "Choose a region, then pick a zone from the dynamically loaded options.", [regionInput, zoneInput]); - return { success: !result.canceled, canceled: result.canceled }; + const canceled = await result.canceled(); + return { success: !canceled, canceled }; }); // Exhaustive coverage of the remaining IInteractionService surface so every newly added member is // exercised by the polyglot typecheck: all prompt overloads, every input factory and builder method, @@ -897,19 +898,20 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn } }); - const selectedColor = multi.inputs?.find(input => input.name === "color")?.value; + const selectedColor = await (await multi.inputs()).value("color"); const soloValue = single.input?.value; + const multiCanceled = await multi.canceled(); const success = !confirmation.canceled && confirmation.value === true && !messageBox.canceled && !notification.canceled && !single.canceled - && !multi.canceled; + && !multiCanceled; return { success, - canceled: multi.canceled, + canceled: multiCanceled, message: `color=${selectedColor ?? ""} solo=${soloValue ?? ""}` }; }); From c14984f6fd21b8ff879ba6c39aa390a5bf5f6d0d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 16:23:56 -0700 Subject: [PATCH 16/20] Return a fluent thenable for interaction input collections in TypeScript Collection-returning getters (`result.inputs()`, `validationContext.inputs()`, and a command's `arguments()`) returned `Promise`, which forced a double await to reach the by-name accessors: `await (await result.inputs()).value("color")`. The C#, Go, Java, and Python surfaces all chain in a single step, so this was a TypeScript-only wart. Introduce a hand-written `InteractionInputCollectionPromise` thenable in base.mts that is awaitable to the collection but also forwards the by-name accessors (value/get/required/requiredValue/toArray), and register `InteractionInputCollection` as a promise-wrapper type so the generator emits the fluent wrapper for every collection-returning getter. Callers can now write `await result.inputs().value("color")`; the previous double-await form still compiles because the wrapper resolves to the plain collection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsTypeScriptCodeGenerator.cs | 37 ++++++++++- .../Resources/base.mts | 56 +++++++++++++++++ .../AtsTypeScriptCodeGeneratorTests.cs | 2 +- .../Snapshots/AtsGeneratedAspire.verified.ts | 7 ++- ...TwoPassScanningGeneratedAspire.verified.ts | 63 ++++++++++--------- .../Aspire.Hosting/TypeScript/apphost.mts | 6 +- 6 files changed, 133 insertions(+), 38 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs index 3991dc40531..0daabac8f8d 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs @@ -892,7 +892,8 @@ private string GenerateAspireSdk(AtsContext context) ReferenceExpression, refExpr, AspireDict, - AspireList + AspireList, + InteractionInputCollectionPromiseImpl } from './base.mjs'; export { @@ -902,13 +903,15 @@ private string GenerateAspireSdk(AtsContext context) export type { InteractionInput, - InteractionInputOption + InteractionInputOption, + InteractionInputCollectionPromise } from './base.mjs'; import type { Awaitable, InteractionInput, InteractionInputCollection, + InteractionInputCollectionPromise, InputType } from './base.mjs'; """); @@ -1008,6 +1011,18 @@ import type { _typesWithPromiseWrappers.Add(typeClass.TypeId); } } + + // InteractionInputCollection is a hand-written base.mts type: its by-name accessors + // (value/get/required/requiredValue) are client-side conveniences, not ATS capabilities, so + // it is never registered as a generated type class. Register it as a promise-wrapper type so + // collection-returning getters (result.inputs(), validationContext.inputs(), command + // arguments()) emit the fluent InteractionInputCollectionPromise thenable instead of a bare + // Promise. That lets callers chain `await x.inputs().value("c")` + // without an intermediate await, matching the C#/Go/Java/Python surfaces. The wrapper + // (InteractionInputCollectionPromise / InteractionInputCollectionPromiseImpl) is hand-written + // in base.mts; it is intentionally absent from _wrapperClassNames so the getter impl keeps + // using the marshaller-based collection construction rather than a handle+Impl wrapper. + _typesWithPromiseWrappers.Add(InteractionInputCollectionTypeId); // Note: ReferenceExpression is intentionally NOT added to _wrapperClassNames. // It is a value type defined in base.mts with a private constructor and static factory, // not a handle-based wrapper. It is handled via MapTypeRefToTypeScript instead. @@ -3476,6 +3491,24 @@ private void GenerateGetterOnlyPropertyMethod(string propertyName, AtsCapability return; } + // Promise-wrapper types that are NOT registered as generated wrapper classes (currently only + // InteractionInputCollection, a hand-written base.mts type) wrap the marshalled collection + // promise in their hand-written ...Promise thenable so by-name accessors chain without an + // intermediate await. Awaiting the wrapper still resolves to the plain collection, preserving + // the existing `await (await x.inputs()).value(...)` form. + if (TryGetPromiseWrapperType(getter.ReturnType, out var promiseInterfaceName, out var promiseImplementationClassName)) + { + var collectionType = GetGetterOnlyPropertyReturnType(getter.ReturnType); + WriteLine($" {propertyName}(): {promiseInterfaceName} {{"); + WriteLine($" return new {promiseImplementationClassName}(this._client.invokeCapability<{collectionType}>("); + WriteLine($" '{getter.CapabilityId}',"); + WriteLine(" { context: this._handle }"); + WriteLine(" ), this._client, false);"); + WriteLine(" }"); + WriteLine(); + return; + } + var returnType = GetGetterOnlyPropertyReturnType(getter.ReturnType); WriteLine($" async {propertyName}(): Promise<{returnType}> {{"); diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts index 8a4e5a4763c..3516f18a443 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts @@ -456,6 +456,62 @@ registerHandleWrapper(interactionInputCollectionTypeId, (handle, client) => new InteractionInputCollection(handle as InteractionInputCollectionHandle, client) ); +/** + * Thenable wrapper for {@link InteractionInputCollection} that enables fluent by-name access. + * + * Collection-returning getters (for example `result.inputs()`, `validationContext.inputs()`, and a + * command's `arguments()`) return this instead of a bare `Promise`. It + * is awaitable — `await x.inputs()` still resolves to the {@link InteractionInputCollection} — but + * it also forwards the by-name accessors, so callers can chain `await result.inputs().value("color")` + * without an intermediate await. The C#, Go, Java, and Python surfaces already chain this way; this + * restores the same ergonomics for TypeScript, where the underlying accessor is an async RPC. + */ +export interface InteractionInputCollectionPromise extends PromiseLike { + /** Returns a snapshot copy of all inputs in the collection. */ + toArray(): Promise; + /** Gets the input with the specified name, or `undefined` if no such input exists. */ + get(name: string): Promise; + /** Gets the input with the specified name, throwing if no such input exists. */ + required(name: string): Promise; + /** Gets the value of the input with the specified name, or `undefined` if absent. */ + value(name: string): Promise; + /** Gets the value of the input with the specified name, throwing if absent or unset. */ + requiredValue(name: string): Promise; +} + +export class InteractionInputCollectionPromiseImpl implements InteractionInputCollectionPromise { + constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { + if (track) { _client.trackPromise(_promise); } + } + + then( + onfulfilled?: ((value: InteractionInputCollection) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + toArray(): Promise { + return this._promise.then(c => c.toArray()); + } + + get(name: string): Promise { + return this._promise.then(c => c.get(name)); + } + + required(name: string): Promise { + return this._promise.then(c => c.required(name)); + } + + value(name: string): Promise { + return this._promise.then(c => c.value(name)); + } + + requiredValue(name: string): Promise { + return this._promise.then(c => c.requiredValue(name)); + } +} + // ============================================================================ // ResourceBuilderBase // ============================================================================ diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs index 3b58a0259f7..58a90d331aa 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs @@ -111,7 +111,7 @@ public void GenerateDistributedApplication_WithHostingTypes_KeepsReferenceExpres Assert.Contains("condition: extractHandleForExpr(state.condition),", files["base.mts"]); Assert.Contains("('$handle' in json || '$expr' in json)", files["base.mts"]); Assert.Contains("registerCancellation(state.client, cancellationToken)", files["base.mts"]); - Assert.Contains("arguments(): Promise", aspireTs); + Assert.Contains("arguments(): InteractionInputCollectionPromise", aspireTs); Assert.DoesNotContain("setArguments", aspireTs); } diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsGeneratedAspire.verified.ts index 214adcf2ddb..1032ac8f00a 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsGeneratedAspire.verified.ts @@ -25,7 +25,8 @@ import { ReferenceExpression, refExpr, AspireDict, - AspireList + AspireList, + InteractionInputCollectionPromiseImpl } from './base.mjs'; export { @@ -35,13 +36,15 @@ export { export type { InteractionInput, - InteractionInputOption + InteractionInputOption, + InteractionInputCollectionPromise } from './base.mjs'; import type { Awaitable, InteractionInput, InteractionInputCollection, + InteractionInputCollectionPromise, InputType } from './base.mjs'; diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 98bc1478ccc..13121a28014 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -25,7 +25,8 @@ import { ReferenceExpression, refExpr, AspireDict, - AspireList + AspireList, + InteractionInputCollectionPromiseImpl } from './base.mjs'; export { @@ -35,13 +36,15 @@ export { export type { InteractionInput, - InteractionInputOption + InteractionInputOption, + InteractionInputCollectionPromise } from './base.mjs'; import type { Awaitable, InteractionInput, InteractionInputCollection, + InteractionInputCollectionPromise, InputType } from './base.mjs'; @@ -5157,7 +5160,7 @@ export interface ExecuteCommandContext { * submitted values populated. CLI positional arguments are mapped by declaration order. Dashboard, MCP, and other * named-payload clients are mapped by `Name`. */ - arguments(): Promise; + arguments(): InteractionInputCollectionPromise; } export interface ExecuteCommandContextPromise extends PromiseLike { @@ -5176,7 +5179,7 @@ export interface ExecuteCommandContextPromise extends PromiseLike; + arguments(): InteractionInputCollectionPromise; } // ============================================================================ @@ -5227,11 +5230,11 @@ class ExecuteCommandContextImpl implements ExecuteCommandContext { return new LoggerPromiseImpl(promise, this._client, false); } - async arguments(): Promise { - return await this._client.invokeCapability( + arguments(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._client.invokeCapability( 'Aspire.Hosting.ApplicationModel/ExecuteCommandContext.arguments', { context: this._handle } - ); + ), this._client, false); } } @@ -5267,8 +5270,8 @@ class ExecuteCommandContextPromiseImpl implements ExecuteCommandContextPromise { return new LoggerPromiseImpl(this._promise.then(obj => obj.logger()), this._client, false); } - arguments(): Promise { - return this._promise.then(obj => obj.arguments()); + arguments(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._promise.then(obj => obj.arguments()), this._client, false); } } @@ -5287,7 +5290,7 @@ export interface HttpCommandPrepareRequestContext { /** The cancellation token. */ cancellationToken(): Promise; /** Gets the invocation arguments supplied by the client when the command is executed. */ - arguments(): Promise; + arguments(): InteractionInputCollectionPromise; } export interface HttpCommandPrepareRequestContextPromise extends PromiseLike { @@ -5298,7 +5301,7 @@ export interface HttpCommandPrepareRequestContextPromise extends PromiseLike; /** Gets the invocation arguments supplied by the client when the command is executed. */ - arguments(): Promise; + arguments(): InteractionInputCollectionPromise; } // ============================================================================ @@ -5338,11 +5341,11 @@ class HttpCommandPrepareRequestContextImpl implements HttpCommandPrepareRequestC return CancellationToken.fromValue(result); } - async arguments(): Promise { - return await this._client.invokeCapability( + arguments(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._client.invokeCapability( 'Aspire.Hosting.ApplicationModel/HttpCommandPrepareRequestContext.arguments', { context: this._handle } - ); + ), this._client, false); } } @@ -5374,8 +5377,8 @@ class HttpCommandPrepareRequestContextPromiseImpl implements HttpCommandPrepareR return this._promise.then(obj => obj.cancellationToken()); } - arguments(): Promise { - return this._promise.then(obj => obj.arguments()); + arguments(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._promise.then(obj => obj.arguments()), this._client, false); } } @@ -5535,7 +5538,7 @@ class InitializeResourceEventPromiseImpl implements InitializeResourceEventPromi export interface InputsDialogValidationContext { toJSON(): MarshalledHandle; /** Gets the inputs that are being validated. */ - inputs(): Promise; + inputs(): InteractionInputCollectionPromise; /** Gets the cancellation token for the validation operation. */ cancellationToken(): Promise; /** @@ -5548,7 +5551,7 @@ export interface InputsDialogValidationContext { export interface InputsDialogValidationContextPromise extends PromiseLike { /** Gets the inputs that are being validated. */ - inputs(): Promise; + inputs(): InteractionInputCollectionPromise; /** Gets the cancellation token for the validation operation. */ cancellationToken(): Promise; /** @@ -5570,11 +5573,11 @@ class InputsDialogValidationContextImpl implements InputsDialogValidationContext /** Serialize for JSON-RPC transport */ toJSON(): MarshalledHandle { return this._handle.toJSON(); } - async inputs(): Promise { - return await this._client.invokeCapability( + inputs(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._client.invokeCapability( 'Aspire.Hosting/InputsDialogValidationContext.inputs', { context: this._handle } - ); + ), this._client, false); } async cancellationToken(): Promise { @@ -5621,8 +5624,8 @@ class InputsDialogValidationContextPromiseImpl implements InputsDialogValidation return this._promise.then(onfulfilled, onrejected); } - inputs(): Promise { - return this._promise.then(obj => obj.inputs()); + inputs(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._promise.then(obj => obj.inputs()), this._client, false); } cancellationToken(): Promise { @@ -5652,14 +5655,14 @@ export interface InputsInteractionResult { /** Gets a value indicating whether the interaction was canceled by the user. */ canceled(): Promise; /** Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. */ - inputs(): Promise; + inputs(): InteractionInputCollectionPromise; } export interface InputsInteractionResultPromise extends PromiseLike { /** Gets a value indicating whether the interaction was canceled by the user. */ canceled(): Promise; /** Gets the inputs returned from the interaction. Empty when `Canceled` is `true`. */ - inputs(): Promise; + inputs(): InteractionInputCollectionPromise; } // ============================================================================ @@ -5687,11 +5690,11 @@ class InputsInteractionResultImpl implements InputsInteractionResult { ); } - async inputs(): Promise { - return await this._client.invokeCapability( + inputs(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._client.invokeCapability( 'Aspire.Hosting.Ats/InputsInteractionResult.inputs', { context: this._handle } - ); + ), this._client, false); } } @@ -5715,8 +5718,8 @@ class InputsInteractionResultPromiseImpl implements InputsInteractionResultPromi return this._promise.then(obj => obj.canceled()); } - inputs(): Promise { - return this._promise.then(obj => obj.inputs()); + inputs(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._promise.then(obj => obj.inputs()), this._client, false); } } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index a66a9430e49..bdb4a7700d5 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -873,7 +873,7 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn options: { primaryButtonText: "Save", validationCallback: async (validationContext) => { - const solo = await (await validationContext.inputs()).value("solo"); + const solo = await validationContext.inputs().value("solo"); if (!solo) { await validationContext.addValidationError("solo", "A value is required."); } @@ -890,7 +890,7 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn primaryButtonText: "Submit", enableMessageMarkdown: true, validationCallback: async (validationContext) => { - const name = await (await validationContext.inputs()).value("name"); + const name = await validationContext.inputs().value("name"); if (name === "bad") { await validationContext.addValidationError("name", "Name cannot be 'bad'."); } @@ -898,7 +898,7 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn } }); - const selectedColor = await (await multi.inputs()).value("color"); + const selectedColor = await multi.inputs().value("color"); const soloValue = single.input?.value; const multiCanceled = await multi.canceled(); From 6d8924ef3af1506f43a698588621ea3c1bba147a Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 16:24:05 -0700 Subject: [PATCH 17/20] Fix Go polyglot bench for handle-returning PromptInputs The prompt-result-as-handle change made `PromptInputs` return a single `InputsInteractionResult` handle (captured-error pattern) instead of `(result, error)`, but the Go bench still used two-variable assignment, breaking the Go SDK Validation build (`assignment mismatch: 2 variables but PromptInputs returns 1 value`). Update both call sites to single-value assignment; the error now surfaces on the terminal `Canceled()`/`Inputs()` calls, consistent with the other handle wrappers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 3f82634ec36..6cc060cb7ed 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -618,10 +618,7 @@ ENTRYPOINT ["dotnet", "App.dll"] _ = loadContext.SetChoiceOptions(zones) }) - result, err := interactionService.PromptInputs("Pick a zone", "Choose a region, then pick a zone from the dynamically loaded options.", []aspire.InteractionInputBuilder{regionInput, zoneInput}) - if err != nil { - return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} - } + result := interactionService.PromptInputs("Pick a zone", "Choose a region, then pick a zone from the dynamically loaded options.", []aspire.InteractionInputBuilder{regionInput, zoneInput}) canceled, err := result.Canceled() if err != nil { @@ -735,7 +732,7 @@ ENTRYPOINT ["dotnet", "App.dll"] return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} } - multi, err := interactionService.PromptInputs("Multiple inputs", "Fill out the form.", + multi := interactionService.PromptInputs("Multiple inputs", "Fill out the form.", []aspire.InteractionInputBuilder{textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput}, &aspire.PromptInputsOptions{ Options: &aspire.InteractionInputsDialogOptions{ @@ -749,9 +746,6 @@ ENTRYPOINT ["dotnet", "App.dll"] }, }, }) - if err != nil { - return &aspire.ExecuteCommandResult{Success: false, ErrorMessage: aspire.StringPtr(aspire.FormatError(err))} - } selectedColor, _ := multi.Inputs().Value("color") soloValue := "" From 6d0e4810edafe5d692abb0cf62201ee168e43fe2 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Jun 2026 16:43:53 -0700 Subject: [PATCH 18/20] Flatten options DTO in generated TS when a cancellation token is also present The TypeScript generator only collapsed a lone `options` DTO parameter directly when it was the *single* optional parameter. Methods that also accept a trailing `cancellationToken` (e.g. the IInteractionService prompt methods) therefore bundled both optionals into a generated `{ options?, cancellationToken? }` object, forcing callers to write the awkward double-nested `promptNotification(t, m, { options: { ... } })`. Thread a trailing cancellation token as its own parameter so the options DTO can be used directly alongside it, collapsing the public API to `promptNotification(t, m, { intent, ... }, cancellationToken?)` while keeping the named C# options object. This mirrors the C# IInteractionService shape and removes the generated PromptXxxOptions wrapper interfaces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsTypeScriptCodeGenerator.cs | 68 ++++++++++--- ...TwoPassScanningGeneratedAspire.verified.ts | 95 ++++++------------- .../Aspire.Hosting/TypeScript/apphost.mts | 53 +++++------ 3 files changed, 109 insertions(+), 107 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs index 0daabac8f8d..b6a33f0971c 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs @@ -1434,12 +1434,18 @@ private static bool TryGetDirectOptionsParameter(List optional // When ATS already exposes a single DTO parameter named "options", reuse that DTO type // directly so the generated TypeScript API stays flat instead of wrapping it in another // generated options object. - if (optionalParams.Count != 1) + // + // A trailing cancellation token is threaded as its own parameter (see + // GetTrailingCancellationTokenParameter), so ignore it here. That keeps the generated + // signature flat — e.g. promptNotification(title, message, options?, cancellationToken?) + // — instead of bundling both optionals into a generated { options?, cancellationToken? } bag. + var nonCancellationOptionals = optionalParams.Where(p => !IsCancellationTokenType(p.Type)).ToList(); + if (nonCancellationOptionals.Count != 1) { return false; } - var candidate = optionalParams[0]; + var candidate = nonCancellationOptionals[0]; if (!string.Equals(candidate.Name, "options", StringComparison.Ordinal) || candidate.Type?.Category != AtsTypeCategory.Dto) { return false; @@ -1449,6 +1455,21 @@ private static bool TryGetDirectOptionsParameter(List optional return true; } + /// + /// When the options DTO is threaded directly (see ), + /// returns the trailing cancellation token optional parameter (if any) so it can be appended to + /// the generated method as its own argument rather than being folded into a generated options bag. + /// + private static AtsParameterInfo? GetTrailingCancellationTokenParameter(List optionalParams) + { + if (!TryGetDirectOptionsParameter(optionalParams, out _)) + { + return null; + } + + return optionalParams.FirstOrDefault(p => IsCancellationTokenType(p.Type)); + } + /// /// Registers an options interface to be generated later. /// Uses method name to create the interface name. When methods share a name but have @@ -1631,7 +1652,8 @@ private string BuildPublicParameterList( List requiredParams, bool hasOptionals, string optionsInterfaceName, - string optionsParameterName = "options") + string optionsParameterName = "options", + AtsParameterInfo? trailingCancellationToken = null) { var publicParamDefs = new List(); foreach (var param in requiredParams) @@ -1643,6 +1665,10 @@ private string BuildPublicParameterList( { publicParamDefs.Add($"{optionsParameterName}?: {optionsInterfaceName}"); } + if (trailingCancellationToken is not null) + { + publicParamDefs.Add($"{trailingCancellationToken.Name}?: {MapParameterToTypeScript(trailingCancellationToken)}"); + } return string.Join(", ", publicParamDefs); } @@ -1830,7 +1856,7 @@ private void GenerateBuilderInterface(BuilderModel builder) var hasOptionals = optionalParams.Count > 0; var hasDirectOptionsParameter = TryGetDirectOptionsParameter(optionalParams, out var directOptionsParam); var optionsInterfaceName = hasDirectOptionsParameter ? MapParameterToTypeScript(directOptionsParam!) : ResolveOptionsInterfaceName(capability); - var publicParamsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName); + var publicParamsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, trailingCancellationToken: GetTrailingCancellationTokenParameter(optionalParams)); var hasNonBuilderReturn = !capability.ReturnsBuilder && capability.ReturnType != null; WriteCapabilityDocComment(" ", capability, requiredParams, hasOptionals ? "options" : null); @@ -1890,7 +1916,7 @@ private void GenerateBuilderPromiseInterface(BuilderModel builder) var hasOptionals = optionalParams.Count > 0; var hasDirectOptionsParameter = TryGetDirectOptionsParameter(optionalParams, out var directOptionsParam); var optionsInterfaceName = hasDirectOptionsParameter ? MapParameterToTypeScript(directOptionsParam!) : ResolveOptionsInterfaceName(capability); - var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName); + var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, trailingCancellationToken: GetTrailingCancellationTokenParameter(optionalParams)); var hasNonBuilderReturn = !capability.ReturnsBuilder && capability.ReturnType != null; WriteCapabilityDocComment(" ", capability, requiredParams, hasOptionals ? "options" : null); @@ -1927,7 +1953,7 @@ private void GenerateTypeClassInterfaceMethod(string className, AtsCapabilityInf var hasOptionals = optionalParams.Count > 0; var hasDirectOptionsParameter = TryGetDirectOptionsParameter(optionalParams, out var directOptionsParam); var optionsInterfaceName = hasDirectOptionsParameter ? MapParameterToTypeScript(directOptionsParam!) : ResolveOptionsInterfaceName(capability); - var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName); + var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, trailingCancellationToken: GetTrailingCancellationTokenParameter(optionalParams)); var isVoid = capability.ReturnType == null || capability.ReturnType.TypeId == AtsConstants.Void; WriteCapabilityDocComment(" ", capability, requiredParams, hasOptionals ? "options" : null); @@ -2099,7 +2125,7 @@ private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capab var publicOptionsParamName = GetPublicOptionsParameterName(userParams, hasOptionals, hasDirectOptionsParameter); // Build parameter list for public method - var publicParamsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsTypeName, publicOptionsParamName); + var publicParamsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsTypeName, publicOptionsParamName, GetTrailingCancellationTokenParameter(optionalParams)); // Build parameter list for internal method (all params positional for callback registration) var internalParamDefs = new List(); @@ -2676,6 +2702,7 @@ private void GenerateThenableClass(BuilderModel builder) var hasOptionals = optionalParams.Count > 0; var hasDirectOptionsParameter = TryGetDirectOptionsParameter(optionalParams, out var directOptionsParam); var optionsTypeName = hasDirectOptionsParameter ? MapParameterToTypeScript(directOptionsParam!) : ResolveOptionsInterfaceName(capability); + var trailingCancellationToken = GetTrailingCancellationTokenParameter(optionalParams); // Build parameter list using options pattern var publicParamDefs = new List(); @@ -2688,6 +2715,10 @@ private void GenerateThenableClass(BuilderModel builder) { publicParamDefs.Add($"options?: {optionsTypeName}"); } + if (trailingCancellationToken is not null) + { + publicParamDefs.Add($"{trailingCancellationToken.Name}?: {MapParameterToTypeScript(trailingCancellationToken)}"); + } var paramsString = string.Join(", ", publicParamDefs); // Forward args to underlying object's method (which handles options extraction) @@ -2700,6 +2731,10 @@ private void GenerateThenableClass(BuilderModel builder) { forwardArgs.Add("options"); } + if (trailingCancellationToken is not null) + { + forwardArgs.Add(trailingCancellationToken.Name); + } var argsString = string.Join(", ", forwardArgs); // Check if this method returns a non-builder type @@ -3817,7 +3852,7 @@ private void GenerateContextMethod(AtsCapabilityInfo method) var publicOptionsParamName = GetPublicOptionsParameterName(userParams, hasOptionals, hasDirectOptionsParameter); // Build parameter list using options pattern - var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, publicOptionsParamName); + var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, publicOptionsParamName, GetTrailingCancellationTokenParameter(optionalParams)); // Determine return type var returnType = GetReturnTypeId(method) != null @@ -3932,7 +3967,7 @@ private void GenerateWrapperMethod(AtsCapabilityInfo capability) var publicOptionsParamName = GetPublicOptionsParameterName(userParams, hasOptionals, hasDirectOptionsParameter); // Build parameter list using options pattern - var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, publicOptionsParamName); + var paramsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, publicOptionsParamName, GetTrailingCancellationTokenParameter(optionalParams)); // Determine return type var returnType = MapTypeRefToTypeScript(capability.ReturnType); @@ -4050,7 +4085,7 @@ private void GenerateTypeClassMethod(BuilderModel model, AtsCapabilityInfo capab var publicOptionsParamName = GetPublicOptionsParameterName(userParams, hasOptionals, hasDirectOptionsParameter); // Build parameter list for public method - var publicParamsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, publicOptionsParamName); + var publicParamsString = BuildPublicParameterList(requiredParams, hasOptionals, optionsInterfaceName, publicOptionsParamName, GetTrailingCancellationTokenParameter(optionalParams)); // Build parameter list for internal method (all params positional) var internalParamDefs = new List(); @@ -4168,7 +4203,7 @@ private void GenerateTypeClassMethod(BuilderModel model, AtsCapabilityInfo capab WriteLine($"): {promiseClass} {{"); // Extract optional params and forward - foreach (var param in optionalParams) + foreach (var param in hasDirectOptionsParameter ? [] : optionalParams) { var localParameterName = GetLocalParameterName(param); WriteLine($" {(IsWidenedHandleType(param.Type) ? "let" : "const")} {localParameterName} = {publicOptionsParamName}?.{param.Name};"); @@ -4188,7 +4223,7 @@ private void GenerateTypeClassMethod(BuilderModel model, AtsCapabilityInfo capab WriteLine($"): Promise<{returnType}> {{"); // Extract optional params from options object - foreach (var param in optionalParams) + foreach (var param in hasDirectOptionsParameter ? [] : optionalParams) { var localParameterName = GetLocalParameterName(param); WriteLine($" {(IsWidenedHandleType(param.Type) ? "let" : "const")} {localParameterName} = {publicOptionsParamName}?.{param.Name};"); @@ -4310,6 +4345,7 @@ private void GenerateTypeClassThenableWrapper(BuilderModel model, List 0; var hasDirectOptionsParameter = TryGetDirectOptionsParameter(optionalParams, out var directOptionsParam); var optionsInterfaceName = hasDirectOptionsParameter ? MapParameterToTypeScript(directOptionsParam!) : ResolveOptionsInterfaceName(capability); + var trailingCancellationToken = GetTrailingCancellationTokenParameter(optionalParams); // Build parameter list using options pattern var publicParamDefs = new List(); @@ -4322,6 +4358,10 @@ private void GenerateTypeClassThenableWrapper(BuilderModel model, List Promise; @@ -11340,27 +11315,27 @@ export interface InteractionService { * Prompts the user for confirmation with an OK/Cancel dialog. * @param options Additional options. */ - promptConfirmation(title: string, message: string, options?: PromptConfirmationOptions): Promise; + promptConfirmation(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user with a message box dialog. * @param options Additional options. */ - promptMessageBox(title: string, message: string, options?: PromptMessageBoxOptions): Promise; + promptMessageBox(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user with a notification. * @param options Additional options. */ - promptNotification(title: string, message: string, options?: PromptNotificationOptions): Promise; + promptNotification(title: string, message: string, options?: InteractionNotificationOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user for a single input. * @param options Additional options. */ - promptInput(title: string, message: string, input: Awaitable, options?: PromptInputOptions): Promise; + promptInput(title: string, message: string, input: Awaitable, options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user for multiple inputs. * @param options Additional options. */ - promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): InputsInteractionResultPromise; + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): InputsInteractionResultPromise; /** * Creates a single-line text input. * @param options Additional options. @@ -11399,27 +11374,27 @@ export interface InteractionServicePromise extends PromiseLike; + promptConfirmation(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user with a message box dialog. * @param options Additional options. */ - promptMessageBox(title: string, message: string, options?: PromptMessageBoxOptions): Promise; + promptMessageBox(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user with a notification. * @param options Additional options. */ - promptNotification(title: string, message: string, options?: PromptNotificationOptions): Promise; + promptNotification(title: string, message: string, options?: InteractionNotificationOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user for a single input. * @param options Additional options. */ - promptInput(title: string, message: string, input: Awaitable, options?: PromptInputOptions): Promise; + promptInput(title: string, message: string, input: Awaitable, options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): Promise; /** * Prompts the user for multiple inputs. * @param options Additional options. */ - promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): InputsInteractionResultPromise; + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): InputsInteractionResultPromise; /** * Creates a single-line text input. * @param options Additional options. @@ -11473,11 +11448,9 @@ class InteractionServiceImpl implements InteractionService { /** * Prompts the user for confirmation with an OK/Cancel dialog. - * @param optionsBag Additional options. + * @param options Additional options. */ - async promptConfirmation(title: string, message: string, optionsBag?: PromptConfirmationOptions): Promise { - const options = optionsBag?.options; - const cancellationToken = optionsBag?.cancellationToken; + async promptConfirmation(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { const rpcArgs: Record = { interactionService: this._handle, title, message }; if (options !== undefined) rpcArgs.options = options; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); @@ -11489,11 +11462,9 @@ class InteractionServiceImpl implements InteractionService { /** * Prompts the user with a message box dialog. - * @param optionsBag Additional options. + * @param options Additional options. */ - async promptMessageBox(title: string, message: string, optionsBag?: PromptMessageBoxOptions): Promise { - const options = optionsBag?.options; - const cancellationToken = optionsBag?.cancellationToken; + async promptMessageBox(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { const rpcArgs: Record = { interactionService: this._handle, title, message }; if (options !== undefined) rpcArgs.options = options; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); @@ -11505,11 +11476,9 @@ class InteractionServiceImpl implements InteractionService { /** * Prompts the user with a notification. - * @param optionsBag Additional options. + * @param options Additional options. */ - async promptNotification(title: string, message: string, optionsBag?: PromptNotificationOptions): Promise { - const options = optionsBag?.options; - const cancellationToken = optionsBag?.cancellationToken; + async promptNotification(title: string, message: string, options?: InteractionNotificationOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { const rpcArgs: Record = { interactionService: this._handle, title, message }; if (options !== undefined) rpcArgs.options = options; if (cancellationToken !== undefined) rpcArgs.cancellationToken = CancellationToken.fromValue(cancellationToken); @@ -11521,11 +11490,9 @@ class InteractionServiceImpl implements InteractionService { /** * Prompts the user for a single input. - * @param optionsBag Additional options. + * @param options Additional options. */ - async promptInput(title: string, message: string, input: Awaitable, optionsBag?: PromptInputOptions): Promise { - const options = optionsBag?.options; - const cancellationToken = optionsBag?.cancellationToken; + async promptInput(title: string, message: string, input: Awaitable, options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { input = isPromiseLike(input) ? await input : input; const __optionsForRpc = options === undefined || options === null ? options : { ...options }; if (__optionsForRpc !== undefined && __optionsForRpc !== null) { @@ -11576,11 +11543,9 @@ class InteractionServiceImpl implements InteractionService { /** * Prompts the user for multiple inputs. - * @param optionsBag Additional options. + * @param options Additional options. */ - promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], optionsBag?: PromptInputsOptions): InputsInteractionResultPromise { - const options = optionsBag?.options; - const cancellationToken = optionsBag?.cancellationToken; + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): InputsInteractionResultPromise { return new InputsInteractionResultPromiseImpl(this._promptInputsInternal(title, message, inputs, options, cancellationToken), this._client); } @@ -11704,24 +11669,24 @@ class InteractionServicePromiseImpl implements InteractionServicePromise { return this._promise.then(obj => obj.isAvailable()); } - promptConfirmation(title: string, message: string, options?: PromptConfirmationOptions): Promise { - return this._promise.then(obj => obj.promptConfirmation(title, message, options)); + promptConfirmation(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { + return this._promise.then(obj => obj.promptConfirmation(title, message, options, cancellationToken)); } - promptMessageBox(title: string, message: string, options?: PromptMessageBoxOptions): Promise { - return this._promise.then(obj => obj.promptMessageBox(title, message, options)); + promptMessageBox(title: string, message: string, options?: InteractionMessageBoxOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { + return this._promise.then(obj => obj.promptMessageBox(title, message, options, cancellationToken)); } - promptNotification(title: string, message: string, options?: PromptNotificationOptions): Promise { - return this._promise.then(obj => obj.promptNotification(title, message, options)); + promptNotification(title: string, message: string, options?: InteractionNotificationOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { + return this._promise.then(obj => obj.promptNotification(title, message, options, cancellationToken)); } - promptInput(title: string, message: string, input: Awaitable, options?: PromptInputOptions): Promise { - return this._promise.then(obj => obj.promptInput(title, message, input, options)); + promptInput(title: string, message: string, input: Awaitable, options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): Promise { + return this._promise.then(obj => obj.promptInput(title, message, input, options, cancellationToken)); } - promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: PromptInputsOptions): InputsInteractionResultPromise { - return new InputsInteractionResultPromiseImpl(this._promise.then(obj => obj.promptInputs(title, message, inputs, options)), this._client); + promptInputs(title: string, message: string, inputs: InteractionInputBuilder[], options?: InteractionInputsDialogOptions, cancellationToken?: AbortSignal | CancellationToken): InputsInteractionResultPromise { + return new InputsInteractionResultPromiseImpl(this._promise.then(obj => obj.promptInputs(title, message, inputs, options, cancellationToken)), this._client); } createTextInput(name: string, options?: CreateInteractionInputOptions): InteractionInputBuilderPromise { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index bdb4a7700d5..be2dde587ef 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -806,27 +806,24 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn } const confirmation = await interactionService.promptConfirmation("Confirm", "Proceed?", { - options: { - primaryButtonText: "Yes", - secondaryButtonText: "No", - showSecondaryButton: true, - showDismiss: true, - enableMessageMarkdown: true, - intent: MessageIntent.Confirmation - } + primaryButtonText: "Yes", + secondaryButtonText: "No", + showSecondaryButton: true, + showDismiss: true, + enableMessageMarkdown: true, + intent: MessageIntent.Confirmation }); const messageBox = await interactionService.promptMessageBox("Notice", "Read this.", { - options: { primaryButtonText: "OK", intent: MessageIntent.Information } + primaryButtonText: "OK", + intent: MessageIntent.Information }); const notification = await interactionService.promptNotification("Heads up", "Something happened.", { - options: { - intent: MessageIntent.Warning, - linkText: "Learn more", - linkUrl: "https://aspire.dev", - showDismiss: true - } + intent: MessageIntent.Warning, + linkText: "Learn more", + linkUrl: "https://aspire.dev", + showDismiss: true }); const textInput = await interactionService.createTextInput("name", { @@ -870,13 +867,11 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn "Enter a value.", interactionService.createTextInput("solo"), { - options: { - primaryButtonText: "Save", - validationCallback: async (validationContext) => { - const solo = await validationContext.inputs().value("solo"); - if (!solo) { - await validationContext.addValidationError("solo", "A value is required."); - } + primaryButtonText: "Save", + validationCallback: async (validationContext) => { + const solo = await validationContext.inputs().value("solo"); + if (!solo) { + await validationContext.addValidationError("solo", "A value is required."); } } }); @@ -886,14 +881,12 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn "Fill out the form.", [textInput, secretInput, booleanInput, numberInput, choiceInput, presetInput, sizeInput, dependentInput], { - options: { - primaryButtonText: "Submit", - enableMessageMarkdown: true, - validationCallback: async (validationContext) => { - const name = await validationContext.inputs().value("name"); - if (name === "bad") { - await validationContext.addValidationError("name", "Name cannot be 'bad'."); - } + primaryButtonText: "Submit", + enableMessageMarkdown: true, + validationCallback: async (validationContext) => { + const name = await validationContext.inputs().value("name"); + if (name === "bad") { + await validationContext.addValidationError("name", "Name cannot be 'bad'."); } } }); From 930c3f127877d73ca971e6fc29e81562d06e5c90 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 9 Jun 2026 07:58:31 -0700 Subject: [PATCH 19/20] Expose the dynamic-loading input as a handle on the load context The dynamic-loading callback previously set choices/value through methods on the load context itself (loadContext.setChoiceOptions/setValue), which is asymmetric with the native C# API where the callback mutates the input it is loading via LoadInputContext.Input (e.g. context.Input.Options = [...]). Add an InteractionLoadingInput handle and expose it as loadContext.input(), moving the input mutators (getName, setChoiceOptions, setValue) onto it. The input must be a handle rather than the by-value InteractionInput DTO so guarded setters route back to the live server-side input across the ATS boundary; the cancellation guard against stale loads is preserved. Cross-input reads (getInputValue) stay on the context, mirroring native LoadInputContext.AllInputs. Regenerated ats.txt and all five language snapshots, and updated the TypeScript, Go, Python, and Java polyglot benches to call loadContext.input().setChoiceOptions(...). Each bench was validated against its regenerated SDK (tsc, go build, py_compile, javac). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 57 ++++- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 8 +- ...TwoPassScanningGeneratedAspire.verified.go | 67 +++-- ...oPassScanningGeneratedAspire.verified.java | 66 +++-- ...TwoPassScanningGeneratedAspire.verified.py | 38 ++- ...TwoPassScanningGeneratedAspire.verified.rs | 47 +++- ...TwoPassScanningGeneratedAspire.verified.ts | 235 +++++++++++++----- .../Aspire.Hosting/Go/apphost.go | 9 +- .../Aspire.Hosting/Java/AppHost.java | 9 +- .../Aspire.Hosting/Python/apphost.py | 9 +- .../Aspire.Hosting/TypeScript/apphost.mts | 9 +- 11 files changed, 422 insertions(+), 132 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index 9d79ae4e1fd..f3f05506db8 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -336,27 +336,34 @@ public InteractionInputBuilder WithDynamicLoading(Func -/// The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the -/// prompt, and provides guarded setters to update the loading input. +/// The context passed to a polyglot dynamic-loading callback. Exposes the loading input as a handle and provides +/// read access to the other inputs in the prompt. /// [AspireExport] internal sealed class InteractionInputLoadContext { private readonly LoadInputContext _inner; + private readonly InteractionLoadingInput _input; internal InteractionInputLoadContext(LoadInputContext inner) { _inner = inner; + _input = new InteractionLoadingInput(inner); } /// - /// Gets the name of the input that is loading. + /// Gets a handle to the input that is loading. Mutate the input through this handle. /// - /// The input name. + /// A handle to the loading input. + /// + /// Mirrors the native LoadInputContext.Input: the callback updates the live input it is loading, rather than + /// the context itself. The input is a handle (not a by-value DTO) so guarded setters route back to the server-side + /// input across the ATS boundary. + /// [AspireExport] - public string GetInputName() + public InteractionLoadingInput Input() { - return _inner.Input.Name; + return _input; } /// @@ -364,6 +371,10 @@ public string GetInputName() /// /// The name of the input to read. /// The input value, or an empty string when the input has no value or no input with that name exists. + /// + /// Reads any input in the prompt, mirroring the native LoadInputContext.AllInputs. Use this to read the + /// dependency inputs declared via . + /// [AspireExport] public string GetInputValue(string inputName) { @@ -371,9 +382,39 @@ public string GetInputValue(string inputName) return _inner.AllInputs.TryGetByName(inputName, out var input) ? input.Value ?? string.Empty : string.Empty; } +} + +/// +/// A handle to the input currently being loaded by a dynamic-loading callback. Mirrors the native +/// LoadInputContext.Input by letting callbacks update the live input directly. +/// +/// +/// The handle owns the live for the duration of the load callback. Setters are routed +/// back to the server-side input across the ATS boundary, which is why this is a handle rather than the by-value +/// InteractionInput DTO. +/// +[AspireExport] +internal sealed class InteractionLoadingInput +{ + private readonly LoadInputContext _inner; + + internal InteractionLoadingInput(LoadInputContext inner) + { + _inner = inner; + } + + /// + /// Gets the name of the input. + /// + /// The input name. + [AspireExport] + public string GetName() + { + return _inner.Input.Name; + } /// - /// Sets the choice options for the loading input. + /// Sets the choice options for the input. /// /// The available choices, in display order. Each option pairs a submitted value with a display label. [AspireExport] @@ -387,7 +428,7 @@ public void SetChoiceOptions(IReadOnlyList choices) } /// - /// Sets the value of the loading input. + /// Sets the value of the input. /// /// The value to assign. [AspireExport] diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 76e31852c12..ea5b8293dc7 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -61,6 +61,7 @@ Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext [ExposeP Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult [ExposeProperties] Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext +Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput Aspire.Hosting/Aspire.Hosting.DistributedApplication Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext [ExposeProperties] Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContextOptions @@ -475,12 +476,13 @@ Aspire.Hosting.ApplicationModel/UpdateCommandStateContext.resourceSnapshot(conte Aspire.Hosting.ApplicationModel/warning(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade, message: string) -> void Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.cancellationToken(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> cancellationToken Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.executionContext(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext -Aspire.Hosting.Ats/getInputName(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext) -> string Aspire.Hosting.Ats/getInputValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, inputName: string) -> string +Aspire.Hosting.Ats/getName(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput) -> string +Aspire.Hosting.Ats/input(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput Aspire.Hosting.Ats/InputsInteractionResult.canceled(context: Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult) -> boolean Aspire.Hosting.Ats/InputsInteractionResult.inputs(context: Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult) -> Aspire.Hosting/Aspire.Hosting.InteractionInputCollection -Aspire.Hosting.Ats/setChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> void -Aspire.Hosting.Ats/setValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, value: string) -> void +Aspire.Hosting.Ats/setChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> void +Aspire.Hosting.Ats/setValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput, value: string) -> void Aspire.Hosting.Ats/withChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting.Ats/withDynamicLoading(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, callback: callback, options?: Aspire.Hosting/Aspire.Hosting.Ats.DynamicLoadingOptions) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder Aspire.Hosting.Ats/withValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, value: string) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 759d950167e..2767ee788e1 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -16356,10 +16356,8 @@ func (s *interactionInputCollection) RequiredValue(name string) (string, error) // InteractionInputLoadContext is the public interface for handle type InteractionInputLoadContext. type InteractionInputLoadContext interface { handleReference - GetInputName() (string, error) GetInputValue(inputName string) (string, error) - SetChoiceOptions(choices []*InteractionChoiceOption) error - SetValue(value string) error + Input() InteractionLoadingInput Err() error } @@ -16373,14 +16371,15 @@ func newInteractionInputLoadContextFromHandle(h *handle, c *client) InteractionI return &interactionInputLoadContext{resourceBuilderBase: newResourceBuilderBase(h, c)} } -// GetInputName gets the name of the input that is loading. -func (s *interactionInputLoadContext) GetInputName() (string, error) { +// GetInputValue gets the current value of an input in the prompt by name. +func (s *interactionInputLoadContext) GetInputValue(inputName string) (string, error) { if s.err != nil { var zero string; return zero, s.err } ctx := context.Background() reqArgs := map[string]any{ "context": s.handle.ToJSON(), } - result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/getInputName", reqArgs) + reqArgs["inputName"] = serializeValue(inputName) + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/getInputValue", reqArgs) if err != nil { var zero string return zero, err @@ -16388,15 +16387,52 @@ func (s *interactionInputLoadContext) GetInputName() (string, error) { return decodeAs[string](result) } -// GetInputValue gets the current value of an input in the prompt by name. -func (s *interactionInputLoadContext) GetInputValue(inputName string) (string, error) { +// Input gets a handle to the input that is loading. Mutate the input through this handle. +func (s *interactionInputLoadContext) Input() InteractionLoadingInput { + if s.err != nil { return &interactionLoadingInput{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/input", reqArgs) + if err != nil { + return &interactionLoadingInput{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting.Ats/input returned unexpected type %T", result) + return &interactionLoadingInput{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionLoadingInput{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + +// InteractionLoadingInput is the public interface for handle type InteractionLoadingInput. +type InteractionLoadingInput interface { + handleReference + GetName() (string, error) + SetChoiceOptions(choices []*InteractionChoiceOption) error + SetValue(value string) error + Err() error +} + +// interactionLoadingInput is the unexported impl of InteractionLoadingInput. +type interactionLoadingInput struct { + *resourceBuilderBase +} + +// newInteractionLoadingInputFromHandle wraps an existing handle as InteractionLoadingInput. +func newInteractionLoadingInputFromHandle(h *handle, c *client) InteractionLoadingInput { + return &interactionLoadingInput{resourceBuilderBase: newResourceBuilderBase(h, c)} +} + +// GetName gets the name of the input. +func (s *interactionLoadingInput) GetName() (string, error) { if s.err != nil { var zero string; return zero, s.err } ctx := context.Background() reqArgs := map[string]any{ "context": s.handle.ToJSON(), } - reqArgs["inputName"] = serializeValue(inputName) - result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/getInputValue", reqArgs) + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/getName", reqArgs) if err != nil { var zero string return zero, err @@ -16404,8 +16440,8 @@ func (s *interactionInputLoadContext) GetInputValue(inputName string) (string, e return decodeAs[string](result) } -// SetChoiceOptions sets the choice options for the loading input. -func (s *interactionInputLoadContext) SetChoiceOptions(choices []*InteractionChoiceOption) error { +// SetChoiceOptions sets the choice options for the input. +func (s *interactionLoadingInput) SetChoiceOptions(choices []*InteractionChoiceOption) error { if s.err != nil { return s.err } ctx := context.Background() reqArgs := map[string]any{ @@ -16416,8 +16452,8 @@ func (s *interactionInputLoadContext) SetChoiceOptions(choices []*InteractionCho return err } -// SetValue sets the value of the loading input. -func (s *interactionInputLoadContext) SetValue(value string) error { +// SetValue sets the value of the input. +func (s *interactionLoadingInput) SetValue(value string) error { if s.err != nil { return s.err } ctx := context.Background() reqArgs := map[string]any{ @@ -27619,6 +27655,9 @@ func registerWrappers(c *client) { c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", func(h *handle, c *client) any { return newInteractionInputLoadContextFromHandle(h, c) }) + c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput", func(h *handle, c *client) any { + return newInteractionLoadingInputFromHandle(h, c) + }) c.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.IInteractionService", func(h *handle, c *client) any { return newInteractionServiceFromHandle(h, c) }) diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index cbc76a3c0dd..0a41afcfd6e 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1374,6 +1374,7 @@ public class AspireRegistrations { AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext", (h, c) -> new EventingSubscriberRegistrationContext(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", (h, c) -> new InteractionInputBuilder(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", (h, c) -> new InteractionInputLoadContext(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput", (h, c) -> new InteractionLoadingInput(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult", (h, c) -> new InputsInteractionResult(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.AfterResourcesCreatedEvent", (h, c) -> new AfterResourcesCreatedEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.BeforeResourceStartedEvent", (h, c) -> new BeforeResourceStartedEvent(h, c)); @@ -15356,12 +15357,12 @@ public class InteractionInputLoadContext extends HandleWrapperBase { super(handle, client); } - /** Gets the name of the input that is loading. */ - public String getInputName() { + /** Gets a handle to the input that is loading. Mutate the input through this handle. */ + public InteractionLoadingInput input() { Map reqArgs = new HashMap<>(); reqArgs.put("context", AspireClient.serializeValue(getHandle())); - var result = getClient().invokeCapability("Aspire.Hosting.Ats/getInputName", reqArgs); - return (String) result; + var result = getClient().invokeCapability("Aspire.Hosting.Ats/input", reqArgs); + return (InteractionLoadingInput) result; } /** Gets the current value of an input in the prompt by name. */ @@ -15373,22 +15374,6 @@ public String getInputValue(String inputName) { return (String) result; } - /** Sets the choice options for the loading input. */ - public void setChoiceOptions(InteractionChoiceOption[] choices) { - Map reqArgs = new HashMap<>(); - reqArgs.put("context", AspireClient.serializeValue(getHandle())); - reqArgs.put("choices", AspireClient.serializeValue(choices)); - getClient().invokeCapability("Aspire.Hosting.Ats/setChoiceOptions", reqArgs); - } - - /** Sets the value of the loading input. */ - public void setValue(String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("context", AspireClient.serializeValue(getHandle())); - reqArgs.put("value", AspireClient.serializeValue(value)); - getClient().invokeCapability("Aspire.Hosting.Ats/setValue", reqArgs); - } - } // ===== InteractionInputsDialogOptions.java ===== @@ -15453,6 +15438,46 @@ public Map toMap() { } } +// ===== InteractionLoadingInput.java ===== +// InteractionLoadingInput.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput. */ +public class InteractionLoadingInput extends HandleWrapperBase { + InteractionLoadingInput(Handle handle, AspireClient client) { + super(handle, client); + } + + /** Gets the name of the input. */ + public String getName() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + var result = getClient().invokeCapability("Aspire.Hosting.Ats/getName", reqArgs); + return (String) result; + } + + /** Sets the choice options for the input. */ + public void setChoiceOptions(InteractionChoiceOption[] choices) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + reqArgs.put("choices", AspireClient.serializeValue(choices)); + getClient().invokeCapability("Aspire.Hosting.Ats/setChoiceOptions", reqArgs); + } + + /** Sets the value of the input. */ + public void setValue(String value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + reqArgs.put("value", AspireClient.serializeValue(value)); + getClient().invokeCapability("Aspire.Hosting.Ats/setValue", reqArgs); + } + +} + // ===== InteractionMessageBoxOptions.java ===== // InteractionMessageBoxOptions.java - GENERATED CODE - DO NOT EDIT @@ -27188,6 +27213,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .aspire/modules/InteractionInputCollection.java .aspire/modules/InteractionInputLoadContext.java .aspire/modules/InteractionInputsDialogOptions.java +.aspire/modules/InteractionLoadingInput.java .aspire/modules/InteractionMessageBoxOptions.java .aspire/modules/InteractionNotificationOptions.java .aspire/modules/JsonSerializable.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 8807a0aa5ab..34277c915be 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -5367,14 +5367,14 @@ def handle(self) -> Handle: """The underlying object reference handle.""" return self._handle - def get_input_name(self) -> str: - """Gets the name of the input that is loading.""" + def input(self) -> InteractionLoadingInput: + """Gets a handle to the input that is loading. Mutate the input through this handle.""" rpc_args: dict[str, typing.Any] = {'context': self._handle} result = self._client.invoke_capability( - 'Aspire.Hosting.Ats/getInputName', + 'Aspire.Hosting.Ats/input', rpc_args, ) - return result + return typing.cast(InteractionLoadingInput, result) def get_input_value(self, input_name: str) -> str: """Gets the current value of an input in the prompt by name.""" @@ -5386,8 +5386,33 @@ def get_input_value(self, input_name: str) -> str: ) return result + +class InteractionLoadingInput: + """Type class for InteractionLoadingInput.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"InteractionLoadingInput(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + def get_name(self) -> str: + """Gets the name of the input.""" + rpc_args: dict[str, typing.Any] = {'context': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/getName', + rpc_args, + ) + return result + def set_choice_options(self, choices: typing.Iterable[InteractionChoiceOption]) -> None: - """Sets the choice options for the loading input.""" + """Sets the choice options for the input.""" rpc_args: dict[str, typing.Any] = {'context': self._handle} rpc_args['choices'] = choices self._client.invoke_capability( @@ -5396,7 +5421,7 @@ def set_choice_options(self, choices: typing.Iterable[InteractionChoiceOption]) ) def set_value(self, value: str) -> None: - """Sets the value of the loading input.""" + """Sets the value of the input.""" rpc_args: dict[str, typing.Any] = {'context': self._handle} rpc_args['value'] = value self._client.invoke_capability( @@ -12034,6 +12059,7 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder", InteractionInputBuilder) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.InteractionInputCollection", InteractionInputCollection) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext", InteractionInputLoadContext) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput", InteractionLoadingInput) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade", LogFacade) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineConfigurationContext", PipelineConfigurationContext) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineContext", PipelineContext) diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index db3c3a743f3..6db1e1c1bfa 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -12297,12 +12297,13 @@ impl InteractionInputLoadContext { &self.client } - /// Gets the name of the input that is loading. - pub fn get_input_name(&self) -> Result> { + /// Gets a handle to the input that is loading. Mutate the input through this handle. + pub fn input(&self) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("context".to_string(), self.handle.to_json()); - let result = self.client.invoke_capability("Aspire.Hosting.Ats/getInputName", args)?; - Ok(serde_json::from_value(result)?) + let result = self.client.invoke_capability("Aspire.Hosting.Ats/input", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionLoadingInput::new(handle, self.client.clone())) } /// Gets the current value of an input in the prompt by name. @@ -12313,8 +12314,42 @@ impl InteractionInputLoadContext { let result = self.client.invoke_capability("Aspire.Hosting.Ats/getInputValue", args)?; Ok(serde_json::from_value(result)?) } +} + +/// Wrapper for Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput +pub struct InteractionLoadingInput { + handle: Handle, + client: Arc, +} + +impl HasHandle for InteractionLoadingInput { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl InteractionLoadingInput { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } - /// Sets the choice options for the loading input. + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Gets the name of the input. + pub fn get_name(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.Ats/getName", args)?; + Ok(serde_json::from_value(result)?) + } + + /// Sets the choice options for the input. pub fn set_choice_options(&self, choices: Vec) -> Result<(), Box> { let mut args: HashMap = HashMap::new(); args.insert("context".to_string(), self.handle.to_json()); @@ -12323,7 +12358,7 @@ impl InteractionInputLoadContext { Ok(()) } - /// Sets the value of the loading input. + /// Sets the value of the input. pub fn set_value(&self, value: &str) -> Result<(), Box> { let mut args: HashMap = HashMap::new(); args.insert("context".to_string(), self.handle.to_json()); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index c3e7d60e372..6b3163161d5 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -334,9 +334,18 @@ type InputsInteractionResultHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.I */ type InteractionInputBuilderHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder'>; -/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the prompt, and provides guarded setters to update the loading input. */ +/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input as a handle and provides read access to the other inputs in the prompt. */ type InteractionInputLoadContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext'>; +/** + * A handle to the input currently being loaded by a dynamic-loading callback. Mirrors the native `LoadInputContext.Input` by letting callbacks update the live input directly. + * + * The handle owns the live `InteractionInput` for the duration of the load callback. Setters are routed + * back to the server-side input across the ATS boundary, which is why this is a handle rather than the by-value + * `InteractionInput` DTO. + */ +type InteractionLoadingInputHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput'>; + /** Represents a distributed application that implements the {@link IHost} and {@link IAsyncDisposable} interfaces. */ type DistributedApplicationHandle = Handle<'Aspire.Hosting/Aspire.Hosting.DistributedApplication'>; @@ -5871,81 +5880,88 @@ class InteractionInputBuilderPromiseImpl implements InteractionInputBuilderPromi // InteractionInputLoadContext // ============================================================================ -/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the prompt, and provides guarded setters to update the loading input. */ +/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input as a handle and provides read access to the other inputs in the prompt. */ export interface InteractionInputLoadContext { toJSON(): MarshalledHandle; /** - * Gets the name of the input that is loading. - * @returns The input name. + * Gets a handle to the input that is loading. Mutate the input through this handle. + * + * Mirrors the native `LoadInputContext.Input`: the callback updates the live input it is loading, rather than + * the context itself. The input is a handle (not a by-value DTO) so guarded setters route back to the server-side + * input across the ATS boundary. + * @returns A handle to the loading input. */ - getInputName(): Promise; + input(): InteractionLoadingInputPromise; /** * Gets the current value of an input in the prompt by name. + * + * Reads any input in the prompt, mirroring the native `LoadInputContext.AllInputs`. Use this to read the + * dependency inputs declared via `DependsOnInputs`. * @param inputName The name of the input to read. * @returns The input value, or an empty string when the input has no value or no input with that name exists. */ getInputValue(inputName: string): Promise; - /** - * Sets the choice options for the loading input. - * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. - */ - setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise; - /** - * Sets the value of the loading input. - * @param value The value to assign. - */ - setValue(value: string): InteractionInputLoadContextPromise; } export interface InteractionInputLoadContextPromise extends PromiseLike { /** - * Gets the name of the input that is loading. - * @returns The input name. + * Gets a handle to the input that is loading. Mutate the input through this handle. + * + * Mirrors the native `LoadInputContext.Input`: the callback updates the live input it is loading, rather than + * the context itself. The input is a handle (not a by-value DTO) so guarded setters route back to the server-side + * input across the ATS boundary. + * @returns A handle to the loading input. */ - getInputName(): Promise; + input(): InteractionLoadingInputPromise; /** * Gets the current value of an input in the prompt by name. + * + * Reads any input in the prompt, mirroring the native `LoadInputContext.AllInputs`. Use this to read the + * dependency inputs declared via `DependsOnInputs`. * @param inputName The name of the input to read. * @returns The input value, or an empty string when the input has no value or no input with that name exists. */ getInputValue(inputName: string): Promise; - /** - * Sets the choice options for the loading input. - * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. - */ - setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise; - /** - * Sets the value of the loading input. - * @param value The value to assign. - */ - setValue(value: string): InteractionInputLoadContextPromise; } // ============================================================================ // InteractionInputLoadContextImpl // ============================================================================ -/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input and the other inputs in the prompt, and provides guarded setters to update the loading input. */ +/** The context passed to a polyglot dynamic-loading callback. Exposes the loading input as a handle and provides read access to the other inputs in the prompt. */ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { constructor(private _handle: InteractionInputLoadContextHandle, private _client: AspireClientRpc) {} /** Serialize for JSON-RPC transport */ toJSON(): MarshalledHandle { return this._handle.toJSON(); } - /** - * Gets the name of the input that is loading. - * @returns The input name. - */ - async getInputName(): Promise { + /** @internal */ + async _inputInternal(): Promise { const rpcArgs: Record = { context: this._handle }; - return await this._client.invokeCapability( - 'Aspire.Hosting.Ats/getInputName', + const result = await this._client.invokeCapability( + 'Aspire.Hosting.Ats/input', rpcArgs ); + return new InteractionLoadingInputImpl(result, this._client); + } + + /** + * Gets a handle to the input that is loading. Mutate the input through this handle. + * + * Mirrors the native `LoadInputContext.Input`: the callback updates the live input it is loading, rather than + * the context itself. The input is a handle (not a by-value DTO) so guarded setters route back to the server-side + * input across the ATS boundary. + * @returns A handle to the loading input. + */ + input(): InteractionLoadingInputPromise { + return new InteractionLoadingInputPromiseImpl(this._inputInternal(), this._client); } /** * Gets the current value of an input in the prompt by name. + * + * Reads any input in the prompt, mirroring the native `LoadInputContext.AllInputs`. Use this to read the + * dependency inputs declared via `DependsOnInputs`. * @param inputName The name of the input to read. * @returns The input value, or an empty string when the input has no value or no input with that name exists. */ @@ -5957,8 +5973,112 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { ); } +} + +/** + * Thenable wrapper for InteractionInputLoadContext that enables fluent chaining. + */ +class InteractionInputLoadContextPromiseImpl implements InteractionInputLoadContextPromise { + constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { + if (track) { _client.trackPromise(_promise); } + } + + then( + onfulfilled?: ((value: InteractionInputLoadContext) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + input(): InteractionLoadingInputPromise { + return new InteractionLoadingInputPromiseImpl(this._promise.then(obj => obj.input()), this._client); + } + + getInputValue(inputName: string): Promise { + return this._promise.then(obj => obj.getInputValue(inputName)); + } + +} + +// ============================================================================ +// InteractionLoadingInput +// ============================================================================ + +/** + * A handle to the input currently being loaded by a dynamic-loading callback. Mirrors the native `LoadInputContext.Input` by letting callbacks update the live input directly. + * + * The handle owns the live `InteractionInput` for the duration of the load callback. Setters are routed + * back to the server-side input across the ATS boundary, which is why this is a handle rather than the by-value + * `InteractionInput` DTO. + */ +export interface InteractionLoadingInput { + toJSON(): MarshalledHandle; + /** + * Gets the name of the input. + * @returns The input name. + */ + getName(): Promise; + /** + * Sets the choice options for the input. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. + */ + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionLoadingInputPromise; + /** + * Sets the value of the input. + * @param value The value to assign. + */ + setValue(value: string): InteractionLoadingInputPromise; +} + +export interface InteractionLoadingInputPromise extends PromiseLike { + /** + * Gets the name of the input. + * @returns The input name. + */ + getName(): Promise; + /** + * Sets the choice options for the input. + * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. + */ + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionLoadingInputPromise; + /** + * Sets the value of the input. + * @param value The value to assign. + */ + setValue(value: string): InteractionLoadingInputPromise; +} + +// ============================================================================ +// InteractionLoadingInputImpl +// ============================================================================ + +/** + * A handle to the input currently being loaded by a dynamic-loading callback. Mirrors the native `LoadInputContext.Input` by letting callbacks update the live input directly. + * + * The handle owns the live `InteractionInput` for the duration of the load callback. Setters are routed + * back to the server-side input across the ATS boundary, which is why this is a handle rather than the by-value + * `InteractionInput` DTO. + */ +class InteractionLoadingInputImpl implements InteractionLoadingInput { + constructor(private _handle: InteractionLoadingInputHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** + * Gets the name of the input. + * @returns The input name. + */ + async getName(): Promise { + const rpcArgs: Record = { context: this._handle }; + return await this._client.invokeCapability( + 'Aspire.Hosting.Ats/getName', + rpcArgs + ); + } + /** @internal */ - async _setChoiceOptionsInternal(choices: InteractionChoiceOption[]): Promise { + async _setChoiceOptionsInternal(choices: InteractionChoiceOption[]): Promise { const rpcArgs: Record = { context: this._handle, choices }; await this._client.invokeCapability( 'Aspire.Hosting.Ats/setChoiceOptions', @@ -5968,15 +6088,15 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { } /** - * Sets the choice options for the loading input. + * Sets the choice options for the input. * @param choices The available choices, in display order. Each option pairs a submitted value with a display label. */ - setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise { - return new InteractionInputLoadContextPromiseImpl(this._setChoiceOptionsInternal(choices), this._client); + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionLoadingInputPromise { + return new InteractionLoadingInputPromiseImpl(this._setChoiceOptionsInternal(choices), this._client); } /** @internal */ - async _setValueInternal(value: string): Promise { + async _setValueInternal(value: string): Promise { const rpcArgs: Record = { context: this._handle, value }; await this._client.invokeCapability( 'Aspire.Hosting.Ats/setValue', @@ -5986,44 +6106,40 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { } /** - * Sets the value of the loading input. + * Sets the value of the input. * @param value The value to assign. */ - setValue(value: string): InteractionInputLoadContextPromise { - return new InteractionInputLoadContextPromiseImpl(this._setValueInternal(value), this._client); + setValue(value: string): InteractionLoadingInputPromise { + return new InteractionLoadingInputPromiseImpl(this._setValueInternal(value), this._client); } } /** - * Thenable wrapper for InteractionInputLoadContext that enables fluent chaining. + * Thenable wrapper for InteractionLoadingInput that enables fluent chaining. */ -class InteractionInputLoadContextPromiseImpl implements InteractionInputLoadContextPromise { - constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { +class InteractionLoadingInputPromiseImpl implements InteractionLoadingInputPromise { + constructor(private _promise: Promise, private _client: AspireClientRpc, track = true) { if (track) { _client.trackPromise(_promise); } } - then( - onfulfilled?: ((value: InteractionInputLoadContext) => TResult1 | PromiseLike) | null, + then( + onfulfilled?: ((value: InteractionLoadingInput) => TResult1 | PromiseLike) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null ): PromiseLike { return this._promise.then(onfulfilled, onrejected); } - getInputName(): Promise { - return this._promise.then(obj => obj.getInputName()); - } - - getInputValue(inputName: string): Promise { - return this._promise.then(obj => obj.getInputValue(inputName)); + getName(): Promise { + return this._promise.then(obj => obj.getName()); } - setChoiceOptions(choices: InteractionChoiceOption[]): InteractionInputLoadContextPromise { - return new InteractionInputLoadContextPromiseImpl(this._promise.then(obj => obj.setChoiceOptions(choices)), this._client); + setChoiceOptions(choices: InteractionChoiceOption[]): InteractionLoadingInputPromise { + return new InteractionLoadingInputPromiseImpl(this._promise.then(obj => obj.setChoiceOptions(choices)), this._client); } - setValue(value: string): InteractionInputLoadContextPromise { - return new InteractionInputLoadContextPromiseImpl(this._promise.then(obj => obj.setValue(value)), this._client); + setValue(value: string): InteractionLoadingInputPromise { + return new InteractionLoadingInputPromiseImpl(this._promise.then(obj => obj.setValue(value)), this._client); } } @@ -56464,6 +56580,7 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.InputsDialogValidationConte registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult', (handle, client) => new InputsInteractionResultImpl(handle as InputsInteractionResultHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder', (handle, client) => new InteractionInputBuilderImpl(handle as InteractionInputBuilderHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext', (handle, client) => new InteractionInputLoadContextImpl(handle as InteractionInputLoadContextHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput', (handle, client) => new InteractionLoadingInputImpl(handle as InteractionLoadingInputHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade', (handle, client) => new LogFacadeImpl(handle as LogFacadeHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineConfigurationContext', (handle, client) => new PipelineConfigurationContextImpl(handle as PipelineConfigurationContextHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Pipelines.PipelineContext', (handle, client) => new PipelineContextImpl(handle as PipelineContextHandle, client)); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 6cc060cb7ed..6108796b8b1 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -615,7 +615,7 @@ ENTRYPOINT ["dotnet", "App.dll"] if region == "eu" { zones = []*aspire.InteractionChoiceOption{{Value: "eu-west", Label: "EU West"}, {Value: "eu-north", Label: "EU North"}} } - _ = loadContext.SetChoiceOptions(zones) + _ = loadContext.Input().SetChoiceOptions(zones) }) result := interactionService.PromptInputs("Pick a zone", "Choose a region, then pick a zone from the dynamically loaded options.", []aspire.InteractionInputBuilder{regionInput, zoneInput}) @@ -705,14 +705,15 @@ ENTRYPOINT ["dotnet", "App.dll"] presetInput := interactionService.CreateTextInput("greeting").WithValue("hello") sizeInput := interactionService.CreateChoiceInput("size").WithChoiceOptions([]*aspire.InteractionChoiceOption{{Value: "s", Label: "Small"}, {Value: "l", Label: "Large"}}) dependentInput := interactionService.CreateChoiceInput("shade").WithDynamicLoading(func(loadContext aspire.InteractionInputLoadContext) { - inputName, _ := loadContext.GetInputName() + input := loadContext.Input() + inputName, _ := input.GetName() color, _ := loadContext.GetInputValue("color") shades := []*aspire.InteractionChoiceOption{{Value: "lime", Label: "Lime"}, {Value: "forest", Label: "Forest"}} if color == "r" { shades = []*aspire.InteractionChoiceOption{{Value: "crimson", Label: "Crimson"}, {Value: "scarlet", Label: "Scarlet"}} } - _ = loadContext.SetChoiceOptions(shades) - _ = loadContext.SetValue(inputName) + _ = input.SetChoiceOptions(shades) + _ = input.SetValue(inputName) }, &aspire.WithDynamicLoadingOptions{ Options: &aspire.DynamicLoadingOptions{AlwaysLoadOnStart: aspire.BoolPtr(true), DependsOnInputs: []string{"color"}}, }) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index 99d10654a6c..d62c1ea37a1 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -297,7 +297,7 @@ void main() throws Exception { InteractionChoiceOption[] zones = "eu".equals(region) ? new InteractionChoiceOption[] { opt("eu-west", "EU West"), opt("eu-north", "EU North") } : new InteractionChoiceOption[] { opt("us-east", "US East"), opt("us-west", "US West") }; - loadContext.setChoiceOptions(zones); + loadContext.input().setChoiceOptions(zones); }); var result = interactionService.promptInputs( @@ -383,13 +383,14 @@ void main() throws Exception { dynamicLoadingOptions.setAlwaysLoadOnStart(true); dynamicLoadingOptions.setDependsOnInputs(new String[] { "color" }); var dependentInput = interactionService.createChoiceInput("shade").withDynamicLoading((loadContext) -> { - var inputName = loadContext.getInputName(); + var input = loadContext.input(); + var inputName = input.getName(); var color = loadContext.getInputValue("color"); InteractionChoiceOption[] shades = "r".equals(color) ? new InteractionChoiceOption[] { opt("crimson", "Crimson"), opt("scarlet", "Scarlet") } : new InteractionChoiceOption[] { opt("lime", "Lime"), opt("forest", "Forest") }; - loadContext.setChoiceOptions(shades); - loadContext.setValue(inputName); + input.setChoiceOptions(shades); + input.setValue(inputName); }, dynamicLoadingOptions); var singleDialogOptions = new InteractionInputsDialogOptions(); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index 43c53705480..b3bb9da5fdb 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -467,7 +467,7 @@ def load_zones(load_context): zones = ([{"Value": "eu-west", "Label": "EU West"}, {"Value": "eu-north", "Label": "EU North"}] if region == "eu" else [{"Value": "us-east", "Label": "US East"}, {"Value": "us-west", "Label": "US West"}]) - load_context.set_choice_options(zones) + load_context.input().set_choice_options(zones) zone_input = interaction_service.create_choice_input("zone").with_dynamic_loading(load_zones) @@ -543,14 +543,15 @@ def interaction_showcase_command(ctx): size_input = interaction_service.create_choice_input("size").with_choice_options([{"Value": "s", "Label": "Small"}, {"Value": "l", "Label": "Large"}]) def load_shade(load_context): - input_name = load_context.get_input_name() + input = load_context.input() + input_name = input.get_name() color = load_context.get_input_value("color") - load_context.set_choice_options( + input.set_choice_options( [{"Value": "crimson", "Label": "Crimson"}, {"Value": "scarlet", "Label": "Scarlet"}] if color == "r" else [{"Value": "lime", "Label": "Lime"}, {"Value": "forest", "Label": "Forest"}] ) - load_context.set_value(input_name) + input.set_value(input_name) dependent_input = interaction_service.create_choice_input("shade").with_dynamic_loading( load_shade, diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index be2dde587ef..07628163fc5 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -784,7 +784,7 @@ await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { ? [{ value: "eu-west", label: "EU West" }, { value: "eu-north", label: "EU North" }] : [{ value: "us-east", label: "US East" }, { value: "us-west", label: "US West" }]; - await loadContext.setChoiceOptions(zones); + await loadContext.input().setChoiceOptions(zones); }); const result = await interactionService.promptInputs( @@ -850,13 +850,14 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn const dependentInput = await interactionService .createChoiceInput("shade") .withDynamicLoading(async (loadContext) => { - const inputName = await loadContext.getInputName(); + const input = loadContext.input(); + const inputName = await input.getName(); const color = await loadContext.getInputValue("color"); - await loadContext.setChoiceOptions(color === "r" + await input.setChoiceOptions(color === "r" ? [{ value: "crimson", label: "Crimson" }, { value: "scarlet", label: "Scarlet" }] : [{ value: "lime", label: "Lime" }, { value: "forest", label: "Forest" }]); - await loadContext.setValue(inputName); + await input.setValue(inputName); }, { alwaysLoadOnStart: true, dependsOnInputs: ["color"] From 280d208ef980330c34213e35aef13216f935528d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 9 Jun 2026 08:21:29 -0700 Subject: [PATCH 20/20] Replace load-context getInputValue with inputs() collection The dynamic-loading callback context exposed a bespoke getInputValue(name) to read other inputs by name. Every other callback context (validation, prompt results, command arguments) reads inputs through the shared InteractionInputCollection (inputs().value(name)), so the load context was the odd one out. Expose the all-inputs read as an Inputs property (via ExposeProperties) returning the native LoadInputContext.AllInputs collection. This mirrors the native LoadInputContext.AllInputs shape and unifies by-name reads across every callback context. It is a property rather than an [AspireExport] method so it routes through the generated collection accessor; a capability method returning the hand-written InteractionInputCollection would reference a non-existent InteractionInputCollectionImpl in the TypeScript output. input() (the single loading input handle) is unchanged and continues to mirror LoadInputContext.Input. Regenerated ats.txt and all five codegen snapshots, and updated the Go, Java, Python and TypeScript benches to loadContext.inputs().value(...). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Ats/InteractionExports.cs | 22 +++--- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 4 +- ...TwoPassScanningGeneratedAspire.verified.go | 37 +++++----- ...oPassScanningGeneratedAspire.verified.java | 17 +++-- ...TwoPassScanningGeneratedAspire.verified.py | 19 +++-- ...TwoPassScanningGeneratedAspire.verified.rs | 18 ++--- ...TwoPassScanningGeneratedAspire.verified.ts | 71 +++++++++---------- .../Aspire.Hosting/Go/apphost.go | 4 +- .../Aspire.Hosting/Java/AppHost.java | 4 +- .../Aspire.Hosting/Python/apphost.py | 4 +- .../Aspire.Hosting/TypeScript/apphost.mts | 4 +- 11 files changed, 98 insertions(+), 106 deletions(-) diff --git a/src/Aspire.Hosting/Ats/InteractionExports.cs b/src/Aspire.Hosting/Ats/InteractionExports.cs index f3f05506db8..b6f0c81936a 100644 --- a/src/Aspire.Hosting/Ats/InteractionExports.cs +++ b/src/Aspire.Hosting/Ats/InteractionExports.cs @@ -339,7 +339,7 @@ public InteractionInputBuilder WithDynamicLoading(Func -[AspireExport] +[AspireExport(ExposeProperties = true)] internal sealed class InteractionInputLoadContext { private readonly LoadInputContext _inner; @@ -367,21 +367,17 @@ public InteractionLoadingInput Input() } /// - /// Gets the current value of an input in the prompt by name. + /// Gets all inputs in the prompt, including the one currently loading. /// - /// The name of the input to read. - /// The input value, or an empty string when the input has no value or no input with that name exists. /// - /// Reads any input in the prompt, mirroring the native LoadInputContext.AllInputs. Use this to read the - /// dependency inputs declared via . + /// Mirrors the native LoadInputContext.AllInputs. Use the collection's by-name accessors (for example + /// value or requiredValue) to read the dependency inputs declared via + /// . This is the same + /// idiom used by the validation callback and prompt results, so reading inputs by name is consistent across every + /// callback context. This is exposed as a property (rather than a method) so it routes through the generated + /// collection accessor, matching the other contexts that surface an . /// - [AspireExport] - public string GetInputValue(string inputName) - { - ArgumentNullException.ThrowIfNull(inputName); - - return _inner.AllInputs.TryGetByName(inputName, out var input) ? input.Value ?? string.Empty : string.Empty; - } + public InteractionInputCollection Inputs => _inner.AllInputs; } /// diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index ea5b8293dc7..9289814010a 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -60,7 +60,7 @@ Aspire.Hosting/Aspire.Hosting.ApplicationModel.UpdateCommandStateContext [Expose Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext [ExposeProperties] Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult [ExposeProperties] Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder -Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext +Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext [ExposeProperties] Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput Aspire.Hosting/Aspire.Hosting.DistributedApplication Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext [ExposeProperties] @@ -476,11 +476,11 @@ Aspire.Hosting.ApplicationModel/UpdateCommandStateContext.resourceSnapshot(conte Aspire.Hosting.ApplicationModel/warning(context: Aspire.Hosting/Aspire.Hosting.ApplicationModel.LogFacade, message: string) -> void Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.cancellationToken(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> cancellationToken Aspire.Hosting.Ats/EventingSubscriberRegistrationContext.executionContext(context: Aspire.Hosting/Aspire.Hosting.Ats.EventingSubscriberRegistrationContext) -> Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext -Aspire.Hosting.Ats/getInputValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext, inputName: string) -> string Aspire.Hosting.Ats/getName(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput) -> string Aspire.Hosting.Ats/input(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput Aspire.Hosting.Ats/InputsInteractionResult.canceled(context: Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult) -> boolean Aspire.Hosting.Ats/InputsInteractionResult.inputs(context: Aspire.Hosting/Aspire.Hosting.Ats.InputsInteractionResult) -> Aspire.Hosting/Aspire.Hosting.InteractionInputCollection +Aspire.Hosting.Ats/InteractionInputLoadContext.inputs(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputLoadContext) -> Aspire.Hosting/Aspire.Hosting.InteractionInputCollection Aspire.Hosting.Ats/setChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> void Aspire.Hosting.Ats/setValue(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionLoadingInput, value: string) -> void Aspire.Hosting.Ats/withChoiceOptions(context: Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder, choices: Aspire.Hosting/Aspire.Hosting.Ats.InteractionChoiceOption[]) -> Aspire.Hosting/Aspire.Hosting.Ats.InteractionInputBuilder diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 2767ee788e1..bcd9937e2ae 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -16356,8 +16356,8 @@ func (s *interactionInputCollection) RequiredValue(name string) (string, error) // InteractionInputLoadContext is the public interface for handle type InteractionInputLoadContext. type InteractionInputLoadContext interface { handleReference - GetInputValue(inputName string) (string, error) Input() InteractionLoadingInput + Inputs() InteractionInputCollection Err() error } @@ -16371,22 +16371,6 @@ func newInteractionInputLoadContextFromHandle(h *handle, c *client) InteractionI return &interactionInputLoadContext{resourceBuilderBase: newResourceBuilderBase(h, c)} } -// GetInputValue gets the current value of an input in the prompt by name. -func (s *interactionInputLoadContext) GetInputValue(inputName string) (string, error) { - if s.err != nil { var zero string; return zero, s.err } - ctx := context.Background() - reqArgs := map[string]any{ - "context": s.handle.ToJSON(), - } - reqArgs["inputName"] = serializeValue(inputName) - result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/getInputValue", reqArgs) - if err != nil { - var zero string - return zero, err - } - return decodeAs[string](result) -} - // Input gets a handle to the input that is loading. Mutate the input through this handle. func (s *interactionInputLoadContext) Input() InteractionLoadingInput { if s.err != nil { return &interactionLoadingInput{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } @@ -16406,6 +16390,25 @@ func (s *interactionInputLoadContext) Input() InteractionLoadingInput { return &interactionLoadingInput{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} } +// Inputs gets all inputs in the prompt, including the one currently loading. +func (s *interactionInputLoadContext) Inputs() InteractionInputCollection { + if s.err != nil { return &interactionInputCollection{resourceBuilderBase: newErroredResourceBuilder(s.err, s.client)} } + ctx := context.Background() + reqArgs := map[string]any{ + "context": s.handle.ToJSON(), + } + result, err := s.client.invokeCapability(ctx, "Aspire.Hosting.Ats/InteractionInputLoadContext.inputs", reqArgs) + if err != nil { + return &interactionInputCollection{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + href, ok := result.(handleReference) + if !ok { + err := fmt.Errorf("aspire: Aspire.Hosting.Ats/InteractionInputLoadContext.inputs returned unexpected type %T", result) + return &interactionInputCollection{resourceBuilderBase: newErroredResourceBuilder(err, s.client)} + } + return &interactionInputCollection{resourceBuilderBase: newResourceBuilderBase(href.getHandle(), s.client)} +} + // InteractionLoadingInput is the public interface for handle type InteractionLoadingInput. type InteractionLoadingInput interface { handleReference diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 0a41afcfd6e..94d77364d36 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -15357,21 +15357,20 @@ public class InteractionInputLoadContext extends HandleWrapperBase { super(handle, client); } - /** Gets a handle to the input that is loading. Mutate the input through this handle. */ - public InteractionLoadingInput input() { + /** Gets all inputs in the prompt, including the one currently loading. */ + public InteractionInputCollection inputs() { Map reqArgs = new HashMap<>(); reqArgs.put("context", AspireClient.serializeValue(getHandle())); - var result = getClient().invokeCapability("Aspire.Hosting.Ats/input", reqArgs); - return (InteractionLoadingInput) result; + var result = getClient().invokeCapability("Aspire.Hosting.Ats/InteractionInputLoadContext.inputs", reqArgs); + return (InteractionInputCollection) result; } - /** Gets the current value of an input in the prompt by name. */ - public String getInputValue(String inputName) { + /** Gets a handle to the input that is loading. Mutate the input through this handle. */ + public InteractionLoadingInput input() { Map reqArgs = new HashMap<>(); reqArgs.put("context", AspireClient.serializeValue(getHandle())); - reqArgs.put("inputName", AspireClient.serializeValue(inputName)); - var result = getClient().invokeCapability("Aspire.Hosting.Ats/getInputValue", reqArgs); - return (String) result; + var result = getClient().invokeCapability("Aspire.Hosting.Ats/input", reqArgs); + return (InteractionLoadingInput) result; } } diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 34277c915be..ce4cf25303b 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -5367,6 +5367,15 @@ def handle(self) -> Handle: """The underlying object reference handle.""" return self._handle + @_cached_property + def inputs(self) -> InteractionInputCollection: + """Gets all inputs in the prompt, including the one currently loading.""" + result = self._client.invoke_capability( + 'Aspire.Hosting.Ats/InteractionInputLoadContext.inputs', + {'context': self._handle} + ) + return typing.cast(InteractionInputCollection, result) + def input(self) -> InteractionLoadingInput: """Gets a handle to the input that is loading. Mutate the input through this handle.""" rpc_args: dict[str, typing.Any] = {'context': self._handle} @@ -5376,16 +5385,6 @@ def input(self) -> InteractionLoadingInput: ) return typing.cast(InteractionLoadingInput, result) - def get_input_value(self, input_name: str) -> str: - """Gets the current value of an input in the prompt by name.""" - rpc_args: dict[str, typing.Any] = {'context': self._handle} - rpc_args['inputName'] = input_name - result = self._client.invoke_capability( - 'Aspire.Hosting.Ats/getInputValue', - rpc_args, - ) - return result - class InteractionLoadingInput: """Type class for InteractionLoadingInput.""" diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 6db1e1c1bfa..aeaafcc5ae3 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -12297,22 +12297,22 @@ impl InteractionInputLoadContext { &self.client } - /// Gets a handle to the input that is loading. Mutate the input through this handle. - pub fn input(&self) -> Result> { + /// Gets all inputs in the prompt, including the one currently loading. + pub fn inputs(&self) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("context".to_string(), self.handle.to_json()); - let result = self.client.invoke_capability("Aspire.Hosting.Ats/input", args)?; + let result = self.client.invoke_capability("Aspire.Hosting.Ats/InteractionInputLoadContext.inputs", args)?; let handle: Handle = serde_json::from_value(result)?; - Ok(InteractionLoadingInput::new(handle, self.client.clone())) + Ok(InteractionInputCollection::new(handle, self.client.clone())) } - /// Gets the current value of an input in the prompt by name. - pub fn get_input_value(&self, input_name: &str) -> Result> { + /// Gets a handle to the input that is loading. Mutate the input through this handle. + pub fn input(&self) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("context".to_string(), self.handle.to_json()); - args.insert("inputName".to_string(), serde_json::to_value(&input_name).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting.Ats/getInputValue", args)?; - Ok(serde_json::from_value(result)?) + let result = self.client.invoke_capability("Aspire.Hosting.Ats/input", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(InteractionLoadingInput::new(handle, self.client.clone())) } } diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 6b3163161d5..6157f5fdea6 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -5883,6 +5883,17 @@ class InteractionInputBuilderPromiseImpl implements InteractionInputBuilderPromi /** The context passed to a polyglot dynamic-loading callback. Exposes the loading input as a handle and provides read access to the other inputs in the prompt. */ export interface InteractionInputLoadContext { toJSON(): MarshalledHandle; + /** + * Gets all inputs in the prompt, including the one currently loading. + * + * Mirrors the native `LoadInputContext.AllInputs`. Use the collection's by-name accessors (for example + * `value` or `requiredValue`) to read the dependency inputs declared via + * `DependsOnInputs`. This is the same `InteractionInputCollection` + * idiom used by the validation callback and prompt results, so reading inputs by name is consistent across every + * callback context. This is exposed as a property (rather than a method) so it routes through the generated + * collection accessor, matching the other contexts that surface an `InteractionInputCollection`. + */ + inputs(): InteractionInputCollectionPromise; /** * Gets a handle to the input that is loading. Mutate the input through this handle. * @@ -5892,18 +5903,20 @@ export interface InteractionInputLoadContext { * @returns A handle to the loading input. */ input(): InteractionLoadingInputPromise; - /** - * Gets the current value of an input in the prompt by name. - * - * Reads any input in the prompt, mirroring the native `LoadInputContext.AllInputs`. Use this to read the - * dependency inputs declared via `DependsOnInputs`. - * @param inputName The name of the input to read. - * @returns The input value, or an empty string when the input has no value or no input with that name exists. - */ - getInputValue(inputName: string): Promise; } export interface InteractionInputLoadContextPromise extends PromiseLike { + /** + * Gets all inputs in the prompt, including the one currently loading. + * + * Mirrors the native `LoadInputContext.AllInputs`. Use the collection's by-name accessors (for example + * `value` or `requiredValue`) to read the dependency inputs declared via + * `DependsOnInputs`. This is the same `InteractionInputCollection` + * idiom used by the validation callback and prompt results, so reading inputs by name is consistent across every + * callback context. This is exposed as a property (rather than a method) so it routes through the generated + * collection accessor, matching the other contexts that surface an `InteractionInputCollection`. + */ + inputs(): InteractionInputCollectionPromise; /** * Gets a handle to the input that is loading. Mutate the input through this handle. * @@ -5913,15 +5926,6 @@ export interface InteractionInputLoadContextPromise extends PromiseLike; } // ============================================================================ @@ -5935,6 +5939,13 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { /** Serialize for JSON-RPC transport */ toJSON(): MarshalledHandle { return this._handle.toJSON(); } + inputs(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._client.invokeCapability( + 'Aspire.Hosting.Ats/InteractionInputLoadContext.inputs', + { context: this._handle } + ), this._client, false); + } + /** @internal */ async _inputInternal(): Promise { const rpcArgs: Record = { context: this._handle }; @@ -5957,22 +5968,6 @@ class InteractionInputLoadContextImpl implements InteractionInputLoadContext { return new InteractionLoadingInputPromiseImpl(this._inputInternal(), this._client); } - /** - * Gets the current value of an input in the prompt by name. - * - * Reads any input in the prompt, mirroring the native `LoadInputContext.AllInputs`. Use this to read the - * dependency inputs declared via `DependsOnInputs`. - * @param inputName The name of the input to read. - * @returns The input value, or an empty string when the input has no value or no input with that name exists. - */ - async getInputValue(inputName: string): Promise { - const rpcArgs: Record = { context: this._handle, inputName }; - return await this._client.invokeCapability( - 'Aspire.Hosting.Ats/getInputValue', - rpcArgs - ); - } - } /** @@ -5990,12 +5985,12 @@ class InteractionInputLoadContextPromiseImpl implements InteractionInputLoadCont return this._promise.then(onfulfilled, onrejected); } - input(): InteractionLoadingInputPromise { - return new InteractionLoadingInputPromiseImpl(this._promise.then(obj => obj.input()), this._client); + inputs(): InteractionInputCollectionPromise { + return new InteractionInputCollectionPromiseImpl(this._promise.then(obj => obj.inputs()), this._client, false); } - getInputValue(inputName: string): Promise { - return this._promise.then(obj => obj.getInputValue(inputName)); + input(): InteractionLoadingInputPromise { + return new InteractionLoadingInputPromiseImpl(this._promise.then(obj => obj.input()), this._client); } } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go index 6108796b8b1..d7370f70991 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go @@ -610,7 +610,7 @@ ENTRYPOINT ["dotnet", "App.dll"] }) zoneInput := interactionService.CreateChoiceInput("zone").WithDynamicLoading(func(loadContext aspire.InteractionInputLoadContext) { - region, _ := loadContext.GetInputValue("region") + region, _ := loadContext.Inputs().Value("region") zones := []*aspire.InteractionChoiceOption{{Value: "us-east", Label: "US East"}, {Value: "us-west", Label: "US West"}} if region == "eu" { zones = []*aspire.InteractionChoiceOption{{Value: "eu-west", Label: "EU West"}, {Value: "eu-north", Label: "EU North"}} @@ -707,7 +707,7 @@ ENTRYPOINT ["dotnet", "App.dll"] dependentInput := interactionService.CreateChoiceInput("shade").WithDynamicLoading(func(loadContext aspire.InteractionInputLoadContext) { input := loadContext.Input() inputName, _ := input.GetName() - color, _ := loadContext.GetInputValue("color") + color, _ := loadContext.Inputs().Value("color") shades := []*aspire.InteractionChoiceOption{{Value: "lime", Label: "Lime"}, {Value: "forest", Label: "Forest"}} if color == "r" { shades = []*aspire.InteractionChoiceOption{{Value: "crimson", Label: "Crimson"}, {Value: "scarlet", Label: "Scarlet"}} diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index d62c1ea37a1..ac2337339ed 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -293,7 +293,7 @@ void main() throws Exception { new CreateChoiceInputOptions().choices(new InteractionChoiceOption[] { opt("us", "United States"), opt("eu", "Europe") })); var zoneInput = interactionService.createChoiceInput("zone").withDynamicLoading((loadContext) -> { - var region = loadContext.getInputValue("region"); + var region = loadContext.inputs().value("region"); InteractionChoiceOption[] zones = "eu".equals(region) ? new InteractionChoiceOption[] { opt("eu-west", "EU West"), opt("eu-north", "EU North") } : new InteractionChoiceOption[] { opt("us-east", "US East"), opt("us-west", "US West") }; @@ -385,7 +385,7 @@ void main() throws Exception { var dependentInput = interactionService.createChoiceInput("shade").withDynamicLoading((loadContext) -> { var input = loadContext.input(); var inputName = input.getName(); - var color = loadContext.getInputValue("color"); + var color = loadContext.inputs().value("color"); InteractionChoiceOption[] shades = "r".equals(color) ? new InteractionChoiceOption[] { opt("crimson", "Crimson"), opt("scarlet", "Scarlet") } : new InteractionChoiceOption[] { opt("lime", "Lime"), opt("forest", "Forest") }; diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index b3bb9da5fdb..72a9f110cf4 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -463,7 +463,7 @@ def pick_zone_command(ctx): ) def load_zones(load_context): - region = load_context.get_input_value("region") + region = load_context.inputs().value("region") zones = ([{"Value": "eu-west", "Label": "EU West"}, {"Value": "eu-north", "Label": "EU North"}] if region == "eu" else [{"Value": "us-east", "Label": "US East"}, {"Value": "us-west", "Label": "US West"}]) @@ -545,7 +545,7 @@ def interaction_showcase_command(ctx): def load_shade(load_context): input = load_context.input() input_name = input.get_name() - color = load_context.get_input_value("color") + color = load_context.inputs().value("color") input.set_choice_options( [{"Value": "crimson", "Label": "Crimson"}, {"Value": "scarlet", "Label": "Scarlet"}] if color == "r" diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts index 07628163fc5..479fad095fe 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts @@ -778,7 +778,7 @@ await container.withCommand("pick-zone", "Pick Zone", async (ctx) => { const zoneInput = await interactionService .createChoiceInput("zone") .withDynamicLoading(async (loadContext) => { - const region = await loadContext.getInputValue("region"); + const region = await loadContext.inputs().value("region"); const zones: InteractionChoiceOption[] = region === "eu" ? [{ value: "eu-west", label: "EU West" }, { value: "eu-north", label: "EU North" }] @@ -852,7 +852,7 @@ await container.withCommand("interaction-showcase", "Interaction Showcase", asyn .withDynamicLoading(async (loadContext) => { const input = loadContext.input(); const inputName = await input.getName(); - const color = await loadContext.getInputValue("color"); + const color = await loadContext.inputs().value("color"); await input.setChoiceOptions(color === "r" ? [{ value: "crimson", label: "Crimson" }, { value: "scarlet", label: "Scarlet" }]