App:https://aspnet-booklog.onrender.com/ ※1
GitHub:https://github.com/ynakao55/aspnet-booklog
※1 サービスが停止している場合は、起動に50秒程かかります。
「本のタイトル・著者・メモ・ステータス(未読/読書中/読了)」を管理する読書ログ Web アプリを、ASP.NET Core + EF Core + PostgreSQLで作り、Dockerで動かし、Render.comにデプロイしました。
この記事はポートフォリオ向けに、プロジェクト構成・主要ファイルの役割と作成ポイント・実行/デプロイ手順・ハマりどころをまとめたものです。
目次
1. アプリの説明
1-1. 何をするアプリ?
- 書籍(Title/Author/Note)を登録・一覧表示
- ステータス(
unread/reading/done)でフィルタ - 一覧は新しい順に表示、Note は長文でもトリムして見やすく(例:先頭 30 文字 + 省略記号)
1-2. 主な機能
- CRUD(今回は Create + Read の最小構成)
- フィルタ(クエリ文字列 or 画面リンク)
- 画面レイアウト(ヘッダー/フッターをレイアウトファイルで共通化)
- 初回起動時に自動マイグレーションでテーブル作成
1-3. 技術スタック
- フレームワーク:ASP.NET Core 8(Razor Pages)
- ORM:Entity Framework Core + Npgsql Provider
- DB:PostgreSQL(Render のマネージド DB)
- インフラ:Docker / Render.com
- 言語:C#
- OS/実行:Linux コンテナ
2. 学んだ技術とハマりどころ
- EF Core のマイグレーション
Docker 内でdotnet-efをローカルツールとして実行し、src/Migrations/をGit にコミットしておくのが本番デプロイを安定させるコツ。 - DATABASE_URL の取り扱い(Render)
Render の Postgres はpostgres://形式。C# でNpgsqlConnectionStringBuilderを使ってSslMode=Requireなどを付与して接続文字列を再構成すると安全。環境変数が未設定だとlocalhost:5432に落ちるのでログで早期検知できるようにした。 - 自動マイグレーション
Program.cs起動時にdb.Database.Migrate()を呼ぶことで、無料プラン(Pre-Deploy コマンドなし)でも初回にテーブルが作成されるようにした。 - Docker マルチステージ
SDK イメージでビルド → ランタイムイメージに成果物だけコピー。軽量で速い。 - Razor Pages の構造化
_Layout.cshtmlにヘッダー/フッター/メニューをまとめ、Indexは一覧とフィルタ UI に集中。
3. プロジェクト構成と主要ファイル(抜粋)
aspnet-booklog/
├─ src/
│ ├─ aspnet-booklog.csproj
│ ├─ Program.cs # 起動、DI、DB接続、Auto Migrate
│ ├─ Data/
│ │ └─ AppDbContext.cs # EF Core DbContext(Books)
│ ├─ Models/
│ │ └─ Book.cs # エンティティ(Id/Title/Author/Status/Note)
│ ├─ Pages/
│ │ ├─ Index.cshtml.cs # 一覧/フィルタのハンドラ
│ │ ├─ Index.cshtml # 一覧画面(テーブル + フィルタ UI)
│ │ └─ Shared/
│ │ └─ _Layout.cshtml # 共通レイアウト
│ └─ Migrations/ # 生成されたマイグレーション(Git 管理)
├─ Dockerfile
└─ .dockerignore
3-1) Program.cs
役割:サービス登録、DATABASE_URL の再構成、DbContext の DI、起動時に Migrate()。
作成ポイント
Environment.GetEnvironmentVariable("DATABASE_URL")を読み、postgres://→postgresql://に置換。NpgsqlConnectionStringBuilderでSslMode=Require、TrustServerCertificate=true(Render では実務上これで可)。builder.Services.AddDbContext<AppDbContext>(opt => opt.UseNpgsql(connString));- 起動直後に
using var scope = app.Services.CreateScope(); db.Database.Migrate();。
3-2) Data/AppDbContext.cs
役割:EF Core のコンテキスト。
作成ポイント
- EF Core を使って
Bookエンティティをデータベースにマッピング - アプリケーションからデータベース操作を行うための設定を行う。
3-3) Models/Book.cs
役割:エンティティ。
作成ポイント
- バリデーション属性(
[Required]、[MaxLength])を付ける。
[Required, MaxLength(120)]
public string Title { get; set; } = "";
3-4) Pages/Index.cshtml.cs
役割:一覧表示&フィルタのハンドラ(OnGet)。
作成ポイント
- クエリ
statusを受け取って LINQ で絞り込み。 - 新しい順に並べる:
OrderByDescending(b => b.Id)。 - ページモデルに
BooksとCurrentStatusを用意。
擬似コード例:
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
public IReadOnlyList<Book> Books { get; private set; } = [];
public BookStatus? CurrentStatus { get; private set; }
public IndexModel(AppDbContext db) => _db = db;
public async Task OnGetAsync(string? status)
{
IQueryable<Book> q = _db.Books.AsNoTracking();
if (Enum.TryParse<BookStatus>(status, true, out var st))
{
CurrentStatus = st;
q = q.Where(b => b.Status == st);
}
Books = await q.OrderByDescending(b => b.Id).ToListAsync();
}
}
3-5) Pages/Index.cshtml
役割:一覧テーブル&フィルタ UI。
作成ポイント
- 先頭に「[New Book]」ボタン、右側にフィルタ(All / Unread / Reading / Done)。
- Note はトリムして表示。
- 0 件時もフィルタは表示(UX が安定)。
UI のポイント(抜粋):
<div class="toolbar">
<a class="btn" href="/Create">New Book</a>
<span class="filters">
Filter:
<a href="/">All</a> |
<a href="/?status=Unread">Unread</a> |
<a href="/?status=Reading">Reading</a> |
<a href="/?status=Done">Done</a>
</span>
</div>
<table class="table">
<thead>
<tr>
<th>Title</th><th>Author</th><th>Status</th><th>Note</th><th></th>
</tr>
</thead>
<tbody>
@foreach (var b in Model.Books)
{
<tr>
<td>@b.Title</td>
<td>@b.Author</td>
<td>@b.Status</td>
<td>@(string.IsNullOrEmpty(b.Note) ? "" : (b.Note.Length <= 30 ? b.Note : b.Note.Substring(0,30) + "…"))</td>
<td><a href="/Details?id=@b.Id">Show</a></td>
</tr>
}
</tbody>
</table>
3-6) Pages/Shared/_Layout.cshtml
役割:サイト共通の枠(ヘッダー/フッター/メニュー/スタイル)。
作成ポイント
- ヘッダーにサイトタイトル「WEBアプリ開発教室」等を置き、メニューに
HomeやNew Bookを設置。 - 本文は
@RenderBody()、必要なら@RenderSection("Scripts", required: false)。
3-7) Dockerfile
役割:マルチステージでビルドして軽量ランタイムにデプロイ。
作成ポイント
ASPNETCORE_URLS=http://0.0.0.0:8080を環境変数で指定(Render/ローカルで同じ)。UseAppHost=falseでアルパイン系でも動作安定。
3-8) .dockerignore
役割:不要ファイルをビルドコンテキストから除外し、転送・ビルドを高速化。
**/bin/
**/obj/
**/.vs/
**/*.user
**/*.suo
.git
.gitignore
Dockerfile
4. ローカル実行 & Render デプロイ(チェックリスト)
4-1. マイグレーションの作成(Docker 内で)
cd aspnet-booklog
# 必要パッケージ
docker run --rm -v "$PWD/src:/src" -w /src mcr.microsoft.com/dotnet/sdk:8.0 \
bash -lc 'dotnet add package Microsoft.EntityFrameworkCore.Design'
# ツール → 生成
docker run --rm -v "$PWD/src:/src" -w /src mcr.microsoft.com/dotnet/sdk:8.0 bash -lc '
set -e
test -f /src/.config/dotnet-tools.json || dotnet new tool-manifest
dotnet tool install dotnet-ef || true
dotnet tool restore
dotnet ef migrations add InitialCreate --output-dir Migrations
'
# → src/Migrations/ を Git にコミット!
4-2. ローカル実行
docker build -t aspnet-booklog .
docker run --rm -p 8080:8080 \
-e DATABASE_URL="postgresql://<user>:<pass>@<host>:<port>/<db>" \
aspnet-booklog
# http://localhost:8080
4-3. Render デプロイ
- Web Service を Docker リポジトリから作成(Dockerfile 自動検出)。
- Environment に
DATABASE_URLを追加(Postgres の External Database URL)。 - Start Command は Dockerfile の
ENTRYPOINTに任せる(未設定でOK)。 - 初回アクセス時に自動マイグレーションでテーブル作成。
トラブル対処
- ログに
localhost:5432が見えたら →DATABASE_URL未設定/綴りミス No migrations were found→src/Migrations/をコミットし忘れ
5. スクリーンショット

6. まとめ
- ASP.NET Core + EF Core + PostgreSQL を Docker で包むと、ローカルも本番(Render)も同じアーティファクトで再現性高く運用できます。
DATABASE_URLを環境変数で一元化し、起動時にMigrate()を呼ぶことで Render 無料プランでもスムーズに初期化できます。- Razor Pages はレイアウトとページモデルを分けると、UI とロジックの見通しが良く、ポートフォリオとしても読みやすい構成になります。
付録:依存パッケージ(NuGet)
Npgsql.EntityFrameworkCore.PostgreSQLNpgsqlMicrosoft.EntityFrameworkCore.Design
必要なら dotnet add package を Docker 経由で実行しておくと、ローカルに .NET SDK を入れなくても開発できます。


コメント