学習のためサンプルアプリを頻繁に作成するのですが毎回 DB をローカルに作成していたため DB 数が増えてきてしまいました。
そのため Aspire を用いて DB をコンテナ上に構築をし必要なタイミングでのみ Docker 上に構築するようにしてみました。 この際にテーブル作成やシードデータ注入も C#から実行したかったため方法を調査しました。
最終的に目指す環境は下記です。
- Blazor Server + WebAPI プロジェクトの構成
- DB は PostgresSQL を使用
- DB はコンテナ上に構築
- テーブルは EfCore のマイグレーションで作成
- シードデータも EfCore から注入する
- あくまで開発環境とし、本番環境は考慮しない
- サンプル用に API とページも作成するが URL 設計やページレイアウトはひとまず仮置き
本記事ではプロジェクト作成などは CLI 上で行います。Visual Studio 環境の GUI 経由でも同じことが可能です。
手順
プロジェクト作成
Blazor + WebAPI + Aspire のテンプレートが Microsoft より提供されているためそちらを利用します。CLI より下記コマンドを実行します。
dotnet new aspire-starter -n DotnetDemo
プロジェクト作成の際に使用したコマンドだと執筆時点(2025/07/03)で.net 8.0 のプロジェクトが作成されます。そのためバージョンを合わせるため以降に追加するプロジェクトやパッケージも.net 8.0 で作成します。
Blazor と WebAPI を別々に追加すれば最新を対象にプロジェクトを作成することも可能です。
今回はDotnetDemo
という名前のプロジェクトを作成しますが、この部分は実際に作成したいプロジェクト名に変更してください。
もちろん既存プロジェクトに WebAPI や Aspire を追加することも可能です。その場合は下記記事をご覧ください。
データプロジェクトの作成
モデルクラスについてはフロントエンドも使用するため新しくクラスライブラリ作成し、共通化します。
dotnet new classlib -n DotnetDemo.Data -f net8.0
続いて追加したデータクラスへの参照を Blazor と API プロジェクトに追加します。このコマンドはプロジェクトルートで実施します。
dotnet add .\DotnetDemo.ApiService\DotnetDemo.ApiService.csproj reference .\DotnetDemo.Data\DotnetDemo.Data.csproj
dotnet add .\DotnetDemo.Web\DotnetDemo.Web.csproj reference .\DotnetDemo.Data\DotnetDemo.Data.csproj
また C# Dev Kit を使用している場合はsln
ファイルにもデータプロジェクトへの参照を追加しておくと開発作業が効率的になります。
dotnet sln add .\DotnetDemo.Data\DotnetDemo.Data.csproj
データプロジェクトから参照したクラスで API と Web ページ を作成(Option)
参照が正常に追加されていること確認するためにデータ取得 API と表示するページを作成します。この章は必須ではなくすべての設定が完了後にまとめて作成しても大丈夫です。その場合は次の章に進んでください。
データプロジェクトにStudent
クラスを作成します。
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string GetDetails()
{
return $"Id: {Id}, Name: {Name}";
}
}
API プロジェクトのProgram.cs
に API を追加します。
app.MapGet("/students", () =>
{
var students = new List<Student>
{
new Student { Id = 1, Name = "Alice", Age = 21 },
new Student { Id = 2, Name = "Bob", Age = 22 },
new Student { Id = 3, Name = "Charlie", Age = 23 }
};
return students;
});
続いて追加した API を参照する Blazor ページを追加します。DotnetDemo.Web
プロジェクト内にrazor
ページを追加します。
@page "/studentslist"
@using DotnetDemo.Data;
@inject HttpClient HttpClient
<h1 class="text-2xl font-bold">Students List</h1>
@if (isLoading)
{
<p>Loading...</p>
}
else if (students != null && students.Count > 0)
{
<ul class="mt-4">
@foreach (var student in students)
{
<li>@student.Name : @student.Id</li>
}
</ul>
}
else
{
<p>No students found.</p>
}
@code {
private List<Student> students = new List<Student>();
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
HttpClient.BaseAddress = new("https+http://apiservice");
students = await HttpClient.GetFromJsonAsync<List<Student>>("/students");
isLoading = false;
}
}
API と表示のページを作成しました。
Aspire 上に DB コンテナを用意
まず初めにコンテナ上に DB を構築します。今回は PostgresSQL を使用します。AppHost プロジェクトにパッケージをインストールします。
dotnet add .\DotnetDemo.AppHost\DotnetDemo.AppHost.csproj package Aspire.Hosting.PostgreSQL --version 8.2.2
そして AppHost のProgram.cs
ファイルに下記を追加します。
var dotnetDemoDb = builder.AddPostgres("mypostgres")
.WithEnvironment("POSTGRES_DB", "testdb")
.WithInitBindMount("./data")
.WithPgAdmin()
.AddDatabase("dotnetdemodb", "testdb");
上記のコードによりコンテナ上に DB が立ち上がるはずです。ただしまたマイグレーションを行っていないので完全に空の DB が立ち上がります。
EfCore を実装
ここからこのプロジェクトに EfCore を追加していきます。対象の DB はローカル環境ではなく Aspire 経由で構築したコンテナ上に作成します。EfCore で最重要なDbContext
は Data プロジェクトに追加します。
そのためまずは Data プロジェクトで EfCore が使用できるようにパッケージを追加します。
dotnet add .\DotnetDemo.Data\DotnetDemo.Data.csproj package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL --version 8.2.2
続いてDbContext
を追加します。
public class DotnetDemoDbContext : DbContext
{
public DotnetDemoDbContext(DbContextOptions<DotnetDemoDbContext> options)
: base(options)
{
}
public DbSet<Student> Students => Set<Student>();
}
今度は API プロジェクトに EfCore のパッケージをインストールしProgram.cs
にサービス追加のコードを実装します。
dotnet add .\DotnetDemo.ApiService\DotnetDemo.ApiService.csproj package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL --version 8.2.2
dotnet add .\DotnetDemo.ApiService\DotnetDemo.ApiService.csproj package Microsoft.EntityFrameworkCore.Design --version 8.0.10
using Microsoft.EntityFrameworkCore;
// 省略
builder.AddNpgsqlDbContext<DotnetDemoDbContext>("dotnetdemodb");
最後にマイグレーションを実行します。
dotnet ef migrations add Init -s .\DotnetDemo.ApiService\DotnetDemo.ApiService.csproj -p .\DotnetDemo.Data\DotnetDemo.Data.csproj
実際に EfCore を使用するプロジェクトと DbContext が実装されているプロジェクトが別のためコマンドが少し複雑になっています。注意してください。
接続文字列の取得
このままだと接続文字列がわからず実際に動かす際に対象の DB が見つからないため Aspire の機能を用いて接続文字列を取得します。
AppHost プロジェクトのProgram.cs
を編集します。
// APIプロジェクト登録よりも前に
var dotnetDemoDb = builder.AddPostgres("mypostgres")
.WithEnvironment("POSTGRES_DB", "testdb")
.WithInitBindMount("./data")
.WithPgAdmin()
.AddDatabase("dotnetdemodb", "testdb");
- var apiService = builder.AddProject<Projects.DotnetDemo_ApiService>("apiservice");
+ var apiService = builder.AddProject<Projects.DotnetDemo_ApiService>("apiservice");
+ .WithReference(dotnetDemoDb);
これで API プロジェクトから DB への接続文字列を参照することが可能になりました。
DB のマイグレーションをサービス化
このままだと DB はできても肝心なテーブルが作成されないため自動で作成されるように設定します。この時に SQL を書いてテーブルの作成と初期データの格納を行う方法もあるのですがせっかくなのですべて EfCore 経由で実施しようと思います。
実現方法としては C#の Worker サービスプロジェクトを使用します。こちらを使用することで起動スクリプトのようなことが可能になります。
下記コマンドを実行してプロジェクトの作成と参照の追加を行います。
dotnet new worker -n DotnetDemo.MigrationService -f "net8.0"
dotnet sln add .\DotnetDemo.MigrationService\DotnetDemo.MigrationService.csproj
dotnet add .\DotnetDemo.AppHost\ reference .\DotnetDemo.MigrationService\
dotnet add .\DotnetDemo.MigrationService\ reference .\DotnetDemo.Data\
dotnet add .\DotnetDemo.MigrationService\ reference .\DotnetDemo.ServiceDefaults\
続いてマイグレーションプロジェクトに Aspire 用の EfCore パッケージをインストールします。
dotnet add .\DotnetDemo.MigrationService\ package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL --version 8.2.2
そしてProgram.cs
の値を下記のように書き換えます。
using DotnetDemo.MigrationService;
using DotnetDemo.Data;
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHostedService<Worker>();
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName));
builder.AddNpgsqlDbContext<DotnetDemoDbContext>("dotnetdemodb");
var host = builder.Build();
host.Run();
このままだと Worker.ActivitySourceName の部分でコンパイルエラーになってしまいます。そのためWorker.cs
を下記のように書き換えます。
using System.Diagnostics;
using DotnetDemo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry.Trace;
namespace DotnetDemo.MigrationService;
public class Worker : BackgroundService
{
public const string ActivitySourceName = "Migrations";
private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public Worker(IServiceProvider serviceProvider, IHostApplicationLifetime hostApplicationLifetime)
{
_serviceProvider = serviceProvider;
_hostApplicationLifetime = hostApplicationLifetime;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
using var activity = s_activitySource.StartActivity("Migrating database", ActivityKind.Client);
try
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<DotnetDemoDbContext>();
await RunMigrationAsync(dbContext, cancellationToken);
await SeedDataAsync(dbContext, cancellationToken);
}
catch (Exception ex)
{
activity?.RecordException(ex);
throw;
}
_hostApplicationLifetime.StopApplication();
}
private async Task RunMigrationAsync(DotnetDemoDbContext dbContext, CancellationToken cancellationToken)
{
// マイグレーション処理を実装
await dbContext.Database.MigrateAsync(cancellationToken);
}
private async Task SeedDataAsync(DotnetDemoDbContext dbContext, CancellationToken cancellationToken)
{
// シードデータ処理を実装
// 例: 初期データの挿入など
if (!await dbContext.Students.AnyAsync(cancellationToken))
{
dbContext.Students.AddRange(
new Student { Name = "Alice_Migration", Age = 21 },
new Student { Name = "Bob_Migration", Age = 22 },
new Student { Name = "Charlie_Migration", Age = 23 }
);
await dbContext.SaveChangesAsync(cancellationToken);
}
}
}
上記の処理をざっくりと説明するとワーカー起動時にExecuteAsync
メソッドが呼び出されます。ExecuteAsync
メソッド内でRunMigrationAsync
とSeedDataAsync
を実行して、それぞれマイグレーションと初期データの格納を行います。
デバッグ開始時に自動でマイグレーション
プロジェクト起動時に自動でマイグレーション実施が実施されるように設定を行います。
AppHost プロジェクトのProgram.cs
に下記を追加します。
var dotnetDemoDb = builder.AddPostgres("mypostgres")
.WithEnvironment("POSTGRES_DB", "testdb")
.WithInitBindMount("./data")
.WithPgAdmin()
.AddDatabase("dotnetdemodb", "testdb");
+var migrations = builder.AddProject<Projects.DotnetDemo_MigrationService>("migrations")
+ .WithReference(dotnetDemoDb);
var apiService = builder.AddProject<Projects.DotnetDemo_ApiService>("apiservice")
.WithReference(dotnetDemoDb)
+ .WithReference(migrations);
以上でプロジェクト起動時に自動的に EfCore 経由のマイグレーションが実行される環境が作成できます。
API で取得するデータを DB 経由に変更(Option)
記事前半で作成した API と Web ページについて現在は固定のデータを表示しているため DB から値を取得するように変更します。
API プロジェクトのProgram.cs
の API 登録画面を下記のように修正してください。
app.MapGet("/students", (DotnetDemoDbContext dbContext) =>
{
var students = dbContext.Students.ToList();
return students;
});
おわりに
EfCore と Aspire を用いて実行時のみ DB がコンテナ上に構築される環境を作成しました。もっと簡単にワーカーサービスのことだけ書くつもりでしたが、気が付いたら環境構築記事のようになっていました。肝としてはワーカーサービスを使用して外部からマイグレーションすることなので他の構成へ応用も可能です。
本番環境にデプロイすることが考えたとしても Azure Container Apps へなら簡単にできるはずです。 Aspire 自体もっといろいろなことができると思うので今後もいろいろ調べていきたいとおみました。
この記事が皆様のコーディングライフの助けになれば幸いです。
参考