ドキュメントAspire を試す
ドキュメント試す

カスタム リソース コマンド

Aspire アプリ モデル内の各リソースは IResource として表現され、分散アプリケーション ビルダーに追加されると、IResourceBuilder<T> インターフェイスのジェネリック型パラメーターになります。resource builder API を使って呼び出しを連鎖し、基になるリソースを構成できます。状況によっては、そのリソースにカスタム コマンドを追加したいことがあります。カスタム コマンドを作成する一般的なシナリオには、データベース移行の実行やデータベースのシード/リセットなどがあります。この記事では、キャッシュをクリアする Redis リソースにカスタム コマンドを追加する方法を学びます。

リソースにカスタム コマンドを追加する

Section titled “リソースにカスタム コマンドを追加する”

Aspire AppHost プロジェクトから開始します。以下のウォークスルーでは、Redis リソースに clear-cache コマンドを登録します。C# 版は登録をラップする拡張メソッドを使用し、TypeScript 版は管理エンドポイントを公開する Node サービスに対してコマンドをインライン登録します。どちらのフローでも、同じダッシュボード/CLI 体験を得られます。

TypeScript AppHost では、リソース ビルダー上でコマンドをインライン登録します。TypeScript には拡張メソッドがないため、クロージャーでコマンドに必要な状態をキャプチャします。

apphost.mts
import {
createBuilder,
type ExecuteCommandContext,
type ExecuteCommandResult,
} from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder
.addNodeApp("cache", "./cache-service", "src/server.ts")
.withHttpEndpoint();
await cache.withCommand(
"clear-cache",
"Clear Cache",
async (_context: ExecuteCommandContext): Promise<ExecuteCommandResult> => {
const endpoint = await cache.getEndpoint("http");
const url = await endpoint.url.get();
const res = await fetch(`${url}/admin/cache/clear`, { method: "POST" });
if (!res.ok) {
return {
success: false,
message: `Cache service returned ${res.status} ${res.statusText}.`,
};
}
return { success: true, message: "Cache cleared." };
},
{
commandOptions: {
description: "Drops all entries in the cache service.",
confirmationMessage: "Clear all cached entries?",
iconName: "AnimalRabbitOff",
},
});
await builder.build().run();

前述のコード:

  • cache という名前の Node アプリを追加し、その上に clear-cache コマンドを登録します。
  • executeCommand コールバックを async アロー関数として実装し、リソースの HTTP エンドポイントを呼び出して実際のキャッシュ無効化を実行します。このパターンは、HTTP 経由で管理操作を公開する任意のリソースで機能します。代わりにクライアントを直接使いたいリソース(データベース、メッセージ ブローカー、…)では、同じコールバック内からそのクライアントを呼び出してください。
  • 4 番目の引数として { commandOptions: { ... } } を渡し、ダッシュボードの説明、確認プロンプト、アイコンを設定します。

WithCommand API は、リソースに適切な注釈を追加し、それらは Aspire ダッシュボード で利用されます。ダッシュボードはこれらの注釈を使って UI にコマンドを描画します。詳細へ進む前に、まず WithCommand メソッドのパラメーターを理解しておきましょう。

  • name: 呼び出すコマンド名。
  • displayName: ダッシュボードに表示するコマンド名。
  • executeCommand: コマンド呼び出し時に実行されるコールバック。C# では型が Func<ExecuteCommandContext, Task<ExecuteCommandResult>>、TypeScript では (context: ExecuteCommandContext) => Promise<ExecuteCommandResult> です。
  • updateState: ダッシュボード上でコマンドを有効化する状態を判定するコールバック。C# では Func<UpdateCommandStateContext, ResourceCommandState> で、CommandOptions.UpdateState 経由で指定します。TypeScript では (context: UpdateCommandStateContext) => Promise<ResourceCommandState> で、CommandOptions 上の updateState フィールドとして指定します。
  • iconName: ダッシュボードに表示するアイコン名。アイコンは省略可能ですが、指定する場合は有効な Fluent UI Blazor アイコン名 である必要があります。
  • iconVariant: ダッシュボードに表示するアイコンのバリアント。有効な値は Regular(既定)または Filled です。

executeCommand コールバックは、コマンド ロジックを実装する場所です。受け取る ExecuteCommandContext から、操作対象リソース、キャンセル、ログなどのコマンド固有データにアクセスできます。

TypeScript でのコールバック シグネチャは (context: ExecuteCommandContext) => Promise<ExecuteCommandResult> です。ExecuteCommandContext はコマンド固有データの async アクセサーを公開します。

  • resourceName() — リソース インスタンス名。
  • cancellationToken()AbortSignal を受け取る API に転送できる CancellationToken
  • logger() — リソース ロガー。
  • arguments() — コマンド実行時に渡された呼び出し引数。

TypeScript コールバックは通常、async アロー関数としてインライン定義します。次の例は、リソースの HTTP エンドポイントを取得し、キャンセル トークンを fetch に転送します。

apphost.mts
async (context: ExecuteCommandContext): Promise<ExecuteCommandResult> => {
const endpoint = await cache.getEndpoint("http");
const url = await endpoint.url.get();
const cancellation = await context.cancellationToken();
const res = await fetch(`${url}/admin/cache/clear`, {
method: "POST",
signal: cancellation as unknown as AbortSignal,
});
if (!res.ok) {
return {
success: false,
message: `Cache service returned ${res.status} ${res.statusText}.`,
};
}
return { success: true, message: "Cache cleared." };
}

前述のコード:

  • リソースの HTTP エンドポイント URL を読み取ります。
  • リソースの管理エンドポイントを呼び出し、キャンセル トークンを abort signal として転送することで、ダッシュボードまたは CLI がコマンドをキャンセルしたときに要求を停止します。
  • HTTP 呼び出しが失敗した場合は { success: false, message: ... } を返し、成功した場合は { success: true, message: "Cache cleared." } を返します。

updateState コールバックは、ダッシュボード上でコマンドを有効化するかどうかを判定します。ダッシュボードはリソース状態が変化するたびにこれを呼び出します。典型的なユース ケースは、破壊的なコマンドを正常なリソースに対してのみ有効化すること、または起動プローブ成功後にのみコマンドを有効化することです。

TypeScript では CommandOptionsupdateState フィールドは非同期コールバックで、UpdateCommandStateContext を受け取ります。コンテキストは resourceSnapshot() を公開します。これは、healthStatus とライフサイクル状態の state を含む、リソース インスタンスのスナップショットを返す非同期関数です。

次の例では、C# パターンと同様に、リソースが健全な状態のときのみ clear-cache コマンドを有効にします。

apphost.mts
import { HealthStatus, ResourceCommandState } from './.aspire/modules/aspire.mjs';
await cache.withCommand(
"clear-cache",
"Clear Cache",
executeClearCache,
{
commandOptions: {
iconName: "AnimalRabbitOff",
updateState: async (ctx) => {
const snapshot = await ctx.resourceSnapshot();
return snapshot.healthStatus === HealthStatus.Healthy
? ResourceCommandState.Enabled
: ResourceCommandState.Disabled;
},
},
});

上記のコードは以下を行います。

  • ctx.resourceSnapshot() を呼び出して現在のリソース スナップショットを取得します。
  • リソースが健全であれば ResourceCommandState.Enabled を返し、そうでなければ ResourceCommandState.Disabled を返します。

HealthStatusResourceCommandState 列挙体は生成された SDK(aspire.ts)からエクスポートされており、直接インポートして使用できます。

カスタム コマンドをテストする

Section titled “カスタム コマンドをテストする”

カスタム コマンドをテストするには、AppHost を更新してリソースとコマンドを配線します。

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const cache = await builder.addNodeApp("cache", "../cache/server.js");
await cache.withHttpEndpoint();
await cache.withCommand(
"clear-cache",
"Clear Cache",
async (context) => {
const endpoint = await cache.getEndpoint("http");
const url = await endpoint.url.get();
const cancellation = await context.cancellationToken();
const res = await fetch(`${url}/admin/cache/clear`, {
method: "POST",
signal: cancellation as unknown as AbortSignal,
});
if (!res.ok) {
return {
success: false,
message: `Cache service returned ${res.status} ${res.statusText}.`,
};
}
return { success: true, message: "Cache cleared." };
},
{
commandOptions: {
iconName: "AnimalRabbitOff",
description: "Clears the application cache.",
confirmationMessage: "Are you sure you want to clear the cache?",
},
});
const apiService = await builder.addProject("apiservice", "../ApiService/ApiService.csproj");
const web = await builder.addProject("webfrontend", "../Web/Web.csproj");
await web.withExternalHttpEndpoints();
await web.withReference(cache);
await web.waitFor(cache);
await web.withReference(apiService);
await web.waitFor(apiService);
await builder.build().run();

前述のコードは、キャッシュ サービスを HTTP エンドポイント付き Node アプリとして登録し、clear-cache コマンドを直接アタッチします。

アプリを実行して Aspire ダッシュボードへ移動します。キャッシュ リソース配下にカスタム コマンドが表示されるはずです。ダッシュボードの Resources ページで、Actions 列の下にある省略記号ボタンを選択します。

Aspire ダッシュボード: カスタム コマンドが表示された Redis キャッシュ リソース。

前述の画像には、Redis リソースへ追加した Clear cache コマンドが表示されています。アイコンは、依存リソースの速度がクリアされることを示すため、取り消し線付きのウサギとして表示されます。

Clear cache コマンドを選択して、Redis リソースのキャッシュをクリアします。コマンドは正常に実行され、キャッシュはクリアされるはずです。コマンドの結果は 通知センター で確認できます。

Aspire ダッシュボード: カスタム コマンドが実行された Redis キャッシュ リソース。

コマンドをプログラムで実行する

Section titled “コマンドをプログラムで実行する”

Aspire ダッシュボード を介してコマンドを実行するだけでなく、ResourceCommandService を使ってプログラムからコマンドを実行することもできます。このサービスは、次のような場合に有用です。

  • アプリケーション コード内からコマンドを実行する
  • ワークフローの一部としてコマンド実行を自動化する
  • リソース制御が必要なカスタム ツールを構築する

最も一般的なシナリオは、他のカスタム コマンド実装からコマンドを呼び出すことです。C# では ExecuteCommandContext.ServiceProvider を使って ResourceCommandService を取得します。TypeScript では、コマンドを登録する前に AppHost の実行コンテキストからサービスを解決し、コールバックでキャプチャします。

カスタム コマンドからコマンドを実行する

Section titled “カスタム コマンドからコマンドを実行する”

次の例は、他のコマンドを実行するカスタム コマンドの作成方法を示しています。このケースでは、“reset-all” コマンドがキャッシュをクリアし、データベースを再起動します。

apphost.mts
import { createBuilder } from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
const executionContext = await builder.executionContext();
const serviceProvider = await executionContext.serviceProvider();
const commandService = await serviceProvider.getResourceCommandService();
const cache = await builder.addRedis("cache");
await cache.withCommand("clear-cache", "Clear Cache", async () => {
// キャッシュ クリアの実装
return { success: true };
});
const database = await builder.addPostgres("postgres");
await database.withCommand("restart", "Restart Database", async () => {
// データベース再起動の実装
return { success: true };
});
const api = await builder.addProject("api", "../Api/Api.csproj");
await api.withReference(cache);
await api.withReference(database);
await api.withCommand("reset-all", "Reset Everything", async (ctx) => {
const logger = await ctx.logger();
const cancellationToken = await ctx.cancellationToken();
await logger.logInformation("Starting full system reset...");
const clearResult = await commandService.executeCommandAsync(
"cache", "clear-cache", { cancellationToken });
const restartResult = await commandService.executeCommandAsync(
"postgres", "restart", { cancellationToken });
if (!clearResult.success || !restartResult.success) {
return { success: false, message: "System reset failed" };
}
await logger.logInformation("System reset completed successfully");
return { success: true };
});
await builder.build().run();

この例では:

  • C# では context パラメーターでサービス プロバイダーとロガーにアクセスします。
  • TypeScript では、コマンドを登録する前に AppHost 実行コンテキストのサービス プロバイダーから ResourceCommandService を取得します。
  • C# では cache.Resourcedatabase.Resource を使ってリソース インスタンスに対してコマンドを実行し、TypeScript では "cache""postgres" などのリソース名文字列を使って実行します。
  • 各コマンド結果の成功を確認してから次へ進みます。

リソース ID でコマンドを実行する

Section titled “リソース ID でコマンドを実行する”

リソース ID またはリソース名を使ってコマンドを実行することもできます。リソース ID はリソース インスタンスの一意識別子で、リソース名は表示名です(この方法を使うには一意である必要があります)。

apphost.mts
const result = await commandService.executeCommandAsync(
"cache",
"clear-cache",
{ cancellationToken });
if (result.success) {
console.log("Command executed successfully");
}
else if (result.canceled) {
console.warn("Command was canceled");
}
else {
console.error(`Command failed: ${result.message}`);
}

リソース識別子引数(C# の resourceId、または TypeScript で文字列を渡す場合の最初の引数)には、次のいずれかを指定できます:

  • リソースの一意 ID(例: レプリカを持つリソースの cache-abcdwxyz
  • 重複名がない場合の表示名(例: cache

レプリカを持つリソースでコマンドを実行する

Section titled “レプリカを持つリソースでコマンドを実行する”

複数レプリカを持つリソースに対して IResource オーバーロードでコマンドを実行すると、すべてのインスタンスで並列実行されます。

AppHost.cs
// レプリカを持つリソースでは、すべてのインスタンスでコマンドが実行される
var result = await commandService.ExecuteCommandAsync(
resource: cache.Resource,
commandName: "clear-cache",
cancellationToken: cancellationToken);

TypeScript では、前述のリソース識別子オーバーロードを使います。特定のレプリカ ID を渡して 1 つのインスタンスを対象にするか、重複名がない場合は一意のリソース名を渡します。

複数レプリカ上で実行した場合の動作:

  • コマンドはすべてのインスタンスで並列実行されます。
  • すべてのコマンドが成功した場合、結果は成功を示します。
  • いずれかのコマンドが失敗した場合、結果には失敗の詳細が含まれます。
  • 非成功のすべてがキャンセルだった場合、結果はキャンセルを示します。

ExecuteCommandResult 型はコマンドの結果を保持します。両言語で同じ概念フィールドを公開します。

  • Success / success (boolean) — コマンドが正常完了したかどうか。
  • Canceled / canceled (boolean) — ユーザー操作でコマンドがキャンセルされたかどうか。
  • Message / message (string?)通知センター と CLI 出力に表示される人間向けメッセージ。成功メッセージとエラー メッセージの両方に使われます。
  • Data / data (CommandResultData?) — 省略可能な構造化出力(プレーン テキスト、JSON、Markdown)。コマンドから構造化出力を返す を参照してください。

TypeScript にはヘルパー クラスがないため、ExecuteCommandResult に準拠するプレーン オブジェクトを返します。

// ペイロードなしで成功
return { success: true };
// ステータス メッセージ付きで失敗
return { success: false, message: "Error occurred during execution" };
// キャンセル(通常は確認プロンプトでユーザーが戻った場合)
return { canceled: true };
// 捕捉した例外由来の失敗
return { success: false, message: err instanceof Error ? err.message : String(err) };

コマンドから構造化出力を返す

Section titled “コマンドから構造化出力を返す”

リソース コマンドは、成功/失敗シグナルに加えて、プレーン テキスト、JSON、Markdown の構造化ペイロードを返せます。このペイロードは Aspire パイプライン全体を自動で流れます。

  • Dashboard: 成功通知に View response アクションが追加され、ペイロードを表示するテキスト ビジュアライザー ダイアログが開きます。Json 結果は JSON シンタックス ハイライトに固定され、Markdown 結果は Markdown レンダリングに固定されます。DisplayImmediately を設定すると、コマンド完了直後にダイアログが開きます(クリック不要)。

  • CLI: ステータス メッセージは stderr に、ペイロードは stdout に出力されるため、jq のようなツールへ安全にパイプできます。

    Terminal
    aspire resource cache issue-access-token | jq -r .token
  • MCP tools: ステータス メッセージの後に続く 2 番目の TextContentBlock として、ペイロードがツール応答に追加されます。

一般的なシナリオは、カスタム コマンドから アクセス トークン を発行することです。開発者はそれをダッシュボードから直接コピーしたり、CLI から Bearer ヘッダーにパイプしたりできます。Text モードでは結果はトークン文字列のみ、Json モードでは有効期限とスコープを含む結果になります。

成功結果へペイロードを添付するには、CommandResults.Success(message, result, resultFormat) オーバーロードを使います。message は通知センター/CLI のステータス、result はペイロード文字列、resultFormat はダッシュボード ビジュアライザーの解釈方法を制御します。

TypeScript では、CommandResultData 型の data プロパティを持つ ExecuteCommandResult オブジェクト リテラルを返します。

apphost.mts
import {
createBuilder,
CommandResultFormat,
type ExecuteCommandContext,
type ExecuteCommandResult,
} from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder
.addNodeApp("myservice", "./myservice", "src/server.ts")
.withCommand("issue-access-token", "Issue Access Token",
async (_context: ExecuteCommandContext): Promise<ExecuteCommandResult> => {
const token = crypto.randomUUID().replace(/-/g, "");
return {
success: true,
message: "Access token issued.",
data: {
value: token,
format: CommandResultFormat.Text,
},
};
});
await builder.build().run();

有効期限とスコープ付き JSON を返すには、形式を CommandResultFormat.Json に切り替えます。

apphost.mts
import {
createBuilder,
CommandResultFormat,
type ExecuteCommandContext,
type ExecuteCommandResult,
} from './.aspire/modules/aspire.mjs';
const builder = await createBuilder();
await builder
.addNodeApp("myservice", "./myservice", "src/server.ts")
.withCommand("issue-access-token", "Issue Access Token",
async (_context: ExecuteCommandContext): Promise<ExecuteCommandResult> => {
const token = crypto.randomUUID().replace(/-/g, "");
const json = JSON.stringify({
token,
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
scopes: ["read", "write"],
});
return {
success: true,
message: "Access token issued.",
data: {
value: json,
format: CommandResultFormat.Json,
},
};
});
await builder.build().run();

CommandResultFormat には 3 つの値があります。

ダッシュボードでの動作
Textプレーン テキスト。固定形式なしでテキスト ビジュアライザー ダイアログに開き、ユーザーは別の構文へ切り替えて確認できます。
JsonJSON。ビジュアライザーは JSON モードに固定され、シンタックス ハイライトと整形表示を行います。
MarkdownMarkdown。ビジュアライザーは Markdown レンダリングに固定されます。

コマンドに表示すべきペイロードがない場合は、Data を省略してください(CommandResults.Success() または { success: true } を返します)。

対応する CommandResults.Failure(errorMessage, result, resultFormat) オーバーロードを使うと、失敗結果にも同じペイロード形状を添付できます。これは、ユーザー(または下流ツール)が確認できる診断詳細を表面化するのに便利です。

apphost.mts
return {
success: false,
message: "Migration failed.",
data: {
value: errorJson,
format: CommandResultFormat.Json,
},
};

ダッシュボードで結果ダイアログを自動表示する

Section titled “ダッシュボードで結果ダイアログを自動表示する”

既定では、ダッシュボードは 通知センター の通知に View response アクションとしてペイロードを表示します。CommandResultDataDisplayImmediately を設定すると、コマンド完了時にビジュアライザーを即座に開けます。短いコマンドで応答自体が目的の場合に便利です。

apphost.mts
return {
success: true,
message: "Access token issued.",
data: {
value: json,
format: CommandResultFormat.Json,
displayImmediately: true,
},
};

リソースが複数レプリカを持つ場合、コマンドはすべてのインスタンスで並列実行されます。複数レプリカからペイロードが返された場合、最初に成功したペイロードのみが呼び出し側へ伝搬し、他は破棄されます。

WithCommand で登録した任意のコマンドは、組み込みの startstoprestart コマンドを含め、aspire resource コマンドを使ってターミナルから呼び出せます。

ターミナル
aspire resource <resource-name> <command-name> [--apphost <path>]

CLI はステータス出力とペイロード出力を分離するため、結果はスクリプト処理しやすい形式になります。

  • Message と進捗テキストは stderr に書き込まれます。
  • Data.Value(存在する場合)は stdout にそのまま書き込まれます。JsonText ペイロードはそのまま出力され、Markdown は見出し、リスト、コード ブロックを端末向けに装飾してレンダリングされます。
  • 終了コードは、成功時が 0、失敗またはキャンセル時が非 0 です。

これにより、構造化ペイロードを jq に直接パイプしたり、ファイルへリダイレクトしたり、終了コードで分岐したりできます。

ターミナル — JSON ペイロードを jq にパイプ
$ aspire resource cache issue-access-token | jq -r .token
ey7WqGxk2vL...

ステータス メッセージ Access token issued. は stderr に書き込まれるため、stdout はパイプ用にクリーンなまま保たれます。

ターミナル — スクリプトで終了コードを使用
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