File: ApplicationModel\ResourceCommandAnnotation.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using HealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Represents a command annotation for a resource.
/// </summary>
[DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}")]
public sealed class ResourceCommandAnnotation : IResourceAnnotation
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ResourceCommandAnnotation"/> class.
    /// </summary>
    public ResourceCommandAnnotation(
        string name,
        string displayName,
        Func<UpdateCommandStateContext, ResourceCommandState> updateState,
        Func<ExecuteCommandContext, Task<ExecuteCommandResult>> executeCommand,
        string? displayDescription,
        object? parameter,
        string? confirmationMessage,
        string? iconName,
        IconVariant? iconVariant,
        bool isHighlighted)
        : this(name, displayName, updateState, executeCommand, displayDescription, parameter, arguments: null, confirmationMessage: confirmationMessage, iconName: iconName, iconVariant: iconVariant, isHighlighted: isHighlighted, visibility: ResourceCommandVisibility.UI | ResourceCommandVisibility.Api, validateArguments: null)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ResourceCommandAnnotation"/> class.
    /// </summary>
    public ResourceCommandAnnotation(
        string name,
        string displayName,
        Func<UpdateCommandStateContext, ResourceCommandState> updateState,
        Func<ExecuteCommandContext, Task<ExecuteCommandResult>> executeCommand,
        string? displayDescription,
        IReadOnlyList<InteractionInput>? arguments,
        string? confirmationMessage,
        string? iconName,
        IconVariant? iconVariant,
        bool isHighlighted,
        ResourceCommandVisibility visibility = ResourceCommandVisibility.UI | ResourceCommandVisibility.Api,
        Func<InputsDialogValidationContext, Task>? validateArguments = null)
        : this(name, displayName, updateState, executeCommand, displayDescription, parameter: null, arguments: arguments, confirmationMessage: confirmationMessage, iconName: iconName, iconVariant: iconVariant, isHighlighted: isHighlighted, visibility: visibility, validateArguments: validateArguments)
    {
    }
 
    internal ResourceCommandAnnotation(
        string name,
        string displayName,
        Func<UpdateCommandStateContext, ResourceCommandState> updateState,
        Func<ExecuteCommandContext, Task<ExecuteCommandResult>> executeCommand,
        string? displayDescription,
        object? parameter,
        IReadOnlyList<InteractionInput>? arguments,
        string? confirmationMessage,
        string? iconName,
        IconVariant? iconVariant,
        bool isHighlighted,
        ResourceCommandVisibility visibility = ResourceCommandVisibility.UI | ResourceCommandVisibility.Api,
        Func<InputsDialogValidationContext, Task>? validateArguments = null,
        CommandProgressOptions? progress = null)
    {
        ArgumentNullException.ThrowIfNull(name);
        ArgumentNullException.ThrowIfNull(displayName);
        ArgumentNullException.ThrowIfNull(updateState);
        ArgumentNullException.ThrowIfNull(executeCommand);
 
        Name = name;
        DisplayName = displayName;
        UpdateState = updateState;
        ExecuteCommand = executeCommand;
        DisplayDescription = displayDescription;
#pragma warning disable CS0618 // Parameter is obsolete but still stored for compatibility.
        Parameter = parameter;
#pragma warning restore CS0618
        Arguments = arguments ?? [];
        ValidateArguments = validateArguments;
        ConfirmationMessage = confirmationMessage;
        IconName = iconName;
        IconVariant = iconVariant;
        IsHighlighted = isHighlighted;
        Visibility = visibility;
        Progress = progress;
    }
 
    /// <summary>
    /// The name of command. The name uniquely identifies the command.
    /// </summary>
    public string Name { get; }
 
    /// <summary>
    /// The display name visible in UI.
    /// </summary>
    public string DisplayName { get; }
 
    /// <summary>
    /// A callback that is used to update the command state.
    /// The callback is executed when the command's resource snapshot is updated.
    /// </summary>
    public Func<UpdateCommandStateContext, ResourceCommandState> UpdateState { get; }
 
    /// <summary>
    /// A callback that is executed when the command is executed.
    /// The result is used to indicate success or failure in the UI.
    /// </summary>
    public Func<ExecuteCommandContext, Task<ExecuteCommandResult>> ExecuteCommand { get; }
 
    /// <summary>
    /// Optional description of the command, to be shown in the UI.
    /// Could be used as a tooltip. May be localized.
    /// </summary>
    public string? DisplayDescription { get; }
 
    /// <summary>
    /// Obsolete optional parameter that configures the command in some way.
    /// Clients must return any value provided by the server when invoking the command.
    /// </summary>
    [Obsolete("Use Arguments to describe invocation arguments and ExecuteCommandContext.Arguments to read them.")]
    public object? Parameter { get; }
 
    /// <summary>
    /// Gets the invocation arguments accepted by the command.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The list order is part of the command contract. CLI positional arguments are mapped to this list by index before the
    /// command executes. Clients that submit named argument payloads, such as Dashboard and MCP clients, map values by
    /// <see cref="InteractionInput.Name"/>.
    /// </para>
    /// </remarks>
    public IReadOnlyList<InteractionInput> Arguments { get; }
 
    /// <summary>
    /// Gets the callback that validates invocation arguments before the command callback is executed.
    /// </summary>
    public Func<InputsDialogValidationContext, Task>? ValidateArguments { get; }
 
    /// <summary>
    /// When a confirmation message is specified, the UI will prompt with an OK/Cancel dialog
    /// and the confirmation message before starting the command.
    /// </summary>
    public string? ConfirmationMessage { get; }
 
    /// <summary>
    /// The icon name for the command. The name should be a valid FluentUI icon name. https://aka.ms/fluentui-system-icons
    /// </summary>
    public string? IconName { get; }
 
    /// <summary>
    /// The icon variant for the command.
    /// </summary>
    public IconVariant? IconVariant { get; }
 
    /// <summary>
    /// A flag indicating whether the command is highlighted in the UI.
    /// </summary>
    public bool IsHighlighted { get; }
 
    /// <summary>
    /// Gets where the command is visible to users and clients.
    /// </summary>
    public ResourceCommandVisibility Visibility { get; }
 
    /// <summary>
    /// Gets the progress dialog options for the command.
    /// When <see langword="null"/>, no progress dialog is shown.
    /// </summary>
    public CommandProgressOptions? Progress { get; }
}
 
/// <summary>
/// The icon variant.
/// </summary>
public enum IconVariant
{
    /// <summary>
    /// Regular variant of icons.
    /// </summary>
    Regular,
    /// <summary>
    /// Filled variant of icons.
    /// </summary>
    Filled
}
 
/// <summary>
/// Specifies the format of a command result.
/// </summary>
public enum CommandResultFormat
{
    /// <summary>
    /// Plain text result.
    /// </summary>
    Text,
 
    /// <summary>
    /// JSON result.
    /// </summary>
    Json,
 
    /// <summary>
    /// Markdown result.
    /// </summary>
    Markdown
}
 
/// <summary>
/// A factory for <see cref="ExecuteCommandResult"/>.
/// </summary>
public static class CommandResults
{
    /// <summary>
    /// Produces a success result.
    /// </summary>
    public static ExecuteCommandResult Success() => new() { Success = true };
 
    /// <summary>
    /// Produces a success result.
    /// </summary>
    public static ExecuteCommandResult Success(string message) => new() { Success = true, Message = message };
 
    /// <summary>
    /// Produces a success result with a message and result data.
    /// </summary>
    /// <param name="message">The message associated with the result.</param>
    /// <param name="result">The result data.</param>
    /// <param name="resultFormat">The format of the result data. Defaults to <see cref="CommandResultFormat.Text"/>.</param>
    public static ExecuteCommandResult Success(string message, string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = true, Message = message, Data = new CommandResultData { Value = result, Format = resultFormat } };
 
    /// <summary>
    /// Produces a success result with a message and result data.
    /// </summary>
    /// <param name="message">The message associated with the result.</param>
    /// <param name="result">The result data.</param>
    /// <param name="resultFormat">The format of the result data.</param>
    /// <param name="displayImmediately">A value indicating whether the result data should be displayed immediately in the dashboard.</param>
    /// <remarks>
    /// When <paramref name="displayImmediately"/> is <see langword="true"/>, the dashboard opens the result dialog
    /// automatically when the command completes. Other clients can still read the result data from
    /// <see cref="ExecuteCommandResult.Data"/>.
    /// </remarks>
    public static ExecuteCommandResult Success(string message, string result, CommandResultFormat resultFormat, bool displayImmediately) => new() { Success = true, Message = message, Data = new CommandResultData { Value = result, Format = resultFormat, DisplayImmediately = displayImmediately } };
 
    /// <summary>
    /// Produces a success result with a message and a value.
    /// </summary>
    /// <param name="message">The message associated with the result.</param>
    /// <param name="value">The value produced by the command.</param>
    public static ExecuteCommandResult Success(string message, CommandResultData value) => new() { Success = true, Message = message, Data = value };
 
    /// <summary>
    /// Produces an unsuccessful result with an error message.
    /// </summary>
    /// <param name="errorMessage">An optional error message.</param>
    public static ExecuteCommandResult Failure(string? errorMessage = null) => new() { Success = false, Message = errorMessage };
 
    /// <summary>
    /// Produces an unsuccessful result with an error message and result data.
    /// </summary>
    /// <param name="errorMessage">The error message.</param>
    /// <param name="result">The result data.</param>
    /// <param name="resultFormat">The format of the result data. Defaults to <see cref="CommandResultFormat.Text"/>.</param>
    public static ExecuteCommandResult Failure(string errorMessage, string result, CommandResultFormat resultFormat = CommandResultFormat.Text) => new() { Success = false, Message = errorMessage, Data = new CommandResultData { Value = result, Format = resultFormat } };
 
    /// <summary>
    /// Produces an unsuccessful result with an error message and a value.
    /// </summary>
    /// <param name="errorMessage">The error message.</param>
    /// <param name="value">The value produced by the command.</param>
    public static ExecuteCommandResult Failure(string errorMessage, CommandResultData value) => new() { Success = false, Message = errorMessage, Data = value };
 
    /// <summary>
    /// Produces a canceled result.
    /// </summary>
    public static ExecuteCommandResult Canceled() => new() { Success = false, Canceled = true };
 
    /// <summary>
    /// Produces an unsuccessful result from an <see cref="Exception"/>. <see cref="Exception.Message"/> is used as the error message.
    /// </summary>
    /// <param name="exception">The exception to get the error message from.</param>
    public static ExecuteCommandResult Failure(Exception exception) => Failure(exception.Message);
}
 
/// <summary>
/// The result of executing a command. Returned from <see cref="ResourceCommandAnnotation.ExecuteCommand"/>.
/// </summary>
[AspireDto]
public sealed class ExecuteCommandResult
{
    /// <summary>
    /// A flag that indicates whether the command was successful.
    /// </summary>
    public required bool Success { get; init; }
 
    /// <summary>
    /// A flag that indicates whether the command was canceled by the user.
    /// </summary>
    public bool Canceled { get; init; }
 
    /// <summary>
    /// An optional error message that can be set when the command is unsuccessful.
    /// </summary>
    [Obsolete("Use Message instead.")]
    public string? ErrorMessage
    {
        get => _message;
        init => _message ??= value;
    }
 
    /// <summary>
    /// An optional message associated with the command result.
    /// </summary>
    public string? Message
    {
        get => _message;
        init => _message = value;
    }
 
    private string? _message;
 
    /// <summary>
    /// An optional value produced by the command.
    /// </summary>
    public CommandResultData? Data { get; init; }
 
    internal InteractionInputCollection? InvalidArguments { get; init; }
}
 
/// <summary>
/// Represents a value produced by a command.
/// </summary>
[AspireDto]
public sealed class CommandResultData
{
    /// <summary>
    /// The value data.
    /// </summary>
    public required string Value { get; init; }
 
    /// <summary>
    /// The format of the <see cref="Value"/> data.
    /// </summary>
    public CommandResultFormat Format { get; init; }
 
    /// <summary>
    /// When <see langword="true"/>, the dashboard will immediately display the value in a dialog when the command completes.
    /// </summary>
    public bool DisplayImmediately { get; init; }
}
 
/// <summary>
/// Context for <see cref="ResourceCommandAnnotation.UpdateState"/>.
/// </summary>
/// <ats-summary>Context for <ats-see cref="!:method:ResourceCommandAnnotation.UpdateState" />.</ats-summary>
[AspireExport(ExposeProperties = true)]
public sealed class UpdateCommandStateContext
{
    /// <summary>
    /// The resource snapshot.
    /// </summary>
    [AspireExportIgnore(Reason = "CustomResourceSnapshot contains object-valued properties that are not statically representable in polyglot SDKs. Use ResourceSnapshotData for the curated ATS projection.")]
    public required CustomResourceSnapshot ResourceSnapshot { get; init; }
 
    /// <summary>
    /// Gets the resource snapshot data available to polyglot command state callbacks.
    /// </summary>
    [AspireExport(MethodName = "resourceSnapshot")]
    internal UpdateCommandStateResourceSnapshot ResourceSnapshotData => UpdateCommandStateResourceSnapshot.FromSnapshot(ResourceSnapshot);
 
    /// <summary>
    /// The service provider.
    /// </summary>
    [Obsolete("Use Services instead.")]
    [AspireExportIgnore(Reason = "Obsolete alias for Services. The service provider is exposed to polyglot hosts via Services (services).")]
    public IServiceProvider ServiceProvider
    {
        get => Services;
        init => Services = value;
    }
 
    /// <summary>
    /// The service provider.
    /// </summary>
    public required IServiceProvider Services { get; init; }
}
 
/// <summary>
/// Resource snapshot data exposed to polyglot command state callbacks.
/// </summary>
[AspireDto]
internal sealed class UpdateCommandStateResourceSnapshot
{
    /// <summary>
    /// The type of the resource.
    /// </summary>
    public required string ResourceType { get; init; }
 
    /// <summary>
    /// The current lifecycle state text for the resource.
    /// </summary>
    public string? State { get; init; }
 
    /// <summary>
    /// The display style for the current lifecycle state.
    /// </summary>
    public string? StateStyle { get; init; }
 
    /// <summary>
    /// The current health status for the resource.
    /// </summary>
    public HealthStatus? HealthStatus { get; init; }
 
    /// <summary>
    /// The exit code of the resource.
    /// </summary>
    public int? ExitCode { get; init; }
 
    internal static UpdateCommandStateResourceSnapshot FromSnapshot(CustomResourceSnapshot snapshot)
    {
        return new()
        {
            ResourceType = snapshot.ResourceType,
            State = snapshot.State?.Text,
            StateStyle = snapshot.State?.Style,
            HealthStatus = snapshot.HealthStatus,
            ExitCode = snapshot.ExitCode
        };
    }
}
 
/// <summary>
/// Context for <see cref="ResourceCommandAnnotation.ExecuteCommand"/>.
/// </summary>
/// <ats-summary>Context for <ats-see cref="!:method:ResourceCommandAnnotation.ExecuteCommand" />.</ats-summary>
[AspireExport(ExposeProperties = true)]
public sealed class ExecuteCommandContext
{
    /// <summary>
    /// The service provider.
    /// </summary>
    [Obsolete("Use Services instead.")]
    [AspireExportIgnore(Reason = "Obsolete alias for Services. The service provider is exposed to polyglot hosts via Services (services).")]
    public IServiceProvider ServiceProvider
    {
        get => Services;
        init => Services = value;
    }
 
    /// <summary>
    /// The service provider.
    /// </summary>
    public required IServiceProvider Services { get; init; }
 
    /// <summary>
    /// The resource name.
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <summary>
    /// The cancellation token.
    /// </summary>
    public required CancellationToken CancellationToken { get; init; }
 
    /// <summary>
    /// The logger for the resource.
    /// </summary>
    public required ILogger Logger { get; init; }
 
    /// <summary>
    /// Gets the invocation arguments supplied by the client when the command is executed.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The collection contains the arguments described by <see cref="ResourceCommandAnnotation.Arguments"/> with their
    /// submitted values populated. CLI positional arguments are mapped by declaration order. Dashboard, MCP, and other
    /// named-payload clients are mapped by <see cref="InteractionInput.Name"/>.
    /// </para>
    /// </remarks>
    public required InteractionInputCollection Arguments { get; init; }
 
}