Next.js + ASP.NET Core を .NET Aspire で構成する（with YARP）

# はじめに

この記事は .NET Aspire に関する一連の記事の一部です。

- [.NET Aspire って何？ - 概要](https://qiita.com/takashiuesaka/items/f45e930ef296c5710acc)
- [.NET Aspire を使ってみる](https://qiita.com/takashiuesaka/items/8794f99b0bf3ce3c3106)
- [.NET Aspire をデプロイする](https://qiita.com/takashiuesaka/items/3da744d0511f29270299)
- [.NET Aspire で Prometheus, Jaeger, Grafana を使う](https://qiita.com/takashiuesaka/items/4811e62aa52e39ed834c)
- Next.js + ASP.NET Core を .NET Aspire でホスティングする
- [.NET Aspire でデータベースを扱う - PostgreSQL編](https://qiita.com/takashiuesaka/items/137cb1450c3d9fbc8d1e)
- [.NET Aspire でデータベースを扱う - SQL Server編](https://qiita.com/takashiuesaka/items/6d5ec4d4994649c5dca5)
- [.NET Aspire のダッシュボードを単独で使う](https://qiita.com/takashiuesaka/items/338ae64ca2033e078f7f)

.NET Aspire + Dapr についてはこちらをご覧ください。メインは Dapr についてですが、.NET Aspire を使用する場合についても記載があります。

- [Dapr って何？ - 概要編](https://qiita.com/takashiuesaka/items/ecb8d232d00f0a6591a5)
- [Dapr を使ってみる - 入門編](https://qiita.com/takashiuesaka/items/bdae30de41b0daab881f)
- [Dapr を使ってみる - State management 編](https://qiita.com/takashiuesaka/items/713499c531095d5687c2)

<hr />

この記事は .NET Aspire でフロントエンドフレームワークと他サービスの構成を構築してみよう、という内容です。サンプルとして Next.js を扱いますが、React、 Vue.js を使用する場合でも参考になるかと思います。

## Node アプリケーション用の拡張メソッド　AddNodeApp, AddNpmApp

　2023/12 にリリースされた.NET Aspire の Preview 2 は、IDistributedApplicationBuilder の拡張メソッドに AddNodeApp 拡張メソッドと AddNpmApp 拡張メソッドが追加されました。これによって Node アプリケーションを .NET Aspire 管理下で **Executable** として簡単に扱えるようになりました。

例えば [Vite](https://ja.vitejs.dev/) を使って React アプリケーションを作成したり、Next.js アプリケーションを作成した場合、

```powershell
npm run dev
```

という npm コマンドを使って Hot Reload させながら開発することになりますので、AddNpmApp 拡張メソッドを使います。App Host プロジェクトの Program.cs には次のように実装します。

```csharp:Program.cs
builder.AddNpmApp(name: "ReactApp", workingDirectory: "../frontend", scriptName: "dev");
```

name には好きな名前をつけます。workingDirectoryは AppHost プロジェクトルートから見た React/Next.js アプリケーション のルートフォルダへの相対パスです。scriptName には package.json の scripts に記載されたコマンド名を設定します。この実装例 dev は、npm run dev の dev です（package.json の scripts に記載された dev コマンドです）。

## Visual Studio の Node.js アプリケーション用のテンプレート使えばいいんじゃないの？

Visual Studio には以前より Node.js アプリケーション用のプロジェクトテンプレートがあります。React, Vue.js, Augularのフレームワークに対応しています。Visual Studio 用にプロジェクトファイルも作成されて、デバッグ実行もできます。.NET Aspire 管理下にするのであれば、そのようにして作成した Node.js プロジェクトに対して AddProject 拡張メソッドを使用すれば良さそうなものです。

しかし、AddProject 拡張メソッドは .NET プロジェクト専用なので Node.js アプリケーションに対して AddProject 拡張メソッドは使用できないのです。そのため、Node.js アプリケーションを .NET Aspire で管理するためには AddResource、または AddExecutable メソッドを使って自分でセットアップする必要があったのですが、簡単にセットアップできるように Preview 2のリリース時に新たに AddNodeApp、AddNpmApp拡張メソッドが追加されました.

## .NET Aspire を使う必要ってある？

単体の Node.js アプリケーションを .NET Aspire 管理下にするのは AddNodeApp, AddNpmApp 拡張メソッドを使えば簡単に実現できるのですが、それだけでは全く意味がありません。でも CSR (Client Side Rendering）として Node.js アプリケーションを稼働させる箇所については　WebAPI との通信で CORS への対処が必要です。コンテナベースの場合は次のような構成を取ることになるでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ef61688f-d8e0-41db-bf7b-9faf935765a4.png)

Next.js の API Routes や　Nuxt.js の serverMiddleware を使う場合はこういう構成じゃない！と言う意見があると思いますが、WebAPI は分離して管理・スケールしたいシナリオだと考えていただければと思います。

そして SSR （Server Side Rendering）をする場合や最近では Next.js の Server Components のようにサーバーサイドで Database や 他サービスにアクセスする場合もあるでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/134158b0-5228-02b1-c13d-a330bed5207f.png)

この構成を構築する場合、ローカルと本番の環境による違いを考慮しなければならないのはもちろんですが、開発時のローカルマシンではアクセス先のサービスを立ち上げながら開発する必要があります。そんな環境を整えるのは結構大変です。

これらの課題を .NET Aspire を使ってうまく解決していきましょう。

:::note info
**余談：.NET Aspire の内部実装**
.NET Aspire を使う場合、次のように IDistributedApplicationBuilder インターフェースのインスタンスである builder オブジェクトに対して AddProject や AddContainer 拡張メソッドなどを使ってセットアップします。

```csharp
var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.SomeProj>(name: "SomeProj");
builder.AddContainer(name: "containerA", image:"imageName");
```

この Add〜〜 拡張メソッドは内部で最終的に AddResouce メソッドを使っています。AddResource メソッドの引数に渡すオブジェクトは実行に関する情報を持つのですが、.NET プロジェクト、Container、Executableの3つの形式それぞれごとに異なるクラスのオブジェクトを渡します。

 Node アプリケーション用の情報をもつクラスは [NodeAppResource](https://github.com/dotnet/aspire/blob/main/src/Aspire.Hosting/Node/NodeAppResource.cs) というクラスで、これは ExecutableResource クラスを継承しています。そのため、AddNodeApp, AddNpmApp拡張メソッドを使って管理下にした Node アプリケーションは、**Executable**として .NET Aspire が認識します。
:::

## 今回の構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/80e5a300-039b-985b-b3b7-50209f9ccf7b.png)

### Reverse Proxy
CSR の場合に発生する CORS を解決するために Reverse Proxy を前段に立てることにします。Reverse Proxy は /api/* へのアクセスの場合だけ WebAPI の方にトラフィックを流し、それ以外は全て Node.js アプリケーションを呼び出すように設定します。

.NET で Reverse Proxy の実装は　YARP を使うととても簡単に実装できます。

[YARP: Yet Another Reverse Proxy](https://microsoft.github.io/reverse-proxy/index.html)

YARP は Reverse Proxy を C# で簡単に実装できるので .NET エンジニアにはとてもおすすめです。今回の Reverse Proxy にはこの YARP を使います。

YARP の概要とデモは　2023/11 に開催された .NET Conf 2023 のセッションで紹介されています。英語ではありますが、英語が苦手な方でも自動翻訳を使うとなんとかなると思いますので是非ご覧ください。

[Reverse proxying is easy with YARP | .NET Conf 2023](https://www.youtube.com/watch?v=P8y8NAroVKk)

### Node.js アプリケーション

フロントエンドフレームワークに Next.js を使用します。WebAPI へのアクセスは CSR によるブラウザからのアクセスと Server Compoments による Node.jsからのアクセスの２パターン用意します。

### WebAPI アプリケーション

ASP.NET Core で WebAPI を立てます。上記の通り /api/* へのアクセスは全てこの WebAPI にトラフィックを流すように Reverse Proxy を設定します。

# 開発環境

- Windows 11
- Visual Studio 2022 17.11.0 Preview 1
    - .NET Aspire 8.0.1
- Docker Desktop 4.31.1 (153621)
- PowerShell 7.4.2
- Node.js 20.13.1

.NET Aspire は VSCode でも扱うことができますが、本記事では Visual Studio 2022 Preview 版を使用します。

# 1. WebAPI を ASP.NET Core で作る

## 空のソリューションを作る

新規にプロジェクトを作ると、ソリューション名とプロジェクト名が同じになってしまいますので、先に空のソリューションファイルだけを作って、そこに追加していくことにします。

新規プロジェクト作成のダイアログの検索条件に「ソリューション」と入力すると、空のソリューションが出てきますので、選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e73d9eaf-c1d7-f697-ff08-58a8f1215c67.png)

ソリューション名を NextJSAspire にしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/0cebecdd-1ef6-092d-29d6-7b1bad71de42.png)

## ASP.NET Core を作成

WebAPI 用に ASP.NET Core を作成します。ソリューションエクスプローラで先ほど作成したソリューションを右クリック → 追加 → 新しいプロジェクトを選びます。ダイアログが表示されるので、フィルターで API を選ぶと ASP.NET Core Web API プロジェクトが表示されるので、選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/051c9c7c-c8a4-5b33-29fc-cfb72bfe743a.png)

WebApi という名前のプロジェクトにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/68556796-d44f-5460-419b-4cd8794e58ee.png)

「HTTPS 用の構成」はチェックを入れます。また一番下の「.NET Aspire オーケストレーションへの参加」にも忘れずにチェックを入れましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/dae966d4-db18-437e-5057-3c5294ecd1d2.png)

:::note info
.NET Aspire ダッシュボードは https での実行がデフォルトです。そのため、AppHost プロジェクトを作成する時に「HTTPS 用の構成」のチェックをオンにしています。http での実行も可能ですが、その場合は環境変数に次の設定が必要です。

> "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"

AppHost プロジェクトの launchSettings.json を開いて profiles が http　の  environmentVariables に上の設定を追加するのが簡単です。
:::

3つプロジェクトが作成されたと思います。実行してみます。.NET Aspire のダッシュボードが表示されます。エンドポイントのリンクをクリックすると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8d7aaa55-73df-a434-b0e7-d8ab56910d88.png)

天気予報データが json 形式で取得できることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/63733c7a-d85b-048f-d15c-5c370e8d9453.png)

# 2. フロントエンドを Next.js で作る

ではここでいったん Visual Studio から離れます。ソリューションファイルを作った階層を PowerShell で開き、次のコマンドを入力します。

```powershell
npx create-next-app@latest
```

アプリケーション名は frontend としました。それ以外のオプションは以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/cf93432a-2d18-e399-2675-b8cfcc08d18e.png)

作成終了したら、実行してみましょう。

```powershell
cd frontend && npm run dev
```

http://locahost:3000 をブラウザで開くと、既定の画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/d19d5196-1548-64ea-123c-a11928639d25.png)

frontend フォルダを VSCode で開きます。

```powershell
cd frontend && code .
```

app フォルダの中に server と client というフォルダを作り、それぞれのフォルダの中に page.tsx ファイルを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c38180a8-1dda-1f3c-f56a-cd08c9353398.png)

Server フォルダの中の page.tsx は Server Compomentとして、つまり サーバーサイドから WebAPI を叩きます。 client フォルダの中の page.tsx は CSR として、つまりブラウザから WebAPI を叩くようにしていく想定です。

いきなり WebAPI を叩くのではなく、まずダミー実装をして間違いなく それぞれがサーバーとブラウザで動作することを確認します。

まずはブラウザで動作する場合です。
```tsx:app/client/page.tsx
'use client'

const Page = () => {
    console.log('running in client')

    return (
        <main>
            <div>Client Page</div>
        </main>
    )
}

export default Page
```
先頭に 'use client' と書けば、ブラウザで動きます。簡単ですね。では次に Server Component です。

```tsx:app/server/page.tsx
const Page = async () => {
    console.log('running in server')

    return (
        <main>
            <div>Server Page</div>
        </main>
    )
}

export default Page
```

既定で Server Component として動作しますので、特別な実装はありません。では実行して http://localhost:3000/client にアクセスしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/20821517-e0f2-043f-3203-e60f488b965f.png)

開発者ツールでコンソールを見ると、ログが出力されているので、ブラウザで動作していることがわかります。

では http://localhost:3000/server にアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/208e1bed-2859-b653-871c-f20535f214d8.png)

開発者ツールでコンソールを見ても何も出力されていないのでサーバー側で動いていることが確認できます。コンソールにはログが出ていますね。想定通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8fcecb9b-d675-724d-b000-cf24ea61b175.png)

## Next.js アプリケーションを .NET Aspire 管理にする

では Nex.js アプリケーションを .NET Aspire 管理にします。その前にフォルダ構成を確認します。次のようになっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/73178789-f29e-9476-f8d0-15456eda213e.png)

Node.jsプロジェクトを .NET Aspire で管理する場合、ライブラリ参照が必要です。
AppHost　プロジェクトを右クリック→追加→.NET Aspire パッケージを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b15cf8a7-5940-82a9-0a48-0dd7ec35f35f.png)

表示された NuGet パッケージマネージャー画面には既定で検索文字列が入力されています。その検索文字列に半角スペースと Node という文字列を追加すると、Node.jsをホストするためのパッケージが表示されますので、インストールします。

![AspireNodeJS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/0602200a-ebee-a7fb-a519-5b7c020ddccb.jpeg)


AppHost プロジェクトの Program.cs を開いて、次のように実装します。

```diff_c_sharp:Program.cs
var builder = DistributedApplication.CreateBuilder(args);

- builder.AddProject<Projects.WebApi>("webapi");
+ var api = builder.AddProject<Projects.WebApi>("webapi");

+ var frontend = builder.AddNpmApp(name: "frontend", workingDirectory: "../frontend", scriptName: "dev")
+     .WithHttpEndpoint(env: "PORT")
+     .WithExternalHttpEndpoints()
+     .WithReference(api);

+ if (builder.Environment.IsDevelopment() && builder.Configuration["DOTNET_LAUNCH_PROFILE"] == "https")
+ {
+     // Disable TLS certificate validation in development, see + https://github.com/dotnet/aspire/issues/3324 for more details.
+     frontend.WithEnvironment("NODE_TLS_REJECT_UNAUTHORIZED", "0");
+ }

builder.Build().Run();
```

AddNpmApp 拡張メソッドは上で説明した通りに実装しています。公開するエンドポイントを1つ用意して環境変数に"PORT"を渡しています。これは Node.js では .NET Aspire 8.0.1 の時点では必須の設定だと考えてください。WithExternalHttpEndpoints は Publish した時に外部公開することを意味する設定です。ローカル実行では何も影響はありません。そして、WithReference 拡張メソッドを使って参照先である webapi オブジェクトを渡しました。

:::note warn
開発環境での実行、及び https の呼び出し時は環境変数 NODE_TLS_REJECT_UNAUTHORIZED に0をセットすることで TLS 通信によるセキュリティチェックを無効にしています。詳しくはコメントにある

https://github.com/dotnet/aspire/issues/3324

を確認してください。2024/6/14にこの問題に対する対応はリリース8.1のBacklogに入りましたので、実装は不要になるでしょう。
:::

この時点で実行すると、ダッシュボードに Node.js アプリが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1daf72ce-3247-44c1-0dac-744d9110e2b1.png)


## Next.js アプリケーション から WebAPI を叩く: Server Component

ではまずは Node.js のサーバーから WebAPI を叩いてみます。Next.js は Server Component を使うとこれを簡単に実装できます。

Next.js の実装をする前に、少しだけ WebAPI を修正します。WebAPI のエンドポイントは必ず /api の下になるようにします。WebApi プロジェクトの Program.cs を次のように修正します。

```diff_c_sharp:WebApi/Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Add services to the container.

var app = builder.Build();

app.MapDefaultEndpoints();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

- app.MapGet("/weatherforecast", () =>
+ app.MapGet("/api/weatherforecast", () =>
{
    var forecast =  Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
});

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

:::note info
ASP.NET Core の Minimal API は本番プロジェクトや中・大規模プロジェクトでは無理、という誤解があります。それは上記のように Program.cs に全て実装してしまうサンプルばかりが目に付くからだと思われます。Minimal API をどう構成すれば実際に「使える」状態になるのか、ご興味がある方は是非こちらの記事を参考にしてください。
[ASP.NET Core Minimal API を本番でも大規模でも使えるように構成する](https://qiita.com/takashiuesaka/items/654c6e0d0bb7c92854a8)
:::

では Next.js の Server Component を次のコードに全て入れ替えます。

```tsx:app/server/page.tsx
const getData = async () => {

    const apiServer = process.env['services__webapi__https__0'] ?? process.env['services__webapi__http__0'];
    const weatherData: Response = await fetch(`${apiServer}/api/weatherforecast`, { cache: 'no-cache' });

    if (!weatherData.ok) {
        throw new Error('Failed to fetch data.');
    }

    const data = await weatherData.json();

    return data
}

const Page = async () => {
    const data = await getData()

    return <main>{JSON.stringify(data)}</main>
}

export default Page
```

getData 関数で取得した天気予報データをそのまま画面に表示しているだけです。ポイントは WebAPI のエンドポイントを環境変数で受け取っている箇所です。services__webapi__https__0 と services__webapi__http__0 という謎の環境変数名は一体どこからきたのでしょうか。

それを確認するためにもまずは実行してダッシュボードを立ち上げます。frontend 行の 詳細列 の表示リンクをクリックします。下にスクロールしていくと環境変数が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/aa1f2901-87bf-387c-9ab2-92b4a41993b3.png)

一番下にスクロールすると　services__webapi__https__0 と services__webapi__http__0 という　環境変数の値に WebAPI のホスト名が格納されていることがわかります。

.NET アプリケーションであれば .NET Aspire の　ServiceDiscovery によって環境変数を使わず https://webapi としてアクセスすることができるのですが、違う言語を使う場合はこのように環境変数を使う必要があります。

では ダッシュボードの frontend 行のエンドポイントのリンクをクリックして初期ページを表示してから、/server にアクセスしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e2f16b75-efdb-5a95-c783-ee0fcb9f36ca.png)

天気予報データが無事に取れました。

## Next.js アプリケーション から WebAPI を叩く: CSR

では今度はブラウザ側から WebAPI にアクセスしてみましょう。わかっていることですが、実装しても WebAPI にアクセスした時点で CORS でエラーとなります。言い換えると CORS エラーになれば WebAPI にアクセスする箇所の実装は正しいということになります。

CSR 用のコードを全て次のコードに入れ替えます。

```tsx:app/client/page.tsx
'use client'

import { useEffect, useState } from "react";

const getData = async () => {

    const weatherData: Response = await fetch('http://localhost:5127/api/weatherforecast', { cache: 'no-cache' });

    if (!weatherData.ok) {
        throw new Error('Failed to fetch data.');
    }

    const data = await weatherData.json();

    return data
}

const Page = () => {

    const [data, setData] = useState([])

    useEffect(() => {
        getData().then(setData)
    }, [])

    return <main>{JSON.stringify(data)}</main>
}

export default Page
```

Server Component の時とは異なり、fetch 先のホスト名に http://localhost:5127 と直接 WebAPI のホストを入力していますが、これは Reverse Proxy を導入した後で削除します。今は CORS になることを確認するためにわざと入力しています。ポート番号の5127は WebApi を作成するごとに変わりますので、そのままコピペでは動作しません。 WebApi プロジェクトの launchSettings.json ファイルを開いて、profiles が http の applicationUrl を確認してください。

実行してから http://localhost:3000/client を開くと、次のようになります。開発者ツールのコンソールを見ると CORS が発生していることがわかります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/3f6e606b-1fc8-ab1a-957a-0332d1008ea2.png)

# 3. YARP で Reverse Proxy を実装する

では CORS を解決するための Reverse Proxy を実装しましょう。

## 空の ASP.NET Core アプリケーションを追加する。

YARP はライブラリなので Web プロジェクトを新規に作成します。ソリューションファイルを右クリック → 追加 → 新しいプロジェクトを選択します。今回は ASP.NET Core（空）というプロジェクトを選びます。プロジェクトの種類で Web を選択すると見つけやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/cdbfc219-6744-4f4c-0833-19e4f73d57c9.png)


プロジェクト名は ReverseProxy としました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/efd68cc6-bed1-7906-6c58-7d3b73dc458f.png)

.NET Aspire オーケストレーションにチェックが入っていることを確認して、作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/362e1f40-951c-307c-8f8a-9ceccfd86e74.png)

## AppHost プロジェクトで参照先を実装する

AppHost プロジェクトの Program.cs を開くと、既に ReverseProxy プロジェクトが AddProject メソッドで追加されているはずです。
Reverse Proxy は Next.js, WebAPI の前に立たせますので、Next.js アプリケーションと WebAPI アプリケーションを参照するように実装します。

```diff_c_sharp:Program.cs
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.WebApi>("webapi");

var frontend = builder.AddNpmApp(name: "frontend", workingDirectory: "../frontend", scriptName: "dev")
    .WithHttpEndpoint(env: "PORT")
    .WithExternalHttpEndpoints()
    .WithReference(api);

if (builder.Environment.IsDevelopment() && builder.Configuration["DOTNET_LAUNCH_PROFILE"] == "https")
{
    // Disable TLS certificate validation in development, see https://github.com/dotnet/aspire/issues/3324 for more details.
    frontend.WithEnvironment("NODE_TLS_REJECT_UNAUTHORIZED", "0");
}

+ builder.AddProject<Projects.ReverseProxy>("reverseproxy")
+     .WithReference(frontend)
+     .WithReference(api);

builder.Build().Run();
```

AddNpmApp 拡張メソッドの戻り値を frontend 変数で受け取り、それを Reverse Proxy プロジェクトの WithReference 拡張メソッドに渡すことで、参照を構成します。同様に　WebAPI も構成します。

## Microsoft.Extensions.ServiceDiscovery.Yarp をインストールする

では Yarp を使って実装していきます。Reverse Proxy プロジェクトの Nuget パッケージの管理画面を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f97cb93f-6239-70e0-53ab-fdc30244cc23.png)

検索ボックスに ServiceDiscovery.Yarp と入力します。すると Microsoft.Extensions.ServiceDiscovery.Yarp というパッケージが見つかるので、それをインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/4dd828b1-055e-2eef-ea25-edc39634c597.png)


::: note info
Yarp を使う場合、通常は Yarp.ReverseProxy というパッケージをインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e85ba7e0-9fe8-3f4f-3e6e-6baa6a52c6a8.png)

しかし、今回はそれを使用せずに Yarp.ReverseProxy をラップした Microsoft.Extensions.ServiceDiscovery.Yarp をインストールしました。これは　.NET Aspire 用に作成された ServiceDiscovery を可能にするパッケージです。
:::

## Reverse Proxy を実装する

では、ReverseProxy プロジェクトの Program.cs を次のコードで完全に置き換えます。

```csharp:Program.cs
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddReverseProxy()
    .LoadFromMemory(GetRoutes(), GetClusters())
    .AddServiceDiscoveryDestinationResolver();

var app = builder.Build();

app.MapDefaultEndpoints();

app.MapReverseProxy();

app.Run();

RouteConfig[] GetRoutes()
{
    return
    [
        new RouteConfig
        {
            RouteId = "Route1",
            ClusterId = "default",
            Match = new RouteMatch { Path = "{**catch-all}" }
        },
        new RouteConfig
        {
            RouteId = "Route2",
            ClusterId = "api",
            Match = new RouteMatch { Path = "/api/{*any}" }
        },
    ];
}

ClusterConfig[] GetClusters()
{
    return
    [
        new ClusterConfig
        {
            ClusterId = "default",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination1", new DestinationConfig { Address = "http://frontend" } },
            }
        },
        new ClusterConfig
        {
            ClusterId = "api",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination2", new DestinationConfig { Address =  "https://webapi", Host = "localhost" } },
            }
        },
    ];
}
```

ポイントがいくつかあるので少しずつ解説します。1つ目は AddServiceDefaults メソッドの使用です。

```csharp:Program.csの一部
builder.AddServiceDefaults();

```

このメソッドを使うだけで通常は ServiceDiscovery が有効になります。 今回の場合、AppHost プロジェクトで実装した Next.js と WebAPI プロジェクトへの参照を SerivceDiscovery で解決できるようになるはずなのですが、YARP の場合はこれだけでは ServiceDiscovery できません。これについては、次で説明します。

次のポイントは AddReverseProxy メソッドです。

```csharp:Program.csの一部
builder.Services.AddReverseProxy()
    .LoadFromMemory(GetRoutes(), GetClusters())
    .AddServiceDiscoveryDestinationResolver();
```

AddReverseProxy は Yarp.ReverseProxy パッケージに含まれている YARP を使用することをパイプラインに適用するためのメソッドです。 Add したら　Use するのがパイプラインの基本なので、Use もしています。もう少し下を見ると次の実装があります。Use ではなく Map〜 となっていますけど、同じ意味です。

```csharp:Program.csの一部
app.MapReverseProxy();
```

AddReverseProxy メソッドの話に戻しましょう。

```csharp:Program.csの一部
builder.Services.AddReverseProxy()
    .LoadFromMemory(GetRoutes(), GetClusters())
    .AddServiceDiscoveryDestinationResolver();
```

LoadFromMemory メソッドは YARP による ReverseProxy の定義を行うメソッドです。GetRoutes メソッドで振り分けのルールを定義し、GetClusters メソッドで振り分け先のパスを定義しています。別にメソッドを作らなくても良いのですが、見やすさのためにメソッドに切り出しています。YARP は appSettings.json に振り分け定義を書くことも多いのですが、このようにコードで定義することもできます。.NET Aspire ではコードで全て定義できるのですから、それに合わせてコードで実装してみました。

そして AddServiceDiscoveryDestinationResolver メソッドですが、これが .NET Aspire によって追加されたメソッドです。このメソッドを叩くことで　YARP で ServiceDiscovery が機能します。 AddServiceDefaults メソッドを叩くだけではダメなので、注意が必要です。

最後のポイントは GetClusters メソッドの中です。

```csharp:Program.csの一部
ClusterConfig[] GetClusters()
{
    return
    [
        new ClusterConfig
        {
            ClusterId = "default",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination1", new DestinationConfig { Address = "http://frontend" } },
            }
        },
        new ClusterConfig
        {
            ClusterId = "api",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                { "destination2", new DestinationConfig { Address =  "https://webapi", Host = "localhost" } },
            }
        },
    ];
}
```

http://frontend, https://webapi と AppHost プロジェクトの Program.cs で定義した名称を使って振り分け先を定義しています。.NET Aspire っぽいところですね！

webapi の設定の方だけにHost = "localhost" という設定があることにお気づきでしょうか。これについてはこちらをご確認ください。 **https に対するリクエスト**時に必要です。

https://github.com/dotnet/aspire/issues/836#issuecomment-2075765325


## Next.js の CSR から WebAPI の呼び出し実装を修正

忘れちゃいけない修正がありました。 Next.js の CSR の実装では WebAPI のアクセス先のホスト名を直書きしていましたが、ReverseProxy によって /api へアクセスすれば WebAPI にトラフィックが振り分けられるようになったので、ホスト名を削除します。

```diff_tsx:app\client\page.tsx
'use client'

import { useEffect, useState } from "react";

const getData = async () => {

-     const weatherData: Response = await fetch('http://localhost:5031/api/weatherforecast', { cache: 'no-cache' });
+     const weatherData: Response = await fetch('/api/weatherforecast', { cache: 'no-cache' });

    if (!weatherData.ok) {
        throw new Error('Failed to fetch data.');
    }

    const data = await weatherData.json();

    return data
}

const Page = () => {

    const [data, setData] = useState([])

    useEffect(() => {
        getData().then(setData)
    }, [])

    return <main>{JSON.stringify(data)}</main>
}

export default Page
```

では実行してみましょう。ダッシュボードには ReverseProxy のリソースが増えています。ReverseProxy のエンドポイントのリンクをクリックして初期画面を表示後、 /client にアクセスしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/3ebfb530-4320-ddeb-1d55-46bb5f722b56.png)

ReverseProxy が動いて、WebAPI から取得したデータが表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/5b936d89-30bf-8569-1d6d-0aa61ee845a5.png)

# まとめ

一度この構成を組み上げてしまえば、AppHost プロジェクトは起動したまま、つまりダッシュボードも立ち上げっぱなしで Next.js の開発を行うことができます。
さらに参照先のサービスが増えていくと、それらも .NET Aspire で分散アプリケーションの管理することでローカルでの開発がやりやすくなるのではないでしょうか。

今回 YARP 用にプロジェクトをゼロから作成してセットアップしました。、2024/5現在、YARP による Ingress をもっと簡単にセットアップできないか議論が続いています。

[Support for YARP-based reverse proxy to use as ingress](https://github.com/dotnet/aspire/issues/836)

開発環境限定で YARP 構築用の拡張メソッドを用意してみるなど実験が続いていますので、引き続き注目したいですね。