Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active July 29, 2025 22:31
Show Gist options
  • Save davidfowl/4ce69499b436f562a83e43c042d4d5f5 to your computer and use it in GitHub Desktop.
Save davidfowl/4ce69499b436f562a83e43c042d4d5f5 to your computer and use it in GitHub Desktop.
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