Start Debugging

既存の ASP.NET Core ソリューションを作り直さずに Aspire を追加する方法

既存の ASP.NET Core ソリューションに Aspire 13.4 を追加します。新しいプロジェクト 2 つとサービスあたり 3 行だけで済ませる手順として、aspire init、AddProject と WithReference による AppHost の配線、既存の launchSettings.json と接続文字列の維持、そして初日にぶつかる resilience、ヘルスエンドポイント、プロキシの落とし穴を解説します。

既存の ASP.NET Core ソリューションに Aspire を追加するときは、何かを移動するのではなく、すでにあるプロジェクトの隣に新しいプロジェクトを 2 つ置くだけです。AppHost プロジェクトが開発時にサービスをオーケストレーションし、ServiceDefaults クラスライブラリが共有のテレメトリと resilience の設定を担い、既存の各サービスにはプロジェクト参照 1 つと Program.cs の 2 行が加わるだけです。フォルダー構成、名前空間、launchSettings.json、接続文字列、Dockerfile、CI パイプラインはすべてそのままで構いません。この記事では Aspire 13.4.6 (2026-06-20 に公開された現行の安定リリース) を .NET 10 および .NET 11 Preview 6 に対して使い、全体を通して解説します。

最初に見つかる記事と比べて、変わった点が 2 つあります。Aspire は 2025 年 11 月の Aspire 13 で名前から ”.NET” を外しました。また dotnet workload install aspire という手順は Aspire 9.0 の時点ですでに消えています。現在はすべてが NuGet と MSBuild の SDK 経由で届くので、古い workload がまだマシンに残っているなら、まず dotnet workload uninstall aspire を実行してください。仕組みの前に概念を押さえたい場合は、以前の Aspire とは何かという解説 が今も通用します。

リポジトリに実際に増えるもの

API とワーカーを持つソリューションでの、正直な内訳です。

MyApp.sln
  src/MyApp.Api/            <- unchanged except 1 ProjectReference + 2 lines
  src/MyApp.Worker/         <- unchanged except 1 ProjectReference + 2 lines
  src/MyApp.AppHost/        <- new
  src/MyApp.ServiceDefaults/<- new
  aspire.config.json        <- new, points the CLI at the AppHost

プロジェクトの移動はありません。名前空間の変更もありません。dotnet publish がコンテナーイメージを生成する方法も変わりません。AppHost は開発時のオーケストレーターであり、デプロイ対象には含まれないからです。この最後の点は誤解されがちです。AppHost は production では動きません。ローカルでプロセスを起動し、設定を注入し、ダッシュボードにデータを供給するだけです。

既存ソリューションに Aspire を追加する手順

  1. Aspire CLI をグローバルツールとしてインストールし、SDK を認識できていることを確認します。
  2. ソリューションのルートで aspire init を実行し、.sln を検出させてプロジェクト形式の AppHost を生成します。
  3. 起動させたい各サービスへのプロジェクト参照を AppHost に追加し、AppHost の Program.csAddProject を使ってそれらを宣言します。
  4. 各サービスから ServiceDefaults を参照し、AddServiceDefaults()MapDefaultEndpoints() を呼び出します。
  5. 既存のインフラをモデル化します。ローカルで動かして構わないものはコンテナーに、外部のままにしなければならないものは AddConnectionString にします。
  6. aspire run を実行し、各サービスが以前と同じエンドポイントで起動することを確認します。

この記事の残りは、この 6 手順のコードと、そのあとに壊れる箇所の話です。

CLI のインストール

Aspire 13.3 以降、CLI は NativeAOT でコンパイルされた .NET グローバルツールとして配布されます。つまり workload も Visual Studio への依存も不要です。

dotnet tool install -g Aspire.Cli
aspire doctor

aspire doctor は 13.4 で追加されたもので、何よりも先に実行する価値があります。CLI のバージョン、認識できている SDK、そして何より、CLI のバージョンと Aspire.AppHost.Sdk のバージョンがずれていないかを表示します。この 2 つのバージョン不一致は、Aspire を使うリポジトリで「自分のマシンでは動いた」が発生する最大の原因です。

AppHost の生成

.sln が置かれているディレクトリで実行します。

aspire init

aspire init はソリューションファイルを見つけると、プロジェクト形式の AppHost を作成してソリューションに追加します。見つからない場合 (たとえば多言語混在のリポジトリ) は、代わりに #:sdk#:package ディレクティブを使う単一ファイルの apphost.cs を作成します。既存の ASP.NET Core ソリューションではプロジェクト形式が望ましいでしょう。生成される Projects 名前空間と、全サービスをまとめて扱える IDE 統合デバッグが手に入るのはこちらだからです。

CLI を使いたくない場合は、テンプレートが同じ仕事をします。

dotnet new aspire-apphost -o src/MyApp.AppHost
dotnet new aspire-servicedefaults -o src/MyApp.ServiceDefaults
dotnet sln add src/MyApp.AppHost src/MyApp.ServiceDefaults

AppHost のプロジェクトファイルは小さく、Aspire の SDK が現れる唯一の場所です。

<!-- src/MyApp.AppHost/MyApp.AppHost.csproj -- Aspire 13.4.6 -->
<Project Sdk="Microsoft.NET.Sdk">
  <Sdk Name="Aspire.AppHost.Sdk" Version="13.4.6" />

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <IsAspireHost>true</IsAspireHost>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Aspire.Hosting.AppHost" Version="13.4.6" />
  </ItemGroup>
</Project>

TargetFramework に注目してください。AppHost は起動対象のサービスより新しい TFM を指定できます。別プロセスとして起動するからです。サービスが net8.0 に縛られているソリューションでも、AppHost だけ net10.0 にできます。

既存プロジェクトの配線

AppHost からサービスへの参照を追加し、それらを宣言します。

dotnet add src/MyApp.AppHost reference src/MyApp.Api src/MyApp.Worker
// src/MyApp.AppHost/Program.cs -- Aspire 13.4.6
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.MyApp_Api>("api")
    .WithExternalHttpEndpoints();

builder.AddProject<Projects.MyApp_Worker>("worker")
    .WithReference(api)
    .WaitFor(api);

builder.Build().Run();

Projects.MyApp_Api 型は Aspire の SDK が ProjectReference 項目から生成するもので、ドットはアンダースコアに置き換えられます。自分で書く必要はなく、最初のビルドまでは存在しません。

ここからが、この手順を非侵襲的にしている部分であり、あまり文書化されていないところです。Aspire は既存の Properties/launchSettings.json を読み取ります。プロジェクトリソースを起動するときは、優先順位に従ってプロファイルを選びます。まず launchProfileName 引数を渡していればそれ、次に AppHost 自身の DOTNET_LAUNCH_PROFILE と名前が一致するプロファイル、次にファイル内の最初のプロファイル、最後にプロファイルなしです。選ばれたプロファイルの applicationUrl を解析して ASPNETCORE_URLS に変換し、そのプロファイルの environmentVariables はそのまま適用します。既存のプロファイルはそのまま機能します。あるサービスでファイルの先頭に “IIS Express” プロファイルがあり、Kestrel のほうを使いたい場合は、名前を明示してください。

builder.AddProject<Projects.MyApp_Api>("api", launchProfileName: "https");

launchProfileName: null を渡すとプロファイルなしで起動します。意味のある launchSettings.json を持たないワーカーには、これがいちばんすっきりした選択肢です。

サービスあたり 2 行

ServiceDefaultsIsAspireSharedProject が付いただけの、ごく普通のクラスライブラリです。各サービスから参照して呼び出します。

// src/MyApp.Api/Program.cs -- ASP.NET Core on .NET 10 / .NET 11 Preview 6
var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();   // <- added

builder.Services.AddControllers();
// ... everything you already had, untouched

var app = builder.Build();

app.MapDefaultEndpoints();      // <- added

app.MapControllers();
app.Run();

AddServiceDefaults() は 4 つの仕事をします。OpenTelemetry のログ、メトリクス、トレースを設定し (ヘルスチェックへのリクエストはトレースから除外されます)、liveness のヘルスチェックを登録し、サービスディスカバリーを登録し、ConfigureHttpClientDefaults を適用してすべての HttpClient に標準の resilience ハンドラーとサービスディスカバリーによる解決を与えます。MapDefaultEndpoints()/health (すべてのチェックが通る必要があります) と /alive (live タグの付いたチェックのみ) をマップし、テンプレートはどちらも開発環境かどうかの判定で保護しています。

これらはいずれもランタイムで Aspire に依存しません。AddServiceDefaults() を呼ぶサービスは、AppHost の外でも、dotnet run でも、コンテナーの中でも、既存の Kubernetes デプロイでも問題なく動きます。単に OTEL_EXPORTER_OTLP_ENDPOINT が指す先へ OTLP テレメトリを送るだけで、その送り先は AppHost が起動した場合はダッシュボード、そうでない場合は本物のコレクターになります。まだコレクターがないなら、無料の OpenTelemetry バックエンドの手順 がこのパイプの反対側をカバーしています。

すでにあるインフラをモデル化する

既存プロジェクトがゼロから始めるチュートリアルともっとも食い違うのがここです。ああいったチュートリアルはたいてい、すべてをコンテナー化するところから始まります。しかし普通はそれができません。共有の開発用 SQL Server は理由があって共有されていますし、キューの中にはデータが入っています。

ローカルで動かして構わない依存関係については、統合を追加してコンテナーの管理を Aspire に任せます。

aspire add redis
var cache = builder.AddRedis("cache");

var api = builder.AddProject<Projects.MyApp_Api>("api")
    .WithReference(cache)
    .WaitFor(cache);

WithReference(cache) は API のプロセスに ConnectionStrings__cache を注入します。既存の builder.Configuration.GetConnectionString("cache") の呼び出しは、そのまま変更なしにこの値を読み取ります。既定の構成では環境変数が appsettings.json より優先されるからです。仕掛けはこれだけです。Aspire は設定の読み方をコード側に変えさせるのではなく、より高い優先度で値を供給しているだけです。HybridCache を Redis の L2 として使う 場合も同じで、キャッシュリソースが接続文字列を供給し、残りの設定は変わりません。

外部のままにしなければならない依存関係には、AddConnectionString を使います。コンテナーではなく、AppHost 自身の構成に裏付けられたリソースが作られます。

// Reads ConnectionStrings:orders from the AppHost's appsettings.json or user secrets
var orders = builder.AddConnectionString("orders");

builder.AddProject<Projects.MyApp_Api>("api")
    .WithReference(orders);

実際の値は appsettings.json ではなく、AppHost の user secrets に入れてください。

dotnet user-secrets --project src/MyApp.AppHost set "ConnectionStrings:orders" "Server=dev-sql;Database=Orders;..."

サービス側からは ConnectionStrings__orders が見えるようになり、それ以外は何も変わりません。AppHost が宣言していない名前をサービスが探しにいくと、DefaultConnection という名前の接続文字列が見つからない で扱っているおなじみの起動失敗になります。AddConnectionString のリソース名は、コードが要求するキーと完全に一致していなければなりません。

サービス間の呼び出しも同じ扱いです。WithReference(api)services__api__https__0services__api__http__0 を注入し、サービスディスカバリーが論理名を解決します。

builder.Services.AddHttpClient<OrdersClient>(
    c => c.BaseAddress = new("https+http://api"));

https+http:// は「HTTPS を優先し、なければ HTTP にフォールバックする」という意味です。これはサービスディスカバリーを登録したプロジェクトでしか解決されず、その登録は AddServiceDefaults() が行います。AddServiceDefaults() を飛ばしたプロジェクトでこのスキームを使うと、起動時ではなく最初のリクエストで UriFormatException が発生します。

実行する

aspire run

CLI は aspire.config.json から AppHost を見つけ、すべてのリソースを起動し、ダッシュボードの URL を表示します。Visual Studio や Rider では AppHost をスタートアッププロジェクトに設定して F5 を押すだけで、複数プロジェクトのスタートアップ構成はもう必要ありません。

2023 年ごろのガイドから来た人が驚く点があります。実際にコンテナーリソースを宣言していないかぎり、Docker を動かしておく必要はありません。AddProject の呼び出しだけで構成された AppHost は、コンテナーランタイムが一切インストールされていなくても起動します。おかげで最初のコミットは安全です。コンテナーリソースをゼロにして AppHost だけを入れ、ダッシュボードと分散トレースを手に入れたうえで、依存関係のコンテナー化はあとにする (あるいはしない) という選択ができます。

初日に壊れるもの

標準の resilience ハンドラーが HTTP の挙動を変えます。 AddServiceDefaults() はプロセス内のすべての HttpClient にこれを適用します。つまりリトライ、サーキットブレーカー、リクエスト全体のタイムアウトが入ります。正当に 2 分かかるクライアントがある場合や、すでに手書きの Polly パイプラインがある場合、層が二重になります。自前のものを外すか、既定の適用範囲を絞るかしてください。両方を残してはいけません。

ヘルスエンドポイントの重複。 すでに自分で /health をマップしているなら、MapDefaultEndpoints() によって同じルートに 2 つ目の登録が入ります。どちらか一方を選んでください。既定より詳しい出力が欲しい場合に何を残すかは、minimal API にヘルスチェックエンドポイントを追加する解説 が扱っています。

OpenTelemetry の二重登録。 ServiceDefaultsConfigureOpenTelemetry は、すでに登録済みのものに追加される形で働きます。Program.cs に独自の AddOpenTelemetry().WithTracing(...) があると、計装が重複し、Serilog が絡んでいればログレコードも重複します。自分のほうを削除し、代わりに ServiceDefaults 側をカスタマイズしてください。共有プロジェクトはそのためにあります。

エンドポイントは既定でプロキシされます。 Aspire は各エンドポイントの前にリバースプロキシを置くため、ブラウザーが叩くポートは Kestrel がバインドしたポートとは別です。外部の何かがポートを固定するまで、これは見えません。ID プロバイダーに登録した OIDC のリダイレクト URI、決済サンドボックスからの Webhook、モバイルクライアントにハードコードされた URL などがそれにあたります。エンドポイントごとに無効化できます。

builder.AddProject<Projects.MyApp_Api>("api")
    .WithEndpoint("https", e => e.IsProxied = false);

CI が AppHost をビルドするようになります。 dotnet build MyApp.sln は新しいプロジェクトを拾い、そのために NuGet から Aspire.AppHost.Sdk を復元する必要があります。パッケージの許可リストを明示している閉じたフィードではこれが失敗し、しかもエラーはパッケージ不足ではなく SDK の解決エラーとして出るため、診断に余計な時間がかかります。SDK とホスティング用パッケージを許可リストに入れるか、ソリューションフィルターで AppHost を CI のビルドから除外してください。それ以外にデプロイパイプラインを変える必要はありません。同じサービスプロジェクトを同じ方法で発行し続けるだけだからです。

13.4 で Postgres を使っている場合: 既定のイメージが 17.6 から 18.3 に上がり、既存の 17.x のデータボリュームにはアタッチできません。ローカルのデータが大事なら WithImageTag でタグを固定してください。

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る