LoginSignup
7
7

.NET Aspire を使ってみる

Last updated at Posted at 2023-12-28

はじめに

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

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


この記事は.NET Aspireを既存システムに適用して実際の機能を確認する内容です。

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

.NET Conf 2023 Recap Japan ~ 最新の.NETを学ぶ

開発環境

  • Windows 11
  • Visual Studio 2022 17.10.0 Preview 1.0
  • Docker Desktop 4.26.1 (131620)

.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

image.png

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

image.png

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

Homeページ
image.png

Counterページ
image.png

Weatherページ
image.png

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

image.png

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

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 配列に格納していることがわかります。そしてこのコードの少し上を見ると次のコードがあります。

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 でぐるぐる回して1つずつ要素を取り出しながらレンダリングしていることがわかります。

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

分散アプリケーションにするために、ランダムな天気予報データを生成するのではなく WebAPI から取得するようにします。WebAPI 用に ASP.NET Core プロジェクトを追加します。ソリューションファイルを右クリック→追加→新しいプロジェクトを選択します。
image.png

ASP.NET Core Web API プロジェクトテンプレートを選択します。
「すべての言語」「すべてのプラットフォーム」の右にあるドロップダウンで API を選択すると選びやすいです。
image.png

プロジェクト名はそのままでも良いのですが、WebAPIであることをわかりやすくするために今回は WebAPIApplication1 としました。
image.png

先ほどと同様にトラブルを避けるために「HTTPS用の構成」のチェックはOFFにします。既定ではコントローラーを使用するようになっていませんが、実際のプロジェクトではコントローラーを使用することが多いと思いますので「コントローラーを使用する」にチェックを入れます。

またここでも一番下に「.NET Aspire オーケストレーションへの参加」と言うチェックボックスがありますが、無視します。
image.png

コードを見てみましょう。Controllers\WeatherForecastController.csを開きます。

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 クラスはプロジェクト直下にあります。中を見てみましょう。

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

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

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

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

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

image.png

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

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を呼び出すように実装します。

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 の実装をします。次のコードを追加します。

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

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

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 でした。

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 インスタンスが 挿入されます。

Compomentes\Pages\Weather.razor
@inject WebAPIClient WebAPIClient

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

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

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

これで実装はおしまいです。実行して試したいところですが、一つ問題があります。二つのプロジェクトは同時に稼働している必要があり、かつ WebAPI プロジェクトが先に稼働していなければなりません。これを実現するためにはスタートアップ構成を変更する必要があります。ソリューション、またはどちらかのプロジェクトを右クリックして「スタートアップ プロジェクトの構成」を選択します。
image.png

マルチ スタートアップ プロジェクトを選択し、新しい起動プロファイルの作成をクリックします。一番左のアイコンです。
image.png

右側のプロジェクト列に BlazorApp1 と WebAPIApplication1が表示されているかと思います。上に表示されているプロジェクトから順番に起動しますので、WebAPIApplication1 が上になるようします。そして、どちらのプロジェクトもアクションに開始をセットしてください。
image.png

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

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

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

image.png

ダイアログが表示されます。AppHost と ServiceDefaults という2つプロジェクトが追加されることがわかります。OKをクリックします。
image.png

2つプロジェクトが追加されたことがわかります。よく見ると AppHost プロジェクトが太文字になっています。これは AppHost プロジェクトがスタートアップ プロジェクトであることを意味します。
image.png

スタートアップ プロジェクトの構成を開くと、シングル スタートアップ プロジェクトに構成が変更されていて、確かに AppHost プロジェクトが起動するように変更されています。
image.png

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

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() の2行が追加されました。その他は変更ありません。この2つのメソッドが何をしているのかは後ほど解説します。

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

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

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

builder.Build().Run();

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

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

image.png

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

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

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 を開いて、次のように修正します。

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 変数を渡しました。

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

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

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);

builder.Build().Run();

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

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

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

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

image.png

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

image.png

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

image.png

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

image.png

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

Preview 1からPreview2にUpgradeする

先ほどのダッシュボードは Preview 1バージョンのものです。2023/12にリリースされた Preview 2では見た目が少しシンプルに変更されていますが、機能は同じです。Preview 2 への Upgrade はとても簡単です。AppHost プロジェクトと ServiceDefaults プロジェクトのそれぞれの Nuget パッケージマネージャーを開き、更新対象のパッケージを全て更新するだけです。

AppHost プロジェクトの Nuget パッケージマネージャ
image.png

ServiceDefaults プロジェクトの Nuget パッケージマネージャー
image.png

更新後、ソリューション単位でクリーン → リビルドしてから実行しましょう。次のように Preview 2 のダッシュボードが表示されます。

image.png

Preview 3にUpgradeする

2024/2に Preivew 3がリリースされました。既存プロジェクトのUpgradeは次のページに手順が載っています。

既存のアプリを更新する

プロジェクトを最新にする前に次の作業をしましょう。
Visual Studio 2022 Preview をご利用の場合は Visual Studio 2022 Previewを最新にしてすると、.NET Aspire が最新になります。 Mac 環境などをご利用の場合は次のコマンドでワークロードを更新します。

sudo dotnet workload update

既存のプロジェクトの更新は参照している .NET Aspire のバージョンを次のバージョンに新しくするだけです。

8.0.0-preview.3.24105.21

Nugetパッケージマネージャーで更新するのが簡単でしょう。

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

Preview 2 のダッシュボードについて確認していきます。初期画面は Resources ページです。
※Preivew 3 のダッシュボードは画面に少し機能が追加されていますが、使い方は Preview 2と同じです。

image.png

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

image.png

一番下の環境変数 service__backend__1 に 元々のホストである http://localhost:5050 が格納されていることがわかりますね。

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

image.png

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

では次に左サイドメニューの Structured Logs を見てみましょう。
image.png

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

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

image.png

Trace では複数のサービスの呼び出しを TraceId で紐付けてパフォーマンスを確認することができます。一番下の Trace を見ると Name が frontend:GET /weather となっていることがわかります。これが Weather ページをクリックした時のトレース、ということです。 View をクリックしてみましょう。

image.png

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

image.png

該当の Structured Log が表示されますので、詳細をこの画面のままで確認することができます。ポート番号が5050であることがわかりますね。

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

image.png

ASP.NET Core や System.Net.Http、OpenTelemteryが出力するメトリクスをこの画面でグラフ化したものを確認することができます。

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

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

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

image.png

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

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

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

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

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に関する設定が行われています。

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.AddRuntimeInstrumentation()
                       .AddBuiltInMeters();
            })
            .WithTracing(tracing =>
            {
                if (builder.Environment.IsDevelopment())
                {
                    // We want to view all traces in development
                    tracing.SetSampler(new AlwaysOnSampler());
                }

                tracing.AddAspNetCoreInstrumentation()
                       .AddGrpcClientInstrumentation()
                       .AddHttpClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();

        return builder;
    }

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

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

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

        if (useOtlpExporter)
        {
            builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
        }

        // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // builder.Services.AddOpenTelemetry()
        //    .WithMetrics(metrics => metrics.AddPrometheusExporter());

        // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.Exporter package)
        // builder.Services.AddOpenTelemetry()
        //    .UseAzureMonitor();

        return builder;
    }

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

コメントに注目してください。最初のコメントアウトされている実装は OpenTelemtery 仕様のデータをPrometheus に公開するための設定です。下の実装は AzureMonitor を使う場合です。ご丁寧に必要なパッケージ名まで書いてくれていますね。

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

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

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 に説明があります。

標準の回復性ハンドラーを追加する

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

Building resilient cloud services with .NET 8

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

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

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

Extensions.cs
    public static WebApplication MapDefaultEndpoints(this WebApplication app)
    {
        // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // app.MapPrometheusScrapingEndpoint();

        // 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メソッドのコードを確認してみましょう。

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 クラス内に実装された メソッドです。コードを見てみましょう。

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 の正常性チェック

Exntentions クラスの実装のほとんどをご紹介してきました。残るは Service Discovery です。この実装は唯一 .NET Aspire 用に拡張が入っている箇所です。いずれ誰かが Deep Dive してくれるのではないかと期待していますが、気が向いたら自分で記事を書くかもしれません。

.NET Aspire コンポーネントとは

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

image.png

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

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

image.png

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

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

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

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

Blazor プロジェクトを右クリック → 追加 → .NET Aspire コンポーネントをクリックします。

image.png

ライセンスの同意ダイアログがポップアップします。一番下に HealthCheck用のパッケージも同時にインストールしていることがわかります。
image.png

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

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 プロジェクトの Program.cs を開いて次のように追加・修正します。

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

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

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

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

builder.Build().Run();

Preview 3の現時点で、Redis 以外にも Mongo, MySQL, PostgreSQL, RabbitMQ, SQLServerのコンテナを自動的に扱ってくれます。

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

1 つのエンドポイントまたはページを構成する

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

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秒間はキャッシュが効くので同じデータが表示されますが、5行経過すると新しいデータが表示されるはずです。

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

image.png

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

image.png

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

image.png

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

image.png

とても簡単に Redis を使った出力キャッシュの環境を整えて、実装することができました。

まとめ

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

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

  • ServiceDiscovery機能を提供し、Redisなどのコンテナで提供可能はリソースを自動的に構築してくれることがわかりました。

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

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

3. 回復力・復元力(Resiliency)設定の実装

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

.NET Aspire は2024/2時点ではまだ Preview 3です。GA は2024年の4月〜6月を予定しています。それまでにどんどん機能拡張されていくと思いますので、目が離せないですね。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7