.NET Aspire を使ってみる

# はじめに

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

- [.NET Aspire って何？ - 概要](https://qiita.com/takashiuesaka/items/f45e930ef296c5710acc)
- .NET Aspire を使ってみる
- [.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 で構成する（with YARP）](https://qiita.com/takashiuesaka/items/e167852af299a7b00939)
- [.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を既存システムに適用して実際の機能を確認する内容です。

本記事の内容は .NET Conf 2023 Recap Japan で解説した
「**.NET 8とクラウドネイティブ：一挙に掴む、必見の開発エッセンス**」
と内容の一部とほぼ同じです。動画の方では.NET AspireとDaprとの組み合わせも紹介しています。動画が良い場合はそちらをご確認ください。

[.NET Conf 2023 Recap Japan ～ 最新の.NETを学ぶ](https://info.microsoft.com/JA-ModApps-WBNR-FY24-12Dec-19-NET-Conf-2023-Recap-Japan-Latest-Learn-NET-SREVM25677_LP01-Registration---Form-in-Body.html?cmo=common_mar)

# 開発環境

- Windows 11
- Visual Studio 2022 17.11.0 Preview 1.1
    - .NET Aspire 8.0.0
- Docker Desktop 4.30.0 (149282)

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

# Frontend と Backend の分散アプリケーションを作成する

.NET Aspire の価値を感じるには既存の分散アプリケーションシステムに対して .NET Aspire を適用することです。今回は Frontend として Blazor Web App を採用し、Backend は ASP.NET Core で WebAPIを使うように構成します。

## 1. Blazor Web App を作る

Frontend として Blazor を使用します。Blazor をご存知ない方でも Blazor について深く知る必要はありませんので安心してください。テンプレートの中から Blazor Web App を選択します。これは.NET 8 で追加された Blazor 用の新しいプロジェクトテンプレートです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7d9ed9ff-dd3c-6d30-7741-7689bb83ed1b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/97155692-c022-79b1-cef1-176c72b09da9.png)

余計なトラブルを回避するために HTTPS を外し、 Include sample pages にチェックを入れます。一番下に「.NET Aspire オーケストレーションへの参加」という、いかにも .NET Aspire と関連がありそうな項目がありますが、無視します。今回は既存のアプリケーションに対して .NET Aspire を適用するシナリオだからです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/d43f9cad-b2a6-4a15-d171-e93436c798a8.png)

実行します。 Blazor ではお馴染みのサンプル画面が表示されます。

Homeページ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/6a9bf173-08ea-2d6c-6c85-21a26141659b.png)

Counterページ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e16e8da7-8491-b09b-6305-cc42291a985a.png)

Weatherページ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/03c42f76-12dd-37a0-6baa-9c890cd2b537.png)

最後の Weather ページを使って分散アプリケーションに修正していきます。Weatherページについて実装を確認しましょう。 Compoments\Pages\Weathre.razorを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f7be8b1b-5bea-6963-4ee5-9937c42787ac.png)

コードの下にある次のコードに注目します。@code のあとの { }で囲われた中は純粋なC#で実装可能なエリアです。 

```csharp:Compoments\Pages\Weather.razor
@code {
    // ランダムな天気予報の値を保持する変数
    private WeatherForecast[]? forecasts;

    // 画面初期化時に呼ばれるメソッド
    protected override async Task OnInitializedAsync()
    {
        // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(500);

        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
        forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }).ToArray();
    }

    private class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}
```

OnInitializedAsync メソッドは画面が初期化される時に呼び出されます。このメソッドの中でランダムな天気予報データを生成して forecasts 配列に格納していることがわかります。そしてこのコードの少し上を見ると次のコードがあります。

```csharp:Weathre.razor
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }

```

画面初期化時に生成したランダムな天気予報のデータを保持している forecasts配列を foreach でぐるぐる回して１つずつ要素を取り出しながらレンダリングしていることがわかります。

## 2. WebAPI を ASP.NET Coreで作成する

分散アプリケーションにするために、ランダムな天気予報データを生成するのではなく WebAPI から取得するようにします。WebAPI 用に ASP.NET Core プロジェクトを追加します。ソリューションファイルを右クリック→追加→新しいプロジェクトを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/95619220-123e-95c1-2740-93c42c02f7d2.png)

ASP.NET Core Web API プロジェクトテンプレートを選択します。
「すべての言語」「すべてのプラットフォーム」の右にあるドロップダウンで API を選択すると選びやすいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/49470616-48f4-226b-bd4a-1b2f20e6de98.png)

プロジェクト名はそのままでも良いのですが、WebAPIであることをわかりやすくするために今回は WebAPIApplication1 としました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/82ae3a73-74d3-5103-49e7-8c432a0acd14.png)

先ほどと同様にトラブルを避けるために「HTTPS用の構成」のチェックはOFFにします。新規に作成する Web API のプロジェクトでは Minimal API 形式の使用を強く推奨しますが、MVC形式の方が良い場合はコントローラーを使用することもできます。今回は「コントローラーを使用する」にチェックを入れます。

:::note info
Minimal API を本番プロジェクトや大規模プロジェクトで使用する場合の構成について別途翻訳記事を書きました。ぜひ参考にしてください。
[ASP.NET Core Minimal API を本番でも大規模でも使えるように構成する](https://qiita.com/takashiuesaka/items/654c6e0d0bb7c92854a8)
:::

またここでも一番下に「.NET Aspire オーケストレーションへの参加」と言うチェックボックスがありますが、無視します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7d35d8d0-7b5b-4d23-b7c9-1cbe56e0ae70.png)

コードを見てみましょう。Controllers\WeatherForecastController.csを開きます。
```csharp:Controllers\WeatherForecastController.cs
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
```

実行してから /weatherforecast にアクセスすると、ランダムな天気予報データを生成して返却していることがわかります。生成したデータを格納している WeatherForecast クラスはプロジェクト直下にあります。中を見てみましょう。

```csharp:WeatherForecast.cs
    public class WeatherForecast
    {
        public DateOnly Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string? Summary { get; set; }
    }
```

プロパティ宣言の順番が少しだけ違いますが、Blazor Web App の WeatherForecastクラスと全く同じ定義です。

ではこの追加した ASP.NET Core WebAPI を実行してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/4d918106-b663-cf1b-a048-e32abe9f075a.png)

json形式にシリアライズされた結果が返却されていることがわかりますね。

## 3. Blazor Web App から WebAPIを呼び出すように実装する

Blazor の天気予報データと WebAPI の天気予報データは全く同じ形式です。Blazorのコードを修正して、WebAPIを呼び出すようにします。以降の作業は　BlazorApp1プロジェクトで行います。

プロジェクト直下にWebAPIを呼び出すクラスを新規追加します。**プロジェクトを選択状態にしてから**CTRL+SHIFT+Aを押すとプロジェクト直下に簡単に項目を追加できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/78ae060e-702b-f8b4-9c31-ad731d2ff40a.png)

ファイル名（クラス名）は WebAPIClient としました。次にWebAPIから受け取った json データを WeatherForecast クラスにデシリアライズするために、Components\Pages\Weather.razor の一番下に実装してある WeatherForecast　クラスの定義を 先ほど新規作成した WebAPIClient.cs ファイルの一番下に移動し、アクセス修飾子を private から　public にします。

```csharp:WebAPIClient.cs
namespace BlazorApp1
{
    public class WebAPIClient
    {
    }

    public class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }

}
```

次は WebAPIClient が WebAPI を呼び出すように実装しましょう。HttpClient オブジェクトをコンストラクタで受け取るようにして、WebAPIを呼び出すように実装します。

```csharp:WebAPIClient.cs
namespace BlazorApp1
{
    public class WebAPIClient(HttpClient httpClient)
    {
        public async Task<WeatherForecast[]> GetWeatherForecastAsync()
        {
            return await httpClient.GetFromJsonAsync<WeatherForecast[]>("weatherforecast") ?? [];
        }
    }

    public class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }

}
```

呼び出し先が "weatherforecast"になっていますが、これは http://localhost:5050/weatherforcast を呼び出すことを意図しています。 http://localhost:5050 の箇所はこの後 Program.cs で HttpClient の DependencyInjection を実装する箇所で指定します。

もしかしたら、class 宣言の真横で HttpClient を受け取っている実装に面食らう方がいるかもしれません。これは .NET 8 で class でも使用可能になった プライマリコンストラクタというものです。また、Null 合体演算子（??）の右側にある [ ] にもびっくりした方もいるかもしれません。これも .NET 8 で実装可能になったもので空の配列を生成しています。

と言うわけで　Program.cs で HttpClient の実装をします。次のコードを追加します。

```csharp:Program.cs
builder.Services.AddHttpClient<WebAPIClient>(client =>
{
    client.BaseAddress = new Uri("http://localhost:5050");
});
```

追加する場所に注意してください。次の２箇所のいずれかに追加します。

```csharp:Program.cs
var builder = WebApplication.CreateBuilder(args);

// ここに追加するか

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// こっちでもOK

var app = builder.Build();

```


呼び出し先の http://localhost:5050 ですが、ポート番号5050は環境によって異なります。WebAPIApplication1プロジェクトの Properties\launchSettings.json を開いて確認してください。 Profiles.http.applicationUrl の 値がローカル実行時にホスティングされます。

私の場合は以下のように launchSettings.json が生成されましたので呼び出し先が http://localhost:5050 でした。
```json:Properties\lauchSettings.json
{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:17179",
      "sslPort": 0
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "weatherforecast",
      "applicationUrl": "http://localhost:5050",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "weatherforecast",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

```

では、Blazorの画面初期化時に WebAPIClient を使うように修正しましょう。まずは WebAPIClientを Weather.razor ページで使えるようにします。ファイルの三行目に次の実装を追加しましょう。この実装によって WebAPIClient インスタンスが 挿入されます。

```csharp:Compomentes\Pages\Weather.razor
@inject WebAPIClient WebAPIClient
```

今までのランダムデータの生成ロジックは全て不要になりました。次の実装に置き換えます。

```csharp:Compomentes\Pages\Weather.razor
@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await WebAPIClient.GetWeatherForecastAsync();
    }
}
```

これで実装はおしまいです。実行して試したいところですが、一つ問題があります。二つのプロジェクトは同時に稼働している必要があり、かつ WebAPI プロジェクトが先に稼働していなければなりません。これを実現するためにはスタートアップ構成を変更する必要があります。ソリューション、またはどちらかのプロジェクトを右クリックして「スタートアップ プロジェクトの構成」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/2df89871-cc3c-6329-fcea-b91e9cf63da0.png)

マルチ スタートアップ プロジェクトを選択します。右側のプロジェクト列に BlazorApp1 と WebAPIApplication1が表示されているかと思います。上に表示されているプロジェクトから順番に起動しますので、WebAPIApplication1 が上になるようします。そして、どちらのプロジェクトもアクションに開始をセットしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/938a192f-1bf6-0b86-4aee-c2c0f4d99615.png)

実行してみて Weather ページに問題なく天気予報データが表示されればOKです。Frontend と Backendの簡単な分散アプリケーションが構築できました。

# システムを .NET Aspire 管理下にする

さて、ここまでは全く .NET Aspire を使っていません。ここから .NET Aspire を適用してみます。Blazor プロジェクトを右クリック→追加→.NET Orchestorator サポートをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/88dd5e0a-4db4-7186-cb40-20ff84e5e3bd.png)

ダイアログが表示されます。AppHost と ServiceDefaults という２つプロジェクトが追加されることがわかります。OKをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/0d6e9ed7-6544-0ea7-76be-6f5b83826912.png)

２つプロジェクトが追加されたことがわかります。よく見ると AppHost プロジェクトが太文字になっています。これは AppHost プロジェクトがスタートアップ プロジェクトであることを意味します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/720e0804-81a8-4ebd-c5b5-3231c2179449.png)

スタートアップ プロジェクトの構成を開くと、シングル スタートアップ プロジェクトに構成が変更されていて、確かに AppHost プロジェクトが起動するように変更されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8a7cce02-3c75-0a68-6ade-f9b32ab7aff3.png)

Blaozr プロジェクトはこれで.NET Aspireの管理下になりました。Blaozrプロジェクトの Program.cs を開いて何が変わったのか確認してみましょう。

```diff_c_sharp:Program.cs
using BlazorApp1;
using BlazorApp1.Components;

var builder = WebApplication.CreateBuilder(args);

+ builder.AddServiceDefaults();

builder.Services.AddHttpClient<WebAPIClient>(client =>
{
    client.BaseAddress = new Uri("http://localhost:5050");
});

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

var app = builder.Build();

+ app.MapDefaultEndpoints();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
}

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();
```

builder.AddServiceDefaults() と app.MapDefaultEndpoints() の２行が追加されました。その他は変更ありません。この２つのメソッドが何をしているのかは後ほど解説します。

次に追加されたプロジェクトの１つ AppHost プロジェクトの Program.cs を開きます。

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

builder.AddProject<Projects.BlazorApp1>("blazorapp1");

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

たった3行しかありません。真ん中の行で Blazor プロジェクトを builder に追加し、３つ目の行で実行していることがわかります。

では次に WebAPI プロジェクトも .NET Aspire 管理下にします。先ほどと同様に WebAPI プロジェクトを右クリック → 追加→ .NET Orchestorator サポートをクリックします。先ほどとは異なるダイアログが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1395dce7-d3b1-3c36-9e61-63d18140a973.png)

既に AppHost と ServiceDefaults プロジェクトが存在しているため、WebAPI プロジェクトを .NET Aspire 管理下にするための更新だけを行うことを確認するためのダイアログです。OKをクリックします。

WebAPI プロジェクトの Program.cs を開いて変更箇所を確認します。

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

+ builder.AddServiceDefaults();

// Add services to the container.

builder.Services.AddControllers();

var app = builder.Build();

+ app.MapDefaultEndpoints();

// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();
```

Blazor プロジェクトの Program.cs と同じメソッド呼び出しが追加されていることがわかります。

# Frontend から Backendの呼び出し時の ServiceDiscoveryを設定する

.NET Aspire は.NET プロジェクトが http/gRPC で別サービスを呼び出すときの ServiceDiscovery 機能があります。この機能を使うことで開発環境と本番環境での実装を変更する必要がなくなります。

今回は Blazor プロジェクトが WebAPI プロジェクトの REST API を呼び出しています。この時、http://localhost:5050 を呼び出し先ホストとして指定しましたが、これは開発環境限定です。本番環境は変更する必要があるため、このままではリリースできません。通常、このような場合は呼び出し先のホストを環境変数として設定しておき、それを使用するように実装する必要があるでしょう。.NET Aspire では次のように呼び出し先などの依存関係をコードで定義することができます。

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

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

+ var api = builder.AddProject<Projects.WebAPIApplication1>("webapiapplication1");

+ builder.AddProject<Projects.BlazorApp1>("blazorapp1")
+     .WithReference(api);

- builder.AddProject<Projects.BlazorApp1>("blazorapp1");

- builder.AddProject<Projects.WebAPIApplication1>("webapiapplication1");

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

WebAPI プロジェクトを builder.AddProjectする実装を Blazor プロジェクトより上に移動してメソッドの戻り値を api 変数で受け取るようにしました。そして、その api 変数を Blazor プロジェクトの builder.AddProjectメソッドの戻り値（IResourceBuilder）に対して WithReference メソッドで api 変数を渡しました。

たったこれだけです。ここで１つ気になるのが AddProject メソッドに渡している blazorapp1, webapiapplication1 という文字列です。これは明らかにプロジェクト名ですが、実はこの文字列が ServiceDiscovery に使用されます。どういうことかはやってみるとすぐわかります。

blazorapp1を frotend に、 webapiapplication1 を backend に修正します。

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

+ var api = builder.AddProject<Projects.WebAPIApplication1>("backend");
- var api = builder.AddProject<Projects.WebAPIApplication1>("webapiapplication1");

+ builder.AddProject<Projects.BlazorApp1>("frontend")
- builder.AddProject<Projects.BlazorApp1>("blazorapp1")
    .WithReference(api);
ba
builder.Build().Run();
```

この修正によって、WebAPI プロジェクトのホストである http://locahost:5050 は http://backend として名前解決されるようになります。

Blazor プロジェクトの Program.cs を開いて修正します。

```diff_c_sharp:Program.cs
builder.Services.AddHttpClient<WebAPIClient>(client =>
{
+    client.BaseAddress = new Uri("http://backend");
-    client.BaseAddress = new Uri("http://localhost:5050");
});
```

では実行してみます。もしかすると Docker Desktop を起動しますか？というダイアログが上がってくるかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/bacdf0c6-584f-7d4c-578b-a2b2eae03f1f.png)

今は起動しなくても問題ありませんが、この後で使用するので起動しておいてください。実行に成功すると次のようなダッシュボードが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8f87eba5-5014-4210-440b-53bf5f19ce57.png)

frontend の行のエンドポイントのリンクをクリックします。すると Blazor アプリの画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e40b1c4d-50a9-9da6-264b-3abba6706a67.png)

ポート番号が変わっていることにすぐ気がつくと思います。.NET Aspire 管理下のプロジェクトのホストは.NET Aspire 側で管理されることがわかります。では Weather ページを見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/9f85cae7-3a07-e40f-f15f-970f39373912.png)

WebAPI プロジェクトの /weatherforecast から問題なくデータを取得できていることがわかります。ServiceDiscoveryも上手く動作しています。

# ダッシュボードの機能を確認する

ダッシュボードについて確認していきます。初期画面はリソースページです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/643a53d2-ce79-99a2-e352-7a0ac39b3425.png)

このページでは管理対象のプロジェクトとコンテナ、実行ファイルについての情報が表示されます。エンドポイントは文字通り各プロジェクトのエンドポイントへのリンクです。一番右列の詳細は各実行中のリソースについての情報を確認することができます。試しに frontend の詳細列の表示リンクをクリックするとこのように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/45d2be61-04d9-05f7-ce28-51a89722856a.png)

一番下に環境変数、という文字が見えています。さらにスクロールすると、このリソースが実行しているコンテキストにセットされた環境変数を見ることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1e6dca2f-c2f6-023c-370e-0ad6a475c7b4.png)

一番下の環境変数 service__backend__http__0 に 元々のホストである http://localhost:5050 が格納されていることがわかりますね。OTEL_〜〜となっているのは全て OpenTelemetryについての設定値です。

ログ列の表示リンクをクリックすると 各プロジェクトのコンソールログを表示する画面に遷移します。
これは左サイドメニューのコンソールをクリックした場合のショートカットです。frontend のログ列の表示リンクをクリックしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/320c612b-a69c-f97c-a090-74de12dbaa8d.png)

コンソール画面に遷移して、frontend が選択された状態であることがわかります。

では次に左サイドメニューの構造化を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c60d4b22-156b-3842-b66c-c6896c931da1.png)

frontend, backend 両方の全てのログが表示されます。ドロップダウンで frontend のみ、 backend のみを見るように簡単に絞り込めますし、Error や Information などのログレベルでも絞り込みできます。また文字列で素早い検索も可能です。

詳細列の表示リンクをクリックするとログの詳細を見ることができます。では次に左サイドメニューのトレースをクリックしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/a33e3ea4-28fa-a752-bff1-20db72fd7ba9.png)

Trace では複数のサービスの呼び出しを TraceId で紐付けてパフォーマンスを確認することができます。名前が 
 frontend:GET /weather となっている行を見ると、frontend の横に backend の表示があることがわかります。これが　Weather ページをクリックした時に backend へ裏側で backend にアクセスしていることを表しています。 詳細列の表示リンクをクリックしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/2eff37d7-cf7f-537a-4d36-ce4734160926.png)

このように同じ Trace IDを持つログを呼び出し順に並べてパフォーマンスを調べることができます。一番下の backend の行をクリックしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/a1705149-cf1d-7553-b5d4-d0459c794cb7.png)

該当の構造化ログが表示されますので、詳細をこの画面のままで確認することができます。どのパスへアクセスしているのかがわかります。

最後に左サイドメニューのメトリックをクリックしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/140a55c4-2ac5-f39c-5263-87c65a670e4a.png)

ASP.NET Core や System.Net.Http、OpenTelemteryが出力するメトリクスをこの画面でグラフ化したものを確認することができます。.NET プロジェクトを使用した開発において、これほど簡単に OpenTelemetryのテレメトリデータを確認できる手段は他にないでしょう。

# SerivceDefaults プロジェクトを確認する

.NET Aspire 管理下にした時に２つのプロジェクトが追加されました。１つは AppHost でもう１つは ServiceDefaults です。AppHostはServiceDiscoveryのセットアップするときに少し触りましたが、ServiceDefaults についてはまだ何も触れていません。ここから ServiceDefaults についてみていきましょう。

まず、SerivceDefaults プロジェクトの中を見ると Extensions というファイルが１つしかありません。これを開きます。すると、Static Class の中に5つの拡張メソッドが定義されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ec771b0e-8cdb-1411-df46-338a77d8b31d.png)

AddServiceDefaults 拡張メソッドと MapDefaultEndpoints 拡張メソッドは Blazor と WebAPI プロジェクトを .NET Aspire 管理にした時に Program.cs に追加された2行で使われていたメソッドです。

まずここからわかることは SerivceDefaultsプロジェクトは.NET Aspire 管理下に置いたプロジェクトが共通して呼び出すメソッドを実装してある、ということです。

## SerivceDefaults を確認する：AddServiceDefaults 拡張メソッド

では AddSerivceDefaults メソッドの中身をみてみましょう。

```csharp:Extensions.cs
    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();

        builder.AddDefaultHealthChecks();

        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            // Turn on resilience by default
            http.AddStandardResilienceHandler();

            // Turn on service discovery by default
            http.UseServiceDiscovery();
        });

        return builder;
    }
```

最初に出てくる ConfigureOpenTelemtery メソッドも、AddDefaultsHealthChecks メソッドも、同じ Extensions クラス内に定義されているのでとてもソースを追いかけやすくなっていますので是非ソースコードを確認してみてください。難しい実装はどこにもありません。

ConfigureOpenTelemtery メソッドではその名の通り、OpenTelemteryに関する設定が行われています。

```csharp:Extensions.cs
    public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
    {
        builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation();
            })
            .WithTracing(tracing =>
            {
                tracing.AddAspNetCoreInstrumentation()
                    // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
                    //.AddGrpcClientInstrumentation()
                    .AddHttpClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();

        return builder;
    }
```

最初に builder.Logging でログの設定としてOpenTelemetyを使用する実装をしています。次に builder.Serivces.AddOpenTelemtery メソッドでメトリクスとトレースに設定をしていることがわかりますね。そして最後にOpenTelemteryのExporter設定をしているようです。

その最後のAddOpenTElemteryExporters メソッドは Extensions.cs 内に実装された Private 拡張メソッドです。

```csharp:Extensions.cs
    private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
    {
        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);

        if (useOtlpExporter)
        {
            builder.Services.AddOpenTelemetry().UseOtlpExporter();
        }

        // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
        //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
        //{
        //    builder.Services.AddOpenTelemetry()
        //       .UseAzureMonitor();
        //}

        return builder;
    }
```

環境変数OTEL_EXPORTER_OTLP_ENDPOINTにOpenTelemtery用のエンドポイントが設定されている場合はログ・メトリクス・トレース設定をOpenTelemteryを使用するように設定していますね。

コメントに注目してください。コメントアウトされている実装は OpenTelemtery 仕様のデータを AzureMonitor に公開する実装例です。ご丁寧に必要なパッケージ名まで書いてくれていますね。

余談ですが .NET 8 から　テレメトリは OpenTelemetery を使用するようにかなりの機能強化が入りました。それによってこのように好きな APM（Application Performace Management）ツールを使用しやすくなっています。

では再度 AddSerivceDefaults 拡張メソッドの実装に戻りましょう。

```csharp:Extensions.cs
    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();

        builder.AddDefaultHealthChecks();

        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            // Turn on resilience by default
            http.AddStandardResilienceHandler();

            // Turn on service discovery by default
            http.UseServiceDiscovery();
        });

        return builder;
    }
```

次に注目していただきたいのは http.AddStandardResilienceHandler() です。ここで、Resiliency（回復力）つまり Retry や CircuitBreaker、Timeoutなどの既定の設定が行われています。そのため、設定値をカスタマイズするのもここで行うことになります。

この AddStandardResilienceHandler メソッドはコードとして実装が提供されていません。というのも、このメソッドは .NET Aspire の独自メソッドではなく、 Microsoft.Extensions.Http.Resilience パッケージに定義されているクラスの拡張メソッドだからです。.NET Aspireはそれを使用しているだけに過ぎません。

AddStandardResilienceHandlerメソッドについては MS Learn に説明があります。

[標準の回復性ハンドラーを追加する](https://learn.microsoft.com/ja-JP/dotnet/core/resilience/http-resilience?tabs=dotnet-cli#add-standard-resilience-handler)

残念ながらこの MS Learn の説明はわかりにくいので、次の MS公式 Blog を読むことをお勧めします。

[Building resilient cloud services with .NET 8](https://devblogs.microsoft.com/dotnet/building-resilient-cloud-services-with-dotnet-8/)

英語ですが、最近の翻訳はとてもよくできているので英語が苦手な方でも翻訳機能を使えば読めると思います。


## SerivceDefaults を確認する：MapDefaultEndPoints 拡張メソッド

プロジェクトを .NET Aspire 管理にすると、2行コードが追加されました。1つ目は builder.AddServiceDefaults メソッドを呼び出す実装で、もう１つは app.MapDefaultEndpoints メソッドを呼び出す実装でした。この MapDefaultEndpoints メソッドは ServiceDefaults プロジェクトの Extensions クラスに拡張メソッドとして定義してあります。コードをみてみましょう。

```csharp:Extensions.cs
    public static WebApplication MapDefaultEndpoints(this WebApplication app)
    {
        // Adding health checks endpoints to applications in non-development environments has security implications.
        // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
        if (app.Environment.IsDevelopment())
        {
            // All health checks must pass for app to be considered ready to accept traffic after starting
            app.MapHealthChecks("/health");

            // Only health checks tagged with the "live" tag must pass for app to be considered alive
            app.MapHealthChecks("/alive", new HealthCheckOptions
            {
                Predicate = r => r.Tags.Contains("live")
            });
        }

        return app;
    }
```

MapHealthChecks メソッドを使ってヘルスチェックの Endpoint を定義しています。MapHealthChecks メソッドを使う場合、事前に builder.Services.AddHealthChecksメソッドを叩いておく必要があります。

ここでもう一度 AddSerivceDefaultsメソッドのコードを確認してみましょう。
```csharp:Extentions.cs
    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();

        builder.AddDefaultHealthChecks();

        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            // Turn on resilience by default
            http.AddStandardResilienceHandler();

            // Turn on service discovery by default
            http.UseServiceDiscovery();
        });

        return builder;
    }
```

builder.AddDefaultHealthChecks というメソッドを叩いていることがわかります。このメソッドは Extensions クラス内に実装された メソッドです。コードを見てみましょう。

```csharp:Extensions.cs
    public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
    {
        builder.Services.AddHealthChecks()
            // Add a default liveness check to ensure app is responsive
            .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);

        return builder;
    }
```

builder.Services.AddHealthChecks メソッドを叩いてますね。ASP.NET Core のヘルスチェックもまた.NET Aspire の独自機能ではありません。詳しい解説は MS Learn をご確認ください。

[ASP.NET Core の正常性チェック](https://learn.microsoft.com/ja-jp/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-8.0)

Exntentions クラスの実装のほとんどをご紹介してきました。残るは Service Discovery です。この実装は唯一 .NET Aspire 用に拡張が入っている箇所です。.NET Aspire の Servivce Discovery はとても有用でもし.NET Aspire は使用できない場合であっても、Service Discovery だけは使用したいぐらい便利です。別途記事を書きましたので、そちらをご確認ください。

[.NET の Service Discovery だけを使いたい](https://qiita.com/takashiuesaka/items/d04073c086fa05cd4128)

## .NET Aspire パッケージとは

WebAPI プロジェクトを .NET Aspire 管理する時、プロジェクトファイルを右クリック → 追加 → .NET  Orchestratior サポート、を選択しました。この時、「.NET Aspire パッケージ」という項目がメニューにあったことに気づかれた方もいるでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ecf6d279-1aa0-2ce3-fc9d-602dd5005d69.png)

.NET Aspire パッケージとはなんでしょうか。考えるよりもやってみたほうが早いです。Blazor プロジェクトを右クリック → 追加 → .NET Aspire パッケージを選択してみましょう。


.NET パッケージマネージャーが立ち上がり、検索条件に owner:Aspire tags:component という文字列が入力されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/32bb98f5-56ab-e096-30ec-e22f47f3d27c.png)

ここに並んでいるパッケージ名から、何かのリソースを使うことを想定したパッケージであることが想像できると思います。.NET Aspire パッケージは、何かのリソースを使う場合に必要なパッケージをまとめたものです。

例えば Database など、何かのリソースを扱う時に、Nuget パッケージを使うことはわかるとしても、どのパッケージを使えばいいのかわからないことはよくあります。また、Resiliency を実装する場合は別のパッケージが必要でしょう。それらを１つにまとめたものが .NET Aspire パッケージです。また、物によってですが便利な拡張メソッドが用意されている場合があります。

## 出力キャッシュを追加してみる

では .NET Aspire パッケージを実際に使ってみましょう。今回は .NET 8 で追加された Redis を使用した出力キャッシュを実装してみます。

Blazor プロジェクトを右クリック → 追加 → .NET Aspire パッケージをクリックします。表示された中から Aspire.StackExchange.Redis.OutputCaching を選択し、インストールをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7051d65a-5c0c-5dfe-2f0b-e2b18670aa0f.png)

変更のプレビューダイアログと、ライセンスの同意ダイアログがポップアップします。HealthCheck 用のパッケージも同時にインストールしていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/d6bd4842-f803-cb29-c7f2-e7da109954b9.png)

ではBlaozr プロジェクトの Program.cs で Redis の出力キャッシュを使用するように設定しましょう。
次のように2行追加してください。

```diff_c_sharp:Program.cs
using BlazorApp1;
using BlazorApp1.Components;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddHttpClient<WebAPIClient>(client =>
{
    client.BaseAddress = new Uri("http://backend");
});

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

+ builder.AddRedisOutputCache("cache");

var app = builder.Build();


app.MapDefaultEndpoints();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
}

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

+ app.UseOutputCache();

app.Run();
```

最初の追加した実装、builder.AddRedisOutputCache("cache");の AddRedisOutputCacheメソッドは簡単に Redis 出力キャッシュを使用できるようにした .NET Aspire の独自メソッドです。

では次に ローカル環境で Redis を使うようにセットアップします。.NET Aspire はDocker Desktopを使用して特定の依存リソースを用意する機能があります。Redis はその対象となっていますので、その便利さを体感しましょう。

先ほどと同様に AppHost プロジェクトを右クリックして、.NET Aspire パッケージを選択します。
NuGet パッケージ管理ダイアログには既に検索文字列が入っています。その後ろに半角スペースと redis と入力すると、Aspire.Hosting.Redis というパッケージが見つかります。インストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/a34d2c8b-de36-e06e-fcd2-a3973ddf3b72.png)

AppHost プロジェクトの Program.cs を開いて次のように追加・修正します。
```diff_c_sharp:Program.cs
var builder = DistributedApplication.CreateBuilder(args);

+ var cache = builder.AddRedis("cache");

var api = builder.AddProject<Projects.WebAPIApplication1>("backend");

builder.AddProject<Projects.BlazorApp1>("frontend")
-     .WithReference(api);
+     .WithReference(api)
+     .WithReference(cache);

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

Redis 以外にも Mongo, RabbitMQ, MySQL, PostgreSQL, SQLServer, Oracleや Azure CosmosDBや Storage のコンテナを扱うことができます。扱えるリソースの種類はどんどん追加されています。

では最後の修正です。出力キャッシュはURL、ページごとに適用の有無を実装する必要があります。Razorページの場合は OutputCache属性を使います。

[1 つのエンドポイントまたはページを構成する](https://learn.microsoft.com/ja-jp/aspnet/core/performance/caching/output?view=aspnetcore-8.0#configure-one-endpoint-or-page)

Components\Pages\Weathre.razorを開いて、次のように2行追加します。

```diff_c_sharp:Components\Pages\Weathre.razor
+ @using Microsoft.AspNetCore.OutputCaching;
@page "/weather"
@attribute [StreamRendering]
+ @attribute [OutputCache(Duration = 5)]
@inject WebAPIClient WebAPIClient

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
       forecasts = await WebAPIClient.GetWeatherForecastAsync();
    }

}
```

OutputCache(Duration = 5) の Duration = 5、とすることで5秒間キャッシュを維持します。

では、実行しましょう・・・・の前に、Docker Desktopは起動していますでしょうか。起動していなかったら起動しておいてください。

実行して Weather ページを表示してみます。初回は Redis コンテナが立ち上がるまで少し時間がかかると思います。一度天気予報データが表示されたら、リロードを繰り返してみてください。５秒間はキャッシュが効くので同じデータが表示されますが、5行経過すると新しいデータが表示されるはずです。

ダッシュボードの開いて挙動を確認しましょう。リソースの中に種類が Container という行が増えていることがわかります。Redis コンテナですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f739e15b-1ae9-e502-a810-b0aff67dc7cf.png)


Docker Desktopも確認してみましょう。 Redis の 7.2 ラベルのイメージがダウンロードされてコンテナが立ち上がっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/94a8d1e4-7066-5b3c-a6bc-c9fb91258b22.png)

次に Traces ページを開きます。右上の検索で /weather と入力しましょう。 一番上に表示されたトレースの表示リンクをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/4c6c767c-5bb3-e239-b433-6602a82ca965.png)

これは Weather ページを初回に訪れた時の Trace です。最初に Cache の存在を確認し、Cache がなかったのでbackend にアクセスしました。そして Cache に保存していることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ceb4eee9-6ed0-4644-05dd-a57521de0b67.png)

とても簡単に Redis を使った出力キャッシュの環境を整えて、実装することができました。アプリケーションを停止すると、起動した Redis コンテナは自動的に削除されます。

# まとめ

.NET Aspire は３つの機能があります。実際に使ってみて、以下の３つが提供されていることが確認できました。

**1. 分散アプリケーションのローカル開発環境対応**

- ServiceDiscovery機能でサービス間の名前解決しくれます。Redisなどのコンテナで提供されているリソースは起動時に自動的に立ち上げてくれます。

**2. OpenTelemetryによる監視設定のテンプレートコードとダッシュボードの提供**

- Service Defaults プロジェクトに OpenTelemtery の監視設定のコードが実装されていて、自由にカスタマイズができます。また、ダッシュボードを提供することによってパフォーマンスチェックやバグの原因追及をしやすくしています。

**3. 回復力・復元力（Resiliency）設定の実装**

- Serivce Defaluts プロジェクトで ASP.NET Core の Resiliency 実装がテンプレートに含まれていました。それによって .NET Aspire 管理下のプロジェクトは全て自動的に Resiliency 対応していることになります。

.NET Aspire は2024/5 に開催された Microsft Build 2024 で GA しました。ぜひどんな感じなのかを実際に使ってみて、本番プロジェクトでも採用していただけると嬉しいです。