跳转到内容
Docs Try Aspire
Docs Try

Custom resource commands

此内容尚不支持你的语言。

Each resource in the Aspire app model is represented as an IResource and when added to the distributed application builder, it’s the generic-type parameter of the IResourceBuilder<T> interface. You use the resource builder API to chain calls, configuring the underlying resource, and in some situations, you might want to add custom commands to the resource. Some common scenario for creating a custom command might be running database migrations or seeding/resetting a database. In this article, you learn how to add a custom command to a Redis resource that clears the cache.

Start with an Aspire AppHost project. The walkthrough below registers a clear-cache command on a Redis resource. The C# version uses an extension method that wraps the registration; the TypeScript version registers the command inline against a Node service that exposes an admin endpoint. Both flows produce the same dashboard/CLI experience.

Add a new class named RedisResourceBuilderExtensions.cs to the AppHost project and replace its contents with the following code:

RedisResourceBuilderExtensions.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace Aspire.Hosting;
internal static class RedisResourceBuilderExtensions
{
public static IResourceBuilder<RedisResource> WithClearCommand(
this IResourceBuilder<RedisResource> builder)
{
var commandOptions = new CommandOptions
{
UpdateState = OnUpdateResourceState,
IconName = "AnimalRabbitOff",
IconVariant = IconVariant.Filled
};
builder.WithCommand(
name: "clear-cache",
displayName: "Clear Cache",
executeCommand: context => OnRunClearCacheCommandAsync(builder, context),
commandOptions: commandOptions);
return builder;
}
private static async Task<ExecuteCommandResult> OnRunClearCacheCommandAsync(
IResourceBuilder<RedisResource> builder,
ExecuteCommandContext context)
{
var connectionString = await builder.Resource.GetConnectionStringAsync() ??
throw new InvalidOperationException(
$"Unable to get the '{context.ResourceName}' connection string.");
await using var connection = ConnectionMultiplexer.Connect(connectionString);
var database = connection.GetDatabase();
await database.ExecuteAsync("FLUSHALL");
return CommandResults.Success();
}
private static ResourceCommandState OnUpdateResourceState(
UpdateCommandStateContext context)
{
var logger = context.ServiceProvider.GetRequiredService<ILogger<Program>>();
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation(
"Updating resource state: {ResourceSnapshot}",
context.ResourceSnapshot);
}
return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy
? ResourceCommandState.Enabled
: ResourceCommandState.Disabled;
}
}

The preceding code:

  • Shares the Aspire.Hosting namespace so that the extension is visible from the AppHost.
  • Is a static class containing extension methods on IResourceBuilder<RedisResource>.
  • Defines a single extension method named WithClearCommand that registers a clear-cache command on the Redis resource, plus the executeCommand and updateState callbacks the dashboard uses to run the command and decide when it’s enabled.
  • Returns the IResourceBuilder<RedisResource> to keep the call site chainable.

The WithCommand API adds the appropriate annotations to the resource, which are consumed in the Aspire dashboard. The dashboard uses these annotations to render the command in the UI. Before getting too far into those details, let’s ensure that you first understand the parameters of the WithCommand method:

  • name: The name of the command to invoke.
  • displayName: The name of the command to display in the dashboard.
  • executeCommand: The callback that runs when the command is invoked. In C# the type is Func<ExecuteCommandContext, Task<ExecuteCommandResult>>; in TypeScript it’s (context: ExecuteCommandContext) => Promise<ExecuteCommandResult>.
  • updateState: A callback that decides the command’s enabled state in the dashboard. In C# it’s Func<UpdateCommandStateContext, ResourceCommandState> and supplied via CommandOptions.UpdateState; in TypeScript it’s (context: UpdateCommandStateContext) => Promise<ResourceCommandState> and supplied through the updateState field on CommandOptions.
  • iconName: The name of the icon to display in the dashboard. The icon is optional, but when you do provide it, it should be a valid Fluent UI Blazor icon name.
  • iconVariant: The variant of the icon to display in the dashboard, valid options are Regular (default) or Filled.

The executeCommand callback is where the command logic is implemented. The ExecuteCommandContext it receives gives you access to command-specific data such as the resource being acted on, cancellation, and logging.

In C# the parameter is typed as Func<ExecuteCommandContext, Task<ExecuteCommandResult>>. ExecuteCommandContext exposes:

  • ServiceProvider (IServiceProvider) — used to resolve services such as loggers or ResourceCommandService.
  • ResourceName (string) — the name of the resource instance that the command is being executed on.
  • CancellationToken (CancellationToken) — used to cancel command execution.
  • Logger (ILogger) — used to write log messages directly to the resource’s logs. These logs can be viewed in the Aspire dashboard or with the aspire logs <resource> command.

The earlier example delegates to a private static method named OnRunClearCacheCommandAsync to clear the cache:

RedisResourceBuilderExtensions.cs
private static async Task<ExecuteCommandResult> OnRunClearCacheCommandAsync(
IResourceBuilder<RedisResource> builder,
ExecuteCommandContext context)
{
var connectionString = await builder.Resource.GetConnectionStringAsync() ??
throw new InvalidOperationException(
$"Unable to get the '{context.ResourceName}' connection string.");
await using var connection = ConnectionMultiplexer.Connect(connectionString);
var database = connection.GetDatabase();
await database.ExecuteAsync("FLUSHALL");
return CommandResults.Success();
}

The preceding code:

  • Retrieves the connection string from the Redis resource.
  • Connects to the Redis instance.
  • Gets the database instance.
  • Executes the FLUSHALL command to clear the cache.
  • Returns a CommandResults.Success() instance to indicate that the command was successful.

The updateState callback decides whether a command is enabled in the dashboard. The dashboard invokes it whenever the resource’s state changes — typical use cases are gating destructive commands behind a healthy resource, or only enabling a command after a startup probe has succeeded.

In C# the parameter is typed as Func<UpdateCommandStateContext, ResourceCommandState>. UpdateCommandStateContext exposes:

  • ServiceProvider (IServiceProvider) — used to resolve services.
  • ResourceSnapshot (CustomResourceSnapshot) — an immutable snapshot of the resource instance, including health status, lifecycle state, properties, and URLs.

The earlier example gates the clear-cache command on the Redis resource being healthy:

RedisResourceBuilderExtensions.cs
private static ResourceCommandState OnUpdateResourceState(
UpdateCommandStateContext context)
{
var logger = context.ServiceProvider.GetRequiredService<ILogger<Program>>();
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation(
"Updating resource state: {ResourceSnapshot}",
context.ResourceSnapshot);
}
return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy
? ResourceCommandState.Enabled
: ResourceCommandState.Disabled;
}

The preceding code:

  • Retrieves the logger instance from the service provider.
  • Logs the resource snapshot details.
  • Returns ResourceCommandState.Enabled if the resource is healthy; otherwise, it returns ResourceCommandState.Disabled.

To test the custom command, update your AppHost to wire up the resource and the command:

AppHost.cs
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache")
.WithClearCommand();
var apiService = builder.AddProject<Projects.AspireApp_ApiService>("apiservice");
builder.AddProject<Projects.AspireApp_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WaitFor(cache)
.WithReference(apiService)
.WaitFor(apiService);
builder.Build().Run();

The preceding code calls the WithClearCommand extension method to add the custom command to the Redis resource.

Run the app and navigate to the Aspire dashboard. You should see the custom command listed under the cache resource. On the Resources page of the dashboard, select the ellipsis button under the Actions column:

Aspire dashboard: Redis cache resource with custom command displayed.

The preceding image shows the Clear cache command that was added to the Redis resource. The icon displays as a rabbit crossed out to indicate that the speed of the dependent resource is being cleared.

Select the Clear cache command to clear the cache of the Redis resource. The command should execute successfully, and the cache should be cleared. The result of the command is available in the notification center:

Aspire dashboard: Redis cache resource with custom command executed.

In addition to executing commands through the Aspire dashboard, you can also execute commands programmatically using the ResourceCommandService. This service is useful when you need to:

  • Execute commands from within your application code
  • Automate command execution as part of a workflow
  • Build custom tooling that needs to control resources

The most common scenario is to call commands from within other custom command implementations. In C#, use ExecuteCommandContext.ServiceProvider to retrieve the ResourceCommandService. In TypeScript, resolve the service from the AppHost execution context before registering the command, then capture it in the callback.

The following example shows how to create a custom command that executes other commands. In this case, a “reset-all” command clears the cache and restarts a database:

AppHost.cs
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache")
.WithClearCommand(); // Defines a "clear-cache" command as shown earlier
var database = builder.AddPostgres("postgres")
.WithCommand("restart", "Restart Database", async (context, ct) =>
{
// Restart database implementation
return CommandResults.Success();
});
var api = builder.AddProject<Projects.Api>("api")
.WithReference(cache)
.WithReference(database)
.WithCommand("reset-all", "Reset Everything", async (context, ct) =>
{
var commandService = context.ServiceProvider.GetRequiredService<ResourceCommandService>();
context.Logger.LogInformation("Starting full system reset...");
var clearResult = await commandService.ExecuteCommandAsync(
resource: cache.Resource,
commandName: "clear-cache",
cancellationToken: ct);
var restartResult = await commandService.ExecuteCommandAsync(
resource: database.Resource,
commandName: "restart",
cancellationToken: ct);
if (!clearResult.Success || !restartResult.Success)
{
return CommandResults.Failure("System reset failed");
}
context.Logger.LogInformation("System reset completed successfully");
return CommandResults.Success();
});
builder.Build().Run();

In this example:

  • In C#, the context parameter provides access to the service provider and logger
  • In TypeScript, ResourceCommandService is retrieved from the AppHost execution context’s service provider before registering the command
  • In C#, commands are executed on resource instances using cache.Resource and database.Resource; in TypeScript, they are executed by resource name string (for example, "cache" and "postgres")
  • Each command result is checked for success before proceeding

You can also execute a command using the resource ID or resource name. The resource ID is the unique identifier for a resource instance, while the resource name is the display name (which must be unique to use this approach):

AppHost.cs
var result = await commandService.ExecuteCommandAsync(
resourceId: "cache",
commandName: "clear-cache",
cancellationToken: cancellationToken);
if (result.Success)
{
logger.LogInformation("Command executed successfully");
}
else if (result.Canceled)
{
logger.LogWarning("Command was canceled");
}
else
{
logger.LogError("Command failed: {Message}", result.Message);
}

The resource identifier argument (resourceId in C#, or the first argument in TypeScript when passing a string) can be either:

  • The unique ID of the resource (e.g., cache-abcdwxyz for a resource with replicas)
  • The display name (e.g., cache) if there are no duplicate names

Execute commands on resources with replicas

Section titled “Execute commands on resources with replicas”

When executing a command on a resource with multiple replicas using the C# IResource overload, the command runs in parallel on all instances:

AppHost.cs
// For a resource with replicas, the command executes on all instances
var result = await commandService.ExecuteCommandAsync(
resource: cache.Resource,
commandName: "clear-cache",
cancellationToken: cancellationToken);

In TypeScript, use the resource identifier overload shown earlier: pass a specific replica ID to target one instance, or pass a unique resource name when there are no duplicate names.

The behavior when executing on multiple replicas:

  • The command runs in parallel on all instances
  • If all commands succeed, the result indicates success
  • If any commands fail, the result includes details about the failures
  • If all non-successful commands were canceled, the result indicates cancellation

The ExecuteCommandResult type carries the outcome of a command. It exposes the same conceptual fields in both languages:

  • Success / success (boolean) — Whether the command completed successfully.
  • Canceled / canceled (boolean) — Whether the command was canceled by the user.
  • Message / message (string?) — A human-readable message that surfaces in the dashboard notification center and CLI output. Used for both success messages and error messages.
  • Data / data (CommandResultData?) — Optional structured output (plain text, JSON, or Markdown). See Return structured output from a command.

The CommandResults helper class provides factory methods for the common shapes:

// Success with no payload
return CommandResults.Success();
// Failure with a status message
return CommandResults.Failure("Error occurred during execution");
// Cancellation (typically when the user backs out of a confirmation prompt)
return CommandResults.Canceled();
// Failure derived from an exception (uses Exception.Message)
return CommandResults.Failure(ex);

Resource commands can return a structured payload — plain text, JSON, or Markdown — alongside the success/failure signal. The payload flows automatically through the entire Aspire pipeline:

  • Dashboard: The success notification gains a View response action that opens a text visualizer dialog with the payload. Json results lock the visualizer to JSON syntax highlighting; Markdown results lock it to Markdown rendering. Set DisplayImmediately to open the dialog as soon as the command completes (no click required).

  • CLI: Status messages route to stderr and the payload routes to stdout, so the output is safe to pipe to tools such as jq:

    Terminal
    aspire resource cache issue-access-token | jq -r .token
  • MCP tools: The payload is appended to the tool response as a second TextContentBlock after the status message.

A common scenario is issuing an access token from a custom command — developers can copy it straight from the dashboard or pipe it into a Bearer header from the CLI. In Text mode, the result is just the token; in Json mode, the result includes the expiry and scopes.

Use the CommandResults.Success(message, result, resultFormat) overload to attach a payload to a successful result. The message is the notification center/CLI status; result is the payload string; resultFormat controls how the dashboard visualizer interprets it.

C# — AppHost.cs
using System.Security.Cryptography;
using Aspire.Hosting.ApplicationModel;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.MyService>("myservice")
.WithCommand(
name: "issue-access-token",
displayName: "Issue Access Token",
executeCommand: context =>
{
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
return Task.FromResult(CommandResults.Success(
message: "Access token issued.",
result: token,
resultFormat: CommandResultFormat.Text));
});
builder.Build().Run();

To return JSON with the expiry and scopes, switch the format to CommandResultFormat.Json:

C# — AppHost.cs
using System.Security.Cryptography;
using System.Text.Json;
using Aspire.Hosting.ApplicationModel;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.MyService>("myservice")
.WithCommand(
name: "issue-access-token",
displayName: "Issue Access Token",
executeCommand: context =>
{
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
var json = JsonSerializer.Serialize(new
{
token,
expiresAt = DateTimeOffset.UtcNow.AddHours(1),
scopes = new[] { "read", "write" },
});
return Task.FromResult(CommandResults.Success(
message: "Access token issued.",
result: json,
resultFormat: CommandResultFormat.Json));
});
builder.Build().Run();

CommandResultFormat has three values:

ValueDashboard behavior
TextPlain text. Opens in the text visualizer dialog with no fixed format — the user can switch to a different syntax for inspection.
JsonJSON. The visualizer locks to JSON mode for syntax highlighting and pretty-printing.
MarkdownMarkdown. The visualizer locks to Markdown rendering.

Omit Data (return CommandResults.Success() or { success: true }) when the command has no payload to display.

The matching CommandResults.Failure(errorMessage, result, resultFormat) overload attaches the same payload shape to a failed result, which is useful for surfacing diagnostic detail that the user (or a downstream tool) can inspect:

C# — AppHost.cs
return CommandResults.Failure(
errorMessage: "Migration failed.",
result: errorJson,
resultFormat: CommandResultFormat.Json);

Auto-open the result dialog in the dashboard

Section titled “Auto-open the result dialog in the dashboard”

By default, the dashboard surfaces the payload as a View response action on the notification in the notification center. Pass displayImmediately: true to CommandResults.Success to open the visualizer the moment the command completes — useful for short commands where the response is the point:

C# — AppHost.cs
return CommandResults.Success(
message: "Access token issued.",
result: json,
resultFormat: CommandResultFormat.Json,
displayImmediately: true);

When a resource has multiple replicas, the command runs on all instances in parallel. If more than one replica returns a payload, only the first successful payload is propagated to the caller; the others are discarded.

Any command registered with WithCommand can be invoked from the terminal — including the built-in start, stop, and restart commands — using the aspire resource command:

Terminal
aspire resource <resource-name> <command-name> [--apphost <path>]

The CLI splits status output from payload output so the result is friendly to scripting:

  • The Message and any progress text is written to stderr.
  • The Data.Value (when present) is written to stdout verbatim. Json and Text payloads are emitted as-is; Markdown is rendered with terminal-styled headings, lists, and code blocks.
  • The exit code is 0 on success and non-zero on failure or cancellation.

Combined, this means you can pipe the structured payload directly into jq, redirect it to a file, or branch on the exit code:

Terminal — Pipe a JSON payload through jq
$ aspire resource cache issue-access-token | jq -r .token
ey7WqGxk2vL...

The status message Access token issued. was written to stderr, so stdout stays clean for the pipe.

Terminal — Use the exit code in a script
if aspire resource db migrate > migrations.json; then
echo "Applied $(jq length migrations.json) migrations."
else
echo "Migration failed — see error above."
exit 1
fi

Commands can declare input arguments that the dashboard renders as a prompt dialog before execution and that the CLI accepts as ordered positional values. Arguments are defined by setting CommandOptions.Arguments to an array of InteractionInput objects.

AppHost.cs
using Aspire.Hosting.ApplicationModel;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.MyService>("myservice")
.WithCommand(
name: "send-message",
displayName: "Send Message",
executeCommand: context =>
{
var text = context.Arguments.GetString("text")!;
var repeat = int.TryParse(context.Arguments.GetString("repeat"), out var r) ? r : 1;
for (var i = 0; i < repeat; i++)
{
context.Logger.LogInformation("{Text}", text);
}
return Task.FromResult(CommandResults.Success($"Sent '{text}' {repeat} time(s)."));
},
commandOptions: new CommandOptions
{
Arguments =
[
new InteractionInput { Name = "text", Label = "Message", InputType = InputType.Text, Required = true, MaxLength = 200 },
new InteractionInput { Name = "repeat", Label = "Repeat", InputType = InputType.Number, Value = "1" },
]
});
builder.Build().Run();

Each InteractionInput object in the Arguments array configures one input field in the dashboard prompt and one positional argument for the CLI:

PropertyTypeDescription
Name / namestringUnique identifier used to read the value at runtime.
Label / labelstring? / string | undefinedOptional human-readable label shown in the dashboard input dialog. When omitted, the name is used as the label.
InputType / inputTypeInputTypeControls the widget type. See the table below.
Value / valuestring?Default value pre-filled in the dialog.
Required / requiredboolWhen true, the built-in validator rejects empty submissions.
MaxLength / maxLengthint?Optional maximum character length for Text and SecretText inputs.
Options / optionsIReadOnlyList<KeyValuePair<string,string>>? / InteractionInputOption[] | undefinedRequired for Choice inputs. Each option provides the submitted key and display value.

The InputType enum controls how the dashboard renders the field and how built-in validation works:

ValueDashboard widgetCLI behaviorBuilt-in validation
TextSingle-line text boxPositional string valueRequired, MaxLength
NumberNumber inputPositional string parsed as numberMust be a valid number
BooleanCheckboxtrue or false stringMust be true or false
ChoiceDrop-down listString must match one of OptionsMust be a member of Options
SecretTextMasked text boxPositional string valueRequired, MaxLength

The ExecuteCommandContext exposes an Arguments property (C#) / arguments() method (TypeScript) that returns an InteractionInputCollection for reading submitted values.

InteractionInputCollection provides typed convenience accessors for common argument reads:

var text = context.Arguments.GetString("text"); // string? — null when absent or empty
var repeat = context.Arguments.GetInt32("repeat"); // int — throws when absent or unparseable
var shout = context.Arguments.GetBoolean("shout"); // bool — throws when absent or unparseable
var amount = context.Arguments.GetDouble("amount"); // double — throws when absent or unparseable

GetString returns null when the argument is absent or empty. The numeric and boolean accessors throw InvalidOperationException when the value is missing and FormatException (or OverflowException for GetInt32 and GetDouble) when the value can’t be parsed. Use Required = true on InteractionInput to enforce presence before the callback runs, or call GetString first and parse the result manually if you need a fallback.

When invoking a command from the CLI, supply argument values as ordered positional tokens after the command name. The CLI maps them onto the declared Arguments array by position:

Terminal
aspire resource myservice send-message "Hello world" 3 --apphost MyApp.AppHost.csproj

In the example above, "Hello world" maps to the first argument (text) and 3 maps to the second (repeat).

Commands can run server-side validation before the callback executes. Declare a ValidateArguments callback on CommandOptions to add custom rules on top of the built-in required, max-length, and type checks.

AppHost.cs
using Aspire.Hosting.ApplicationModel;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.MyService>("myservice")
.WithCommand(
name: "deploy",
displayName: "Deploy",
executeCommand: context =>
{
var target = context.Arguments.GetString("target")!;
// ... perform deployment ...
return Task.FromResult(CommandResults.Success($"Deployed to {target}."));
},
commandOptions: new CommandOptions
{
Arguments =
[
new InteractionInput { Name = "target", Label = "Target", InputType = InputType.Text, Required = true },
],
ValidateArguments = context =>
{
var target = context.Inputs.GetString("target");
if (string.Equals(target, "prod", StringComparison.OrdinalIgnoreCase))
{
context.AddValidationError("target", "Target must not be 'prod'.");
}
return Task.CompletedTask;
}
});
builder.Build().Run();

Built-in validation runs first (required fields, max length, choice membership, and type coercion). If built-in validation passes, the ValidateArguments callback runs next. Either kind of validation failure prevents the command callback from executing and returns structured field errors to the caller:

  • Dashboard: The input dialog stays open with inline field-level error messages.
  • CLI / MCP: The command exits with a non-zero status and prints the field errors to stderr.

The ValidateArguments context exposes the same Inputs/inputs() surface as ExecuteCommandContext.Arguments/arguments(), so the same typed accessors work in both places.

By default, commands registered with WithCommand are visible to both the Aspire dashboard and to API/MCP callers. The ResourceCommandVisibility enum is bit-combinable, so you can restrict or expand where a command appears by setting CommandOptions.Visibility:

ValueWhere the command is available
ResourceCommandVisibility.UIDashboard and other UI clients only
ResourceCommandVisibility.ApiAPI and MCP clients only
ResourceCommandVisibility.UI | ResourceCommandVisibility.ApiBoth UI and API/MCP (default)
ResourceCommandVisibility.NoneHidden from all clients
AppHost.cs
using Aspire.Hosting.ApplicationModel;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.MyService>("myservice")
// Only exposed through the API/MCP — not shown in the dashboard
.WithCommand(
name: "get-metrics",
displayName: "Get Metrics",
executeCommand: context =>
{
// ... collect and return metrics ...
return Task.FromResult(CommandResults.Success(
message: "Metrics collected.",
value: new CommandResultData { Value = "{}", Format = CommandResultFormat.Json }));
},
commandOptions: new CommandOptions
{
Visibility = ResourceCommandVisibility.Api,
});
builder.Build().Run();

The WithProcessCommand API provides a reusable helper for the common pattern of exposing a local-tool invocation as a resource command. Instead of managing process start, stdout/stderr capture, and cancellation yourself inside a WithCommand callback, WithProcessCommand does that for you:

  • Starts a local process from the AppHost machine using the provided executable path and arguments.
  • Passes arguments as an argument list (not through a shell), so no shell-quoting is needed.
  • Streams stdout and stderr to the command logger (visible in the Aspire dashboard and via aspire logs <resource>).
  • Returns the captured output as bounded text (configurable via MaxOutputLineCount).
  • Maps non-zero exit codes and cancellation to appropriate ExecuteCommandResult values automatically.

The simplest form takes the executable and an argument array directly:

AppHost.cs
#pragma warning disable ASPIREPROCESSCOMMAND001
var builder = DistributedApplication.CreateBuilder(args);
builder.AddRedis("cache")
.WithProcessCommand(
commandName: "dotnet-version",
displayName: "Show .NET version",
executablePath: "dotnet",
arguments: ["--version"]);
builder.Build().Run();

When the command runs in the dashboard or CLI, dotnet --version (C#) or node --version (TypeScript) executes on the AppHost machine, and the version string is captured and displayed as the command output.

When the command arguments depend on runtime context — for example, a dataset name supplied by the user through the dashboard’s argument dialog — build the process specification from the command execution context:

AppHost.cs
#pragma warning disable ASPIREPROCESSCOMMAND001
#pragma warning disable ASPIREINTERACTION001
using Aspire.Hosting.ApplicationModel;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddRedis("cache")
.WithProcessCommand(
commandName: "seed-data",
displayName: "Seed data",
createProcessSpec: context => new ProcessCommandSpec("dotnet")
{
Arguments =
[
"run",
"--project",
"tools/SeedData",
"--",
context.Arguments.GetString("dataset") ?? "small",
],
EnvironmentVariables = { ["ConnectionStrings__db"] = "Host=localhost;Database=db" },
},
options: new ProcessCommandOptions
{
MaxOutputLineCount = 20,
Arguments =
[
new InteractionInput { Name = "dataset", Label = "Dataset", InputType = InputType.Text, Required = true },
],
});
builder.Build().Run();

In C#, the callback overload receives an ExecuteCommandContext. In TypeScript, withProcessCommandFactory receives the same execution context and returns the process specification. The dashboard renders the configured arguments as a prompt dialog before starting the process, and the entered values are available through the command context.

The following configuration options are available for the process to run. In C#, these map to ProcessCommandSpec properties. In TypeScript, provide them as fields in the options object passed to withProcessCommand, or return them from the withProcessCommandFactory callback.

  • Executable path — the path to the process to launch. Short names (no directory separator) are resolved from the AppHost’s PATH.
  • Arguments — a list of arguments passed to the process. Each entry is treated as a separate argument, so no shell quoting or escaping is needed.
  • Environment variables — additional key/value pairs set in the child process’s environment.
  • Inherit environment variables — whether the child process inherits the AppHost process’s environment variables. Defaults to true.
  • Standard input content — optional text written to the process’s stdin immediately after it starts.
  • Working directory — the directory the process starts in. Defaults to the AppHost directory if not specified.
  • Kill entire process tree — whether cancellation and disposal should terminate the whole child process tree. Defaults to true.

The following options control how WithProcessCommand handles the process result. In C#, set them on ProcessCommandOptions. In TypeScript, provide them in the same options object as the process configuration for withProcessCommand, or in the fourth argument to withProcessCommandFactory.

  • Max output line count — the maximum number of combined stdout/stderr lines captured and returned as the command result. Defaults to 50. Lines beyond this limit are silently discarded (oldest lines first).
  • Display immediately — when true (the default), the captured output is automatically shown in the dashboard as soon as the command finishes.
  • Success exit codes — the list of process exit codes that are treated as a successful command invocation. Defaults to [0].

WithProcessCommand resolves the executable using the following rules:

  1. If ExecutablePath is an absolute path or contains a path separator, it is used as-is.
  2. Otherwise, the AppHost process’s PATH is searched for the named executable. On Windows, PATHEXT extensions are also tried.

This means you can reference tools such as dotnet, node, or docker by short name as long as they are on the AppHost’s PATH when the app starts.