Last active
July 29, 2025 22:31
-
-
Save davidfowl/4ce69499b436f562a83e43c042d4d5f5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics.CodeAnalysis; | |
using Aspire.Hosting; | |
using Aspire.Hosting.ApplicationModel; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.DependencyInjection.Extensions; | |
using Microsoft.Extensions.Logging; | |
namespace WizardDemo.AppHost; | |
/// <summary> | |
/// Builder for creating interaction inputs in a fluent manner. | |
/// </summary> | |
[Experimental("WIZARD001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] | |
public sealed class InputsBuilder | |
{ | |
private readonly List<InteractionInput> _inputs = new(); | |
/// <summary> | |
/// Gets the built inputs. | |
/// </summary> | |
internal IReadOnlyList<InteractionInput> Inputs => _inputs; | |
/// <summary> | |
/// Adds a text input to the collection. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="placeholder">The placeholder text.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>The builder for method chaining.</returns> | |
public InputsBuilder AddText(string label, string? placeholder = null, bool required = false, string? value = null) | |
{ | |
_inputs.Add(WizardResourceBuilderExtensions.CreateTextInput(label, placeholder, required, value)); | |
return this; | |
} | |
/// <summary> | |
/// Adds a secret text input to the collection. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="placeholder">The placeholder text.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <returns>The builder for method chaining.</returns> | |
public InputsBuilder AddSecret(string label, string? placeholder = null, bool required = false) | |
{ | |
_inputs.Add(WizardResourceBuilderExtensions.CreateSecretInput(label, placeholder, required)); | |
return this; | |
} | |
/// <summary> | |
/// Adds a choice input to the collection. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="options">The options to choose from.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>The builder for method chaining.</returns> | |
public InputsBuilder AddChoice(string label, IReadOnlyList<KeyValuePair<string, string>> options, bool required = false, string? value = null) | |
{ | |
_inputs.Add(WizardResourceBuilderExtensions.CreateChoiceInput(label, options, required, value)); | |
return this; | |
} | |
/// <summary> | |
/// Adds a choice input to the collection using an array of options. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="options">The options to choose from.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>The builder for method chaining.</returns> | |
public InputsBuilder AddChoice(string label, KeyValuePair<string, string>[] options, bool required = false, string? value = null) | |
{ | |
_inputs.Add(WizardResourceBuilderExtensions.CreateChoiceInput(label, options, required, value)); | |
return this; | |
} | |
/// <summary> | |
/// Adds a boolean input to the collection. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>The builder for method chaining.</returns> | |
public InputsBuilder AddBoolean(string label, bool required = false, string? value = null) | |
{ | |
_inputs.Add(WizardResourceBuilderExtensions.CreateBooleanInput(label, required, value)); | |
return this; | |
} | |
/// <summary> | |
/// Adds a number input to the collection. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="placeholder">The placeholder text.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>The builder for method chaining.</returns> | |
public InputsBuilder AddNumber(string label, string? placeholder = null, bool required = false, string? value = null) | |
{ | |
_inputs.Add(WizardResourceBuilderExtensions.CreateNumberInput(label, placeholder, required, value)); | |
return this; | |
} | |
/// <summary> | |
/// Adds a custom interaction input to the collection. | |
/// </summary> | |
/// <param name="input">The interaction input to add.</param> | |
/// <returns>The builder for method chaining.</returns> | |
public InputsBuilder Add(InteractionInput input) | |
{ | |
ArgumentNullException.ThrowIfNull(input); | |
_inputs.Add(input); | |
return this; | |
} | |
} | |
/// <summary> | |
/// Extension methods for adding wizard resources to the distributed application builder. | |
/// </summary> | |
[Experimental("WIZARD001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] | |
public static class WizardResourceBuilderExtensions | |
{ | |
/// <summary> | |
/// Adds a wizard resource to the distributed application. | |
/// </summary> | |
/// <param name="builder">The distributed application builder.</param> | |
/// <param name="name">The name of the wizard resource.</param> | |
/// <returns>A resource builder for the wizard resource.</returns> | |
public static IResourceBuilder<WizardResource> AddWizard( | |
this IDistributedApplicationBuilder builder, | |
string name) | |
{ | |
ArgumentNullException.ThrowIfNull(builder); | |
ArgumentException.ThrowIfNullOrWhiteSpace(name); | |
var wizard = new WizardResource(name); | |
return builder.AddResource(wizard) | |
.ExcludeFromManifest() // Wizards don't need to be in the manifest | |
.WithInitialState(new CustomResourceSnapshot | |
{ | |
ResourceType = "Wizard", | |
CreationTimeStamp = DateTime.UtcNow, | |
State = KnownResourceStates.NotStarted, | |
IsHidden = true, // Wizards are not user-facing resources | |
Properties = [ | |
new(CustomResourceKnownProperties.Source, "Wizard") | |
] | |
}) | |
.OnInitializeResource(static async (resource, evt, ct) => | |
{ | |
await ExecuteWizardAsync(resource, evt, ct); | |
}); | |
} | |
/// <summary> | |
/// Executes the wizard steps for the given wizard resource. | |
/// </summary> | |
/// <param name="wizard">The wizard resource to execute.</param> | |
/// <param name="serviceProvider">The service provider.</param> | |
/// <param name="cancellationToken">The cancellation token.</param> | |
private static async Task ExecuteWizardAsync(WizardResource wizard, InitializeResourceEvent evt, CancellationToken cancellationToken) | |
{ | |
var serviceProvider = evt.Services; | |
var logger = evt.Logger; | |
if (!wizard.Steps.Any()) | |
{ | |
logger.LogInformation("No steps defined for wizard '{WizardName}'", wizard.Name); | |
return; | |
} | |
var interactionService = serviceProvider.GetRequiredService<IInteractionService>(); | |
if (!interactionService.IsAvailable) | |
{ | |
logger.LogWarning("Interaction service is not available. Skipping wizard execution for '{WizardName}'", wizard.Name); | |
return; | |
} | |
logger.LogInformation("Starting wizard execution for '{WizardName}'", wizard.Name); | |
var context = new WizardExecutionContext | |
{ | |
InteractionService = interactionService, | |
ServiceProvider = serviceProvider, | |
Logger = logger, | |
CancellationToken = cancellationToken | |
}; | |
var currentStepIndex = 0; | |
while (currentStepIndex < wizard.Steps.Count) | |
{ | |
var step = wizard.Steps[currentStepIndex]; | |
logger.LogInformation("Executing wizard step '{StepId}': {StepTitle}", step.Id, step.Title); | |
try | |
{ | |
var result = await step.ExecuteAsync(context); | |
if (!result.IsSuccess) | |
{ | |
logger.LogError("Wizard step '{StepId}' failed: {ErrorMessage}", step.Id, result.ErrorMessage); | |
// Show error to user if interaction service is available | |
await interactionService.PromptMessageBoxAsync( | |
"Wizard Error", | |
$"Step '{step.Title}' failed: {result.ErrorMessage}", | |
new MessageBoxInteractionOptions { Intent = MessageIntent.Error }, | |
cancellationToken); | |
break; | |
} | |
// Merge step data into shared context | |
if (result.Data != null) | |
{ | |
foreach (var kvp in result.Data) | |
{ | |
context.SharedData[kvp.Key] = kvp.Value; | |
} | |
} | |
if (!result.ContinueToNext) | |
{ | |
logger.LogInformation("Wizard execution stopped at step '{StepId}'", step.Id); | |
break; | |
} | |
currentStepIndex++; | |
} | |
catch (Exception ex) | |
{ | |
logger.LogError(ex, "Unexpected error executing wizard step '{StepId}'", step.Id); | |
// Show error to user if interaction service is available | |
await interactionService.PromptMessageBoxAsync( | |
"Wizard Error", | |
$"Unexpected error in step '{step.Title}': {ex.Message}", | |
new MessageBoxInteractionOptions { Intent = MessageIntent.Error }, | |
cancellationToken); | |
break; | |
} | |
} | |
logger.LogInformation("Completed wizard execution for '{WizardName}'", wizard.Name); | |
// Update the wizard state to Running | |
await evt.Notifications.PublishUpdateAsync(wizard, s => s with | |
{ | |
State = KnownResourceStates.Running | |
}); | |
} | |
/// <summary> | |
/// Adds an input step to the wizard. | |
/// </summary> | |
/// <param name="builder">The wizard resource builder.</param> | |
/// <param name="stepId">The unique identifier for the step.</param> | |
/// <param name="title">The title of the step.</param> | |
/// <param name="description">The description of the step.</param> | |
/// <param name="inputs">A callback to configure the inputs using the InputsBuilder.</param> | |
/// <param name="dataKey">The key to store the collected inputs in the shared data.</param> | |
/// <param name="validationCallback">The validation callback for the inputs.</param> | |
/// <returns>The wizard resource builder for method chaining.</returns> | |
public static IResourceBuilder<WizardResource> WithInputStep( | |
this IResourceBuilder<WizardResource> builder, | |
string stepId, | |
string title, | |
string? description = null, | |
Action<InputsBuilder>? inputs = null, | |
string? dataKey = null, | |
Func<InputsDialogValidationContext, Task>? validationCallback = null) | |
{ | |
ArgumentNullException.ThrowIfNull(builder); | |
ArgumentException.ThrowIfNullOrWhiteSpace(stepId); | |
ArgumentException.ThrowIfNullOrWhiteSpace(title); | |
var inputsBuilder = new InputsBuilder(); | |
inputs?.Invoke(inputsBuilder); | |
var step = new InputWizardStep | |
{ | |
Id = stepId, | |
Title = title, | |
Description = description, | |
Inputs = inputsBuilder.Inputs.ToList(), | |
DataKey = dataKey, | |
ValidationCallback = validationCallback | |
}; | |
builder.Resource.Steps.Add(step); | |
return builder; | |
} | |
/// <summary> | |
/// Adds a confirmation step to the wizard. | |
/// </summary> | |
/// <param name="builder">The wizard resource builder.</param> | |
/// <param name="stepId">The unique identifier for the step.</param> | |
/// <param name="title">The title of the step.</param> | |
/// <param name="message">The confirmation message to display.</param> | |
/// <param name="options">The options for the confirmation dialog.</param> | |
/// <returns>The wizard resource builder for method chaining.</returns> | |
public static IResourceBuilder<WizardResource> WithConfirmationStep( | |
this IResourceBuilder<WizardResource> builder, | |
string stepId, | |
string title, | |
string message, | |
MessageBoxInteractionOptions? options = null) | |
{ | |
ArgumentNullException.ThrowIfNull(builder); | |
ArgumentException.ThrowIfNullOrWhiteSpace(stepId); | |
ArgumentException.ThrowIfNullOrWhiteSpace(title); | |
ArgumentException.ThrowIfNullOrWhiteSpace(message); | |
var step = new ConfirmationWizardStep | |
{ | |
Id = stepId, | |
Title = title, | |
Message = message, | |
Options = options | |
}; | |
builder.Resource.Steps.Add(step); | |
return builder; | |
} | |
/// <summary> | |
/// Adds a message step to the wizard. | |
/// </summary> | |
/// <param name="builder">The wizard resource builder.</param> | |
/// <param name="stepId">The unique identifier for the step.</param> | |
/// <param name="title">The title of the step.</param> | |
/// <param name="message">The message to display.</param> | |
/// <param name="options">The options for the message dialog.</param> | |
/// <returns>The wizard resource builder for method chaining.</returns> | |
public static IResourceBuilder<WizardResource> WithMessageStep( | |
this IResourceBuilder<WizardResource> builder, | |
string stepId, | |
string title, | |
string message, | |
MessageBoxInteractionOptions? options = null) | |
{ | |
ArgumentNullException.ThrowIfNull(builder); | |
ArgumentException.ThrowIfNullOrWhiteSpace(stepId); | |
ArgumentException.ThrowIfNullOrWhiteSpace(title); | |
ArgumentException.ThrowIfNullOrWhiteSpace(message); | |
var step = new MessageWizardStep | |
{ | |
Id = stepId, | |
Title = title, | |
Message = message, | |
Options = options | |
}; | |
builder.Resource.Steps.Add(step); | |
return builder; | |
} | |
/// <summary> | |
/// Adds an action step to the wizard. | |
/// </summary> | |
/// <param name="builder">The wizard resource builder.</param> | |
/// <param name="stepId">The unique identifier for the step.</param> | |
/// <param name="title">The title of the step.</param> | |
/// <param name="action">The action to execute.</param> | |
/// <param name="description">The description of the step.</param> | |
/// <returns>The wizard resource builder for method chaining.</returns> | |
public static IResourceBuilder<WizardResource> WithActionStep( | |
this IResourceBuilder<WizardResource> builder, | |
string stepId, | |
string title, | |
Func<WizardExecutionContext, Task<WizardStepResult>> action, | |
string? description = null) | |
{ | |
ArgumentNullException.ThrowIfNull(builder); | |
ArgumentException.ThrowIfNullOrWhiteSpace(stepId); | |
ArgumentException.ThrowIfNullOrWhiteSpace(title); | |
ArgumentNullException.ThrowIfNull(action); | |
var step = new ActionWizardStep | |
{ | |
Id = stepId, | |
Title = title, | |
Description = description, | |
Action = action | |
}; | |
builder.Resource.Steps.Add(step); | |
return builder; | |
} | |
/// <summary> | |
/// Creates a text input for use in wizard steps. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="placeholder">The placeholder text.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>A text interaction input.</returns> | |
public static InteractionInput CreateTextInput( | |
string label, | |
string? placeholder = null, | |
bool required = false, | |
string? value = null) | |
{ | |
return new InteractionInput | |
{ | |
Label = label, | |
InputType = InputType.Text, | |
Required = required, | |
Placeholder = placeholder, | |
Value = value | |
}; | |
} | |
/// <summary> | |
/// Creates a secret text input for use in wizard steps. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="placeholder">The placeholder text.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <returns>A secret text interaction input.</returns> | |
public static InteractionInput CreateSecretInput( | |
string label, | |
string? placeholder = null, | |
bool required = false) | |
{ | |
return new InteractionInput | |
{ | |
Label = label, | |
InputType = InputType.SecretText, | |
Required = required, | |
Placeholder = placeholder | |
}; | |
} | |
/// <summary> | |
/// Creates a choice input for use in wizard steps. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="options">The options to choose from.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>A choice interaction input.</returns> | |
public static InteractionInput CreateChoiceInput( | |
string label, | |
IReadOnlyList<KeyValuePair<string, string>> options, | |
bool required = false, | |
string? value = null) | |
{ | |
return new InteractionInput | |
{ | |
Label = label, | |
InputType = InputType.Choice, | |
Options = options, | |
Required = required, | |
Value = value | |
}; | |
} | |
/// <summary> | |
/// Creates a boolean input for use in wizard steps. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>A boolean interaction input.</returns> | |
public static InteractionInput CreateBooleanInput( | |
string label, | |
bool required = false, | |
string? value = null) | |
{ | |
return new InteractionInput | |
{ | |
Label = label, | |
InputType = InputType.Boolean, | |
Required = required, | |
Value = value | |
}; | |
} | |
/// <summary> | |
/// Creates a number input for use in wizard steps. | |
/// </summary> | |
/// <param name="label">The label for the input.</param> | |
/// <param name="placeholder">The placeholder text.</param> | |
/// <param name="required">Whether the input is required.</param> | |
/// <param name="value">The initial value.</param> | |
/// <returns>A number interaction input.</returns> | |
public static InteractionInput CreateNumberInput( | |
string label, | |
string? placeholder = null, | |
bool required = false, | |
string? value = null) | |
{ | |
return new InteractionInput | |
{ | |
Label = label, | |
InputType = InputType.Number, | |
Required = required, | |
Placeholder = placeholder, | |
Value = value | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment