.NET Aspire でデータベースを扱う - PostgreSQL編

この記事は .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 で構成する（with YARP）](https://qiita.com/takashiuesaka/items/e167852af299a7b00939)
- .NET Aspire でデータベースを扱う - PostgreSQL編
- [.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)

# PostgreSQL サーバーを使用する

.NET Aspire は データベースのサポートもかなり手厚いです。.NET Aspire は分散アプリケーション開発を楽にするための機能としてローカル環境でコンテナの扱いが得意です。そのため、ローカル開発ではコンテナのDBを使用し、クラウドにデプロイした後はマネージドのDBを使う、というシナリオにも対応しています。

この記事では PostgreSQL を使う場合の.NET Aspire の使い方についてご紹介します。

## 開発環境

- Windows 11
- Visual Studio 2022 17.11.0 Preview 2
    - .NET Aspire 8.0.1
- Docker Desktop 4.31.1 (153621)
- PowerShell 7.4.2
- Azure Developer CLI 1.9.3

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

## 1. .NET Aspire Starter アプリケーションを作成する

今回は .NET Aspire Starter アプリケーションをベースにアプリケーションを少しずつ段階を踏んで PostgreSQL 対応していきます。そのため、この記事に辿り着いた方にとって必要な情報がどこかにあると思います。

まずは .NET Aspire Starter アプリケーションを作成します。「プロジェクトの種類」検索ボックスで.NET Aspire を選ぶとすぐ表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/0452ee6e-50cf-e87e-5db7-ff2bbe56decf.png)

この次の次の画面で Redis Cache を使用するかを選択するダイアログが表示されます。使用してもしなくてもどちらも構いませんが、この記事では使用していません。

.NET Aspire Starter アプリケーションには天気予報データを表示する画面があります。この画面では 天気予報データをランダムに作成する ApiService プロジェクトに対して Blazor Web プロジェクトからアクセスしてそれを画面に表示します。

Blazor Web プロジェクトの天気予報画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/30eab90f-88a9-c2bb-ea55-46f72bc118bd.png)

ApiService のエンドポイントに直接アクセスして結果を確認することもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ee7012c0-fc01-3afa-fca4-eb61f52900c8.png)

まずは ApiService プロジェクトを修正して、 PostgreSQL サーバーにアクセスして天気予報データを取得した結果を返却するようにします。

## 2. PostgreSQL サーバーを作成し、データを用意する

どこかに PostgreSQL サーバーを構築します。この記事では最後に Azure にデプロイしますので、Azure 上に Azure Database for PostgreSQL Flexible サーバーを構築しました。

**testdb**、というデータベースを作成した後、その中に Weatherforecasts テーブルを作って天気予報データを挿入しておきます。

```sql
CREATE TABLE IF NOT EXISTS "WeatherForecasts"
(
    "Id" uuid NOT NULL,
    "Date" date NOT NULL,
    "TemperatureC" integer NOT NULL,
    "Summary" text COLLATE pg_catalog."default",
    CONSTRAINT weatherforecast_pkey PRIMARY KEY ("Id")
);

INSERT INTO "WeatherForecasts"(
	"Id", "Date", "TemperatureC", "Summary")
VALUES
('6555da29-89f1-4207-b20d-bd8d507e7c32', '2024-03-25', 17, '肌寒い'),
('b3d43a4a-c877-4580-89af-2e26ed7e68e2', '2024-03-22', 9, '寒い'),
('b57deef9-1d55-496d-a4ac-1ac88c93a4e2', '2024-03-23', 14, '寒い'),
('c84c8347-e6f4-44da-a27d-9f5c0f6ca3dd', '2024-03-21', 7, '寒い'),
('ee4f05a1-2435-44b6-b868-5434cc87bcfa', '2024-03-24',11, '寒い')
ON CONFLICT DO NOTHING;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/3748c8df-66a9-78b0-fa92-2b959b06cbd4.png)

## 3. PostgreSQL 用の .NET Aspire コンポーネントをインストールする

ソリューションエクスプローラーの ApiService プロジェクトを右クリック→追加→.NET Aspire パッケージを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/90ff9934-df8e-e410-fdde-859b29cc9574.png)

Nuget パッケージマネージャーが立ち上がり、 .NET Aspire パッケージだけが表示されるように検索条件が入力された結果が表示されますので、検索条件に追加で半角スペース＋Postgreと入力します。すると、PostgreSQL用の.NET Aspire コンポーネントだけが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c9a150f0-bbcd-583b-dc18-baacddf79370.png)

今回は EntityFrameworkCore を使用しますので、Aspire.Npgsql.EntityFrameworkCore.PostgreSQL をインストールします。

## 4. 接続文字列をコード内にハードコーディングする

一度の沢山の実験をするとトラブルが起こった場合に原因特定が難しくなります。まずはインストールした PostgreSQL 用の.NET Aspire コンポーネントが正しく動作することを確認するため、接続文字列はコード内にハードコーディングして問題なく PostgreSQL に接続できることを確認します。

ApiService プロジェクトの Program.cs を開きます。一番を下までスクロールするとランダム生成した天気予報データを格納する WeatherForecast レコードの宣言があります。これに Guid 型の Id 列を追加します。

```diff_c_sharp:ApiService/Program.cs
- record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
+ record WeatherForecast([property: Key]Guid Id, DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

Keyクラスが見当たらないくてエラーとなりますので、Program.cs の一番上に次の実装を追加します。

```csharp:ApiService/Program.cs
using System.ComponentModel.DataAnnotations;
```

[property: Key]Guid Id という実装に見慣れない方がきっといらっしゃると思います。プライマリコンストラクタは引数から自動的にプロパティを生成します。そのプロパティに属性をつける場合、[property: 属性クラス]と言う実装をします。つまり [property: Key]Guid Id は

```csharp
[Key]
public Guid Id { get; }
```

と同じです。

次に EntityFrameworkCore を使って DB アクセスするために、DbContextを継承したクラスを実装します。今回は ApiService プロジェクトの Program.cs の一番下に実装します。

```csharp:ApiService/Program.cs
internal class WeatherForcastDbContext : DbContext
{
    public WeatherForcastDbContext() { }
    public WeatherForcastDbContext(DbContextOptions<WeatherForcastDbContext> options) : base(options) { }

    public DbSet<WeatherForecast> WeatherForecasts { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Server=<serverurl>;Database=testdb;Port=5432;User Id=<userid>;Password=<password>;Ssl Mode=Require;");
    }
}
```

接続文字列はご自身の環境に合わせて変更してください。Database=testdb、をお忘れなく。

/weatherforecast にアクセスされた場合、テンプレートではランダム生成した天気予報データを返却するようになっているのですが、 PostgreSQL からデータを取得するように変更します。

```diff_c_sharp:ApiService/Program.cs
- app.MapGet("/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();
+ app.MapGet("/weatherforecast", async () =>
+ {
+     var forecast = await new WeatherForcastDbContext().WeatherForecasts.ToListAsync();
+ 
+     return forecast;
+ });
```

AppHostを起動して無事にデータが取得できるかどうかを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b8e82385-6e35-883c-7e0d-a8f33aeb6831.png)

Summary に日本語が入っているデータは PostgreSQL に格納されているデータです。ランダム生成ではないので、リロードしても常に同じデータしか表示されませんが、それで正しく実装されていることがわかります。

## 5. 接続文字列を設定ファイルに外出しする

コード内にハードコードした接続文字列を設定ファイルに移動します。これに伴い、先ほど作成した DbContext を継承する WeatherForcastDbContext を DI　で受け取るように修正します。

ApiService プロジェクトの appsettings.Development.jsonを開きます。appsettings.jsonに隠れているので、白三角をクリックして開くと見えるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/304df6a6-88bb-1532-584c-26e1772548de.png)

次のように設定ファイルに接続文字列を設定します。ConnectionStrings の Key として "weatherdb" を使用することにします。このKey名はとても重要で、この後色んなところで使用します。

```diff_json:ApiService/appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
- }
+ },
+ "ConnectionStrings": {
+   "weatherdb": "Server=<serverurl>;Database=testdb;Port=5432;User Id=<userid>;Password=<password>;Ssl Mode=Require;"
+ }
}
```

WeatherForcastDbContext にハードコードしていた接続文字列を使用する実装を削除します。また、DI で WeatherForcastDbContext オブジェクトを受け取るため、既定の（パラメータ無しの）コンストラクタは不要になります。同時に削除しておきます。

```diff_c_sharp:ApiService/Program.cs
internal class WeatherForcastDbContext : DbContext
{
-    public WeatherForcastDbContext() { }
    public WeatherForcastDbContext(DbContextOptions<WeatherForcastDbContext> options) : base(options) { }

    public DbSet<WeatherForecast> WeatherForecasts { get; set; }

-    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
-    {
-        optionsBuilder.UseNpgsql("Server=<serverurl>;Database=testdb;Port=5432;User Id=<userid>;Password=<password>;Ssl Mode=Require;");
- 
}
```

WeatherForcastDbContext オブジェクトを DI で受け取るためのセットアップをします。次の実装を ApiService プロジェクトの Program.cs ファイルに追加します。

```diff_c_sharp:ApiService/Program.cs
・・・
+ builder.AddNpgsqlDbContext<WeatherForcastDbContext>("weatherdb");

var app = builder.Build();
・・・
```

引数の weatherdb はもちろん設定ファイルの ConnectionStrings に指定したKeyの "weatherdb" です。

/weatherforcast にアクセスがあった時のハンドラメソッドの引数で WeatherForcastDbContext オブジェクトを受け取り、DB アクセスに使用するように実装を変更します。

```diff_c_sharp:ApiService/Program.cs
- app.MapGet("/weatherforecast", async () =>
+ app.MapGet("/weatherforecast", async (WeatherForcastDbContext weatherForcastDbContext) =>
{
-   var forecast = await new WeatherForcastDbContext().WeatherForecasts.ToListAsync();
+   var forecast = await weatherForcastDbContext.WeatherForecasts.ToListAsync();

    return forecast;
});
```

実装は終了です。問題なく動作するか起動して確認してください。完成した ApiService プロジェクトの Program.cs は次のようになります。

```csharp:ApiService/Program.cs
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);

// Add service defaults & Aspire components.
builder.AddServiceDefaults();

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

builder.AddNpgsqlDbContext<WeatherForcastDbContext>("weatherdb");

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseExceptionHandler();

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


app.MapGet("/weatherforecast", async (WeatherForcastDbContext weatherForcastDb) =>
{
    var forecast = await weatherForcastDb.WeatherForecasts.ToListAsync();

    return forecast;
});

app.MapDefaultEndpoints();

app.Run();

record WeatherForecast([property: Key]Guid Id, DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

internal class WeatherForcastDbContext : DbContext
{
    public WeatherForcastDbContext(DbContextOptions<WeatherForcastDbContext> options) : base(options) { }

    public DbSet<WeatherForecast> WeatherForecasts { get; set; }
}
```

## 6. 接続文字列を AppHost プロジェクトに移動する

ここからが .NET Aspire の本領発揮です。現在、接続文字列は ApiService プロジェクトの設定ファイル appsettings.Development.json に記載されています。つまり ApiService プロジェクトで DB への接続先を管理しています。このままであっても本番環境にデプロイすると、接続文字列は環境変数から取得しますので、適切な環境変数を手動で設定すれば問題はありません。

しかし、ApiService プロジェクトのような個別のアプリケーションが沢山ある場合はどうでしょうか。個々のプロジェクトで DB の接続文字列のような別のリソースへの依存情報を管理するのではなく、１箇所でまとめて管理した方が見通しは格段に良くなります。

.NET Aspire では　AppHost の Program.cs でコードを用いて依存を表現することで依存関係をわかりやすくします。やってみましょう。まず ApiService プロジェクトの appsettings.Development.json に記載した接続文字列を、 AppHost プロジェクトの appsettings.Development.json に移動します。

```diff_json:ApiService/appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
+ }
- },
- "ConnectionStrings": {
-   "weatherdb": "Server=<serverurl>;Database=testdb;Port=5432;User Id=<userid>;Password=<password>;Ssl Mode=Require;"
- }
}
```

```diff_json:AppHost/appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
- }
+ },
+ "ConnectionStrings": {
+   "weatherdb": "Server=<serverurl>;Database=testdb;Port=5432;User Id=<userid>;Password=<password>;Ssl Mode=Require;"
+ }
}
```

ここで一度アプリケーションを起動してみて、エラーになることを確認しておきましょう。

起動後に Web アプリの画面で天気予報のページを開くと、Loading...と表示されたままデータが表示されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/165510ca-e6d6-5e6a-f02f-e4f0b6737f23.png)

ApiService プロジェクトの ログをダッシュボードで表示してみます。すると、「ConnectionString is missing.」とエラーメッセージがあります。想定通りのエラーですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/fb283af9-c188-47b0-0b11-1aff1f64fe3d.png)

では、移動した接続文字列を AppHost プロジェクトで読み込み、 ApiService プロジェクトに渡す実装をします。

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

+ var weatherSampleDb = builder.AddConnectionString("weatherDb");

- var apiService = builder.AddProject<Projects.PostgreSqlDemo_ApiService>("apiservice");
+ var apiService = builder.AddProject<Projects.PostgreSqlDemo_ApiService>("apiservice")
+     .WithReference(weatherSampleDb);

builder.AddProject<Projects.AspireAppPostgresql_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithReference(apiService);

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

buidler.AddConnectionString メソッドを使うことで、設定ファイルの接続文字列を取得します。ApiService プロジェクトでは WithReference 拡張メソッドでそれを受け取るように実装することで、環境変数として接続文字列を受けとるようになります。

アプリケーションを起動し、問題なく動作することを確認してください。

この時、　ApiServices プロジェクトのソースコードには何も手を加えることなく動作したことも重要なポイントです。.NET Aspire を使用することで、外部リソースへの依存から切り離して個々のプロジェクトを実装・管理していくことができるようになります。

# PostgreSQL コンテナを使用する

ここまではサーバーに建てた PostgreSQL を使用しています。開発時のベストなシナリオは、開発時はローカル環境のコンテナでDBを使用し、本番では既に構築済みのDBを使用する、というシナリオです。ではここから一旦ローカル環境でコンテナのDBを使用する方法を少しずつ構築していきます。

## 1. 最低限のオプションで起動する

AppHost プロジェクトの Program.cs を修正し、PosgreSQL DBをコンテナで動作するように設定します。また、同時に pgAdmin と言う PostgreSQL を管理できる Webアプリケーションもコンテナでホスティングするようにしましょう。

AppHost プロジェクトを右クリック→追加→.NET Aspire パッケージを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b20d0309-fe97-6057-f1fe-c27ae0640d16.png)

NuGet パッケージマネージャーが立ち上がり、検索文字列に既定値が入っています。既定値の後ろに半角スペース+postgres と入力します。

検索結果の Aspire.Hosting.PostgreSQL をインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/5f21c412-2d1a-53a6-198b-508a78904b85.png)

Program.cs で次の実装を行います。

```diff_c_sharp:AppHost/Program.cs
- var weatherSampleDb = builder.AddConnectionString("weatherDb");

+ var weatherSampleDb = builder.AddPostgres("mypostgres")
+     .WithPgAdmin()
+     .AddDatabase("weatherdb", "testdb");
```

コンテナとして立ち上げるPostgreSQLのインスタンス名をmypostgresとしました。この名前はローカルDBにしか関係がありませんので好きな名前を付けてOKです。
WithPgAdminメソッドを使用すると、PgAdmin という PostgreSQL を管理するツールが使用可能になるのでとても便利です。
そして誰もが誤解するのが AddDatabase メソッドです。第一引数に接続文字列のKeyとなる "weatherdb" をセットし、第二引数にデータベースの中に実際に作成しておく必要がある "testdb" をセットしています。

この "testdb" は、この記事の二番目で Azure 上に構築した Azure Database for PostgreSQL Felixble サーバーの中に作成した "testdb" と言うデータベースと同じ名前にしておく必要があります。後でこのアプリケーションを Azure 上にデプロイするのですが、その時 Azure 上に構築した PostgreSQL サーバーの testdb にアクセスしたいからです。

また先ほど実装した接続文字列を取得する builder.AddConnectionString メソッドのはもう不要なので削除しました。

アプリケーションを起動します。事前に Docker Desktop を立ち上げることをお忘れなく。初回起動はイメージの Pull に時間がかかります。しばらく待つと、ダッシュボードを見ると、先ほどまでの Webアプリと ApiService に加えて PostgreSql と pgAdmin のコンテナが立ち上がっていることがわかります。AddDatabase で追加した weatherdb もリソースとして認識しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b08469bc-fdf6-f6a1-a816-40b4f6611ac9.png)

Docker Desktop をみて見ると、確かに2つコンテナが稼働中です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/af2f5774-9975-47aa-947f-e410b6d3baec.png)


では PostgreSql にアクセスしてみましょう。pgAdmin のリンクをクリックするとブラウザの新しいタブが開いて pgAdmin の画面が開くはずです。pgAdmin は立ち上がりが遅い場合があるので、ダッシュボード起動後、数十秒ほど待ってから画面を開く、あるいはリロードを繰り返します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8dcb5364-55f5-1f7d-59c3-c9a5ce02e26d.png)

pgAdmin はコンテナで起動した PostgreSQL に自動的に接続します。mypostgres、というインスタンスがあります。コードで名前を付けたものですね。mypostgresの中にある Databases を開くと、postgresと言うデータベースがありますが、これは管理用です。本来、ここに testdb と言う名前のデータベースが表示されると予想していたはずです。コードで .AddDatabase("weatherdb", "testdb")と実装したからです。なのに testdb がありません。なぜでしょうか。

実は AddDatabase メソッドはデータベースを作成するメソッドではないのです。このメソッドはデータベースへの参照を表すものであって、データベースの作成はしません。大変ややこしいので覚えておく必要があります。

この紛らわしいメソッド名についてはだいぶ早い段階で issue が上がっていますが、2024/6 現在では修正する方針になっていません。

[Aspire.Npgsql component does not create database](https://github.com/dotnet/aspire/issues/1170)

ちなみにまだセットアップは終わっていないので天気予報ページはアクセスしてもエラーになります。

## 2. データベースを自動作成する

testdb を自動的に作成する方法はちゃんと用意されています。環境変数を使用して次のように実装します。

```diff_c_sharp:AppHost/Program.cs
var weatherSampleDb = builder.AddPostgres("mypostgres")
+   .WithEnvironment("POSTGRES_DB", "testdb")
    .WithPgAdmin()
    .AddDatabase("weatherdb", "testdb");
```

これだけで testdb が作成されます。この環境変数を使用したデータベースの作成は.NET Aspire の機能ではありません。PostgreSQL コンテナの機能です。それではアプリケーションを実行し、pgAdmin を開いて確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e021d4e1-3f33-c597-b7ce-1a0bc036de99.png)

testdb がいますね！ただ作成しただけなのでデータは何も格納されていません。

## 3. 初期値（Seed）を自動挿入する

コンテナ立ち上げ時にテーブルとデータを自動的に挿入するようにします。まずスクリプトの置き場所を用意します。場所はどこでも良いのですが、今回は　AppHostプロジェクトの直下に data という名前でフォルダを作成することにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ee59cb99-fd44-ecb4-c7e8-188a05b4a63d.png)

data フォルダの中に init.sql ファイルを新規作成します。ファイル名は　init.sql 以外でもOKですが、拡張子は必ず.sql にしてください。init.sql の中に次のSQLを貼り付けます。

```sql:AppHost/data/init.sql
-- Create the Todos table
CREATE TABLE IF NOT EXISTS "WeatherForecasts"
(
    "Id" uuid NOT NULL,
    "Date" date NOT NULL,
    "TemperatureC" integer NOT NULL,
    "Summary" text,
    CONSTRAINT weatherforecast_pkey PRIMARY KEY ("Id")
);

-- Insert some sample data into the Todos table
INSERT INTO "WeatherForecasts" ("Id", "Date", "TemperatureC", "Summary")
VALUES
    ('6555da29-89f1-4207-b20d-bd8d507e7c32', '2024-03-25', 22, 'Warm'),
    ('b3d43a4a-c877-4580-89af-2e26ed7e68e2', '2024-03-22', 9, 'Cold'),
    ('b57deef9-1d55-496d-a4ac-1ac88c93a4e2', '2024-03-23', 14, 'Little bit Cold'),
    ('c84c8347-e6f4-44da-a27d-9f5c0f6ca3dd', '2024-03-21', 7, 'Cold'),
    ('ee4f05a1-2435-44b6-b868-5434cc87bcfa', '2024-03-24', 2, 'TOOOOO Cold')
ON CONFLICT DO NOTHING;
```

WeatherForecasts テーブルが無かったら作成し、テスト用データを Insert する、というSQLです。

この init.sql を起動時に実行するために、次の実装を追加します。

```diff_c_sharp:AppHost/Program.cs
var weatherSampleDb = builder.AddPostgres("mypostgres")
    .WithEnvironment("POSTGRES_DB", "testdb")
+   .WithInitBindMount("./data")
    .WithPgAdmin()
    .AddDatabase("weatherdb", "testdb");
```

WithInitBindMountメソッドは、Docker のバインドマウント機能を使って、指定した data フォルダを コンテナ側からは /docker-entrypoint-initdb.d フォルダとして扱えるようにします。すると、このフォルダの中の sql ファイルや sh ファイルをコンテナ起動時にDBに対して実行してくれます。このコンテナ起動時のファイル実行機能は PostgreSql コンテナの機能であり、.NET Aspire はそれを利用しているだけです。

アプリケーションを実行して　WebFrontend 画面の天気予報ページを開いてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/5cc00e1c-bc32-c176-eef6-76ea5862b167.png)

表示された天気予報データが INSERT 文で投入されたデータと同じであることから、コンテナで稼働している PostgreSQL からデータを取得していることがわかります。

### テストデータを永続化して管理したい

アプリケーションを停止する度にコンテナは削除されます。つまりテストデータも消えてしまいます。テストデータを毎回作成するのではなく、永続化し、別のツールからテストデータを入力・編集などの管理をしたい場合もあるでしょう。テストデータを永続化する場合は、次の実装をします。

```diff_c_sharp:AppHost/Program.cs
var weatherSampleDb = builder.AddPostgres("mypostgres")
    .WithEnvironment("POSTGRES_DB", "testdb")
    .WithInitBindMount("./data")
+    .WithDataVolume("VolumeMount.postgres.data")
    .WithPgAdmin()
    .AddDatabase("weatherdb", "testdb");
```

ボリュームマウントとは Docker が管理するボリュームです。そしてこの実装方法は PostgreSQL の公式イメージの解説ページに掲載されている方法です。

Docker Desktop を確認すると、引数に指定した名前でボリュームが作成されたことがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c6f4cb39-624a-1c31-39e6-56c4839189d5.png)


確かにこの方法でデータを永続化できますが、永続化の対象はデータだけではなく DB 丸ごとです。そして永続化した DB が既に存在する場合は　 /docker-entrypoint-initdb.d フォルダにマウントした Seed 用のスクリプトが動きません。上記のように Seed 用のスクリプトのセットアップと、 WithDataVolume　メソッドと併用した場合、**DB が存在しない初回しかスクリプトは起動しない**ことに注意してください。つまり、テストデータの Seed を毎回投入するか、永続化するのかは基本的にどちらかしか選択できません。この挙動については PostgreSQL 公式イメージに解説があります。

[Docker Hub - Postgres 公式イメージ](https://hub.docker.com/_/postgres/)

### DB を永続化する場合の注意

DB を永続化する場合はさらに注意が必要です。上でも書きましたが、永続化はデータだけではなく DB 丸ごとです。そして DB の中には接続するためのユーザー情報が含まれているのです。一番最初に永続化した時、ユーザーはランダムなパスワードと共に作成されて永続化されます。このパスワードはダッシュボードから確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/6f3442fb-7664-4884-2ecd-bfa178a758d8.png)

PostgreSQL コンテナの行の詳細列の表示リンクをクリックして、環境変数を表示させます。POSTGRES__PASSWORD という KEY にパスワードが保存されています。

つまり問題は、２回目以降アプリケーションを起動した時です。初回起動時に作成したユーザーのパスワードを.NET Aspire は覚えていません。そのため、.NET Aspire はいつものようにランダム生成したパスワードを使った接続文字列をアプリケーションに渡すため、アプリケーションは DB にアクセスできずエラーとなってしまいます。コンテナでホスティングされる pgAdmin ツールからもやはり DB にアクセスできません。

そのため、初回起動時のユーザーパスワードを忘れずにメモしておかなければならないのですが、これは微妙な運用です。そこでパスワードをランダム生成したものではなく、最初から自分で用意したものを使用するように変更しましょう。次のように実装します。

AppHost プロジェクトのシークレットを使用するか、appsettings.Development.json を開いて、次のようにKey
 とパスワードをセットします。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/6cf4ad7e-e5c9-03d9-5470-548f0cb24268.png)

```AppHost/secrets.json
{
  "Parameters": {
    "pgPassword": "p@ssw0rd1234"
  }
}
```
または

```AppHost/appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "weatherdb": "Server=<serverurl>;Database=testdb;Port=5432;User Id=<userid>;Password=<password>;Ssl Mode=Require;"
  },
  "Parameters": {
    "pgPassword": "p@ssw0rd1234"
  }
}
```

secrets.json または appsettings.Development.json にセットした Key の pgPassword を参照するように実装します。

```diff_c_sharp:AppHost/Program.cs
+ var pgPassword = builder.AddParameter("pgPassword");

- var weatherSampleDb = builder.AddPostgres("mypostgres")
+ var weatherSampleDb = builder.AddPostgres("mypostgres", password: pgPassword)
    .WithEnvironment("POSTGRES_DB", "testdb")
    .WithInitBindMount("./data")
    .WithDataVolume("VolumeMount.postgres.data")
    .WithPgAdmin()
    .AddDatabase("weatherdb", "testdb");
```

アプリケーションを起動させて先ほどと同様にダッシュボードから確認すると、設定したパスワードに変更されています。言うまでもありませんが、実際に設定するパスワードはお好きな文字列を使用可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7f66db8f-5a94-3e49-768b-443e2be2fbe8.png)

secrets.json、appsettings.Development.json どちらかに設定するのが MS Learn　に掲載されている方法([External parameters](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/external-parameters))ですが、結局は環境変数を.NET Aspireがうまいこと読み込んでいるだけです。そのため Key 値がわかっていれば環境変数に直接値をセットして逃げることもできます。AppHostプロジェクトの launchSettings.json を開いて profiles が http, https それぞれの environmentVairables に次のように設定を追加します。

(@k-yamamoto  さん、コメントありがとうございます）

```json
"Parameters:pgPassword": "p@ssw0rd1234"
```

これで Web, ApiService アプリケーションから問題なく DB にアクセスできますし、コンテナホスティングの pgAdmin からのアクセス可能です。
さらにもう一つ、PostgreSQL コンテナのポート番号も毎回ランダムに変わることも注意が必要です。これも固定しておかないと、 psql やスタンドアロンの pdAdmin からアクセスする時に、毎回違うポート番号を指定して接続することになってしまうため、大変不便です。ポート番号の固定は次のように実装します。

```diff_c_sharp:AppHost/Program.cs
var pgPassword = builder.AddParameter("pgPassword");

- var weatherSampleDb = builder.AddPostgres("mypostgres", password: pgPassword)
+ var weatherSampleDb = builder.AddPostgres("mypostgres", password: pgPassword, port: 6500)
    .WithEnvironment("POSTGRES_DB", "testdb")
    .WithInitBindMount("./data")
    .WithDataVolume("VolumeMount.postgres.data")
    .WithPgAdmin()
    .AddDatabase("weatherdb", "testdb");
```

余談ですが、PostgreSQL の既定のユーザー ID は　postgres です。詳しくは公式ページをご覧ください。

[Docker Hub - Postgres 公式イメージ](https://hub.docker.com/_/postgres/)

# Azure Container Apps にデプロイしたら 構築済みの PostgreSQL サーバーを使用するようにする

さて、ローカル開発環境としてコンテナの PostgreSQL を使用できるようになりました。でもこの状態で Azure Depveloper CLI を使用して Azure Container Apps にデプロイすると、コンテナの PostgreSQL をデプロイすることになってしまいます。そうではなく、構築済みの Azure Database for PostgreSQL Flexible サーバーを使用するようにしたいわけです。

アプリケーションからすると、接続先がコンテナの PostgreSQL なのか、Azure Database for PostgreSQL Flexible サーバーなのかは接続文字列次第です。つまりデプロイする時、コンテナをデプロイしないで Azure Database for PostgreSQL Flexible サーバーへ接続する接続文字列をアプリケーションの環境変数にセットしてくれれば良いのです。

まさにこの目的のために、デプロイ時のみ接続文字列を指定できるオプションがあります。次のように実装します。

```diff_c_sharp:AppHost/Program.cs
var pgPassword = builder.AddParameter("pgPassword");

var weatherSampleDb = builder.AddPostgres("mypostgres", password: pgPassword, port: 6500)
    .WithEnvironment("POSTGRES_DB", "testdb")
    .WithInitBindMount("./data")
    .WithDataVolume("VolumeMount.postgres.data")
    .WithPgAdmin()
+   .PublishAsConnectionString()
    .AddDatabase("weatherdb", "testdb");
```

この状態で Azure Developer CLI を使って Azure Container Apps へのデプロイをしてみましょう。

azd コマンドで Azure に ログインします。

```bash
azd login
```

次に ソリューションファイルが存在するフォルダで次のコマンドを実行してマニフェストを作成します。

```bash
azd init
```

オプションを選択・入力するように促されます。詳しい説明はこちらをご覧ください。

[.NET Aspire を デプロイする](https://qiita.com/takashiuesaka/items/3da744d0511f29270299)

次に、Azure へのデプロイを行います。

```bash
azd up
```

サブスクリプション、リージョンの選択後、シークレット入力を求められます。ここで Azure Database for PostgreSQL Flexible サーバーの接続文字列を入力します。 PublishAsConnectionString メソッドを追加したことで、この入力が求められたのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/00db0b2c-4f22-1cf5-f46c-0cabb3289a4d.png)

AddParameter を使用すると、無条件に Publish 時にパスワード値の入力を求められます。本来、ここで値を入力すると KeyVault に格納されて接続文字列をセキュアに管理可能です。今回は接続文字列に本番用のパスワードを設定したので何も入力せずにこのままEnterを押します。

デプロイが正常終了した後、Azure Portalで入力した接続文字列がどのように格納されているのかを見てみましょう。

デプロイしたリソースグループの中に apiservice というアプリがあります。これを開き、左側のメニューからコンテナーを選択し、右画面から「環境変数」をクリックします。すると、一番下に接続文字列の Key である ConnectionStrings__weatherdb を名前に持つ行があります。ソースがシークレットの参照となっていますので、シークレットを見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e0a2bc7d-8a09-6bb5-c380-6525d71f16d0.png)

左側のメニューのシークレットを選択すると、接続文字列が格納されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/649d17fe-3c91-a6b7-6aba-beb8d14d6187.png)

これで、Azure Container Apps にデプロした後は 構築済みの PostgreSQL サーバーを参照するようにする、という目的が達成できました。もちろん、ローカル開発時にはローカルのコンテナでホスティングされた PostgreSQL を使用するようになったままです。

# まとめ

.NET Aspire を使用することで、データベースへの接続部分についてもローカル開発が簡単になるようにサポートが充実しています。環境構築という、とても煩わしい作業に時間を奪われる事なく、開発に集中できます。是非お試しください。