第13章:永続化①:保存の境界を作る(まずはファイル保存でもOK)💾📦
今日いちばん新しいところだけメモしておくね📝 **.NET は 10.0.2 が最新(2026-01-13)**で、Visual Studio 2026 のアップデートも 2026-01-13 に出てるよ〜!🚀 (Microsoft)
1) この章のゴール🎯✨
- Todo が 終了→再起動しても残るようになる🎉
- 「保存」を Model の外側に置いて、あとで差し替えやすい形にする🔁🧼
- 壊れにくい保存(途中終了しても壊れにくい)を “それっぽく” 入門する🛡️💾
2) まず考え方:永続化は「アプリの外」🌍➡️📦

保存先(ファイル/DB/クラウド)はアプリから見ると 外の世界だよね🌏
なので Model に Save() とかを書き始めると、Model が急に「外界の事情」に詳しくなってしまう…😵💫
そこでこの章の合言葉はこれ👇
- Model:やりたいこと・守りたいルール📦
- View:見せ方👀
- Controller/Service:操作の流れ🚦
- **保存:外部との接続(インフラ)**🔌💾
3) 今回の保存方針(いちばん気楽で、あとで置き換えやすい)🍀
保存形式:JSON にする📄✨
- 人間が開いて読める(デバッグが超ラク)👀
- まずは最短で成功体験つくれる🎉
- すぐ DB(SQLite 等)へ移行できる(次の章でやる)🔁
System.Text.Json を使うよ〜!
「JSON を文字列 or ファイルに書くには JsonSerializer.Serialize」が基本だよ🧠✨ (Microsoft Learn)
4) 保存場所:ユーザーごとのデータ領域に置く📁🪟
おすすめは LocalApplicationData(ユーザーごとの安全な場所)にアプリ用フォルダを作るやり方💡
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) が王道だよ〜! (Microsoft Learn)
例:
%LOCALAPPDATA%\CampusTodo\campus-todo.json 🗂️✨
5) ファイルが壊れにくい保存のコツ(超入門)🛡️💾

いきなり本番ファイルに上書きすると、書き込み途中で落ちたときに壊れがち😇 そこでよくある作戦👇
- 一時ファイルに書く(例:
campus-todo.json.tmp)✍️ - 置き換える(必要ならバックアップも作る)🔁
.NET には File.Replace があって、置き換え+バックアップができるよ🧯 (Microsoft Learn)
6) 実装:保存専用クラスを 1 個作ろう🧱✨
6-1) まず「保存用の型(DTO)」を用意する📦
Model は “アプリの中心” だから、保存の都合で Model をいじらない方向にするよ〜🧼✨ (今は同じ形でもOKだけど、分ける癖をつけると後が楽🥳)
// Infrastructure/TodoFileFormat.cs
namespace CampusTodo.Infrastructure;
public sealed record TodoItemDto(
int Id,
string Title,
bool IsDone,
DateTime CreatedAt,
DateTime? DueAt
);
public sealed record TodoFileData(
int SchemaVersion,
int NextId,
List<TodoItemDto> Items
);
SchemaVersionは将来の拡張用(データ構造が変わっても移行しやすい)🧠✨NextIdを保存しておくと、再起動してもIDが衝突しにくい🔢👍
6-2) 保存・読み込みクラス(ファイル担当)を作る💾🧰
ポイントはここ👇
- 読み込み:無ければ空で開始
- 壊れてたら “壊れてるよ” を伝えつつ空で開始(落とさない)🧯
- 保存:temp に書いて
File.Replaceで置換(バックアップも)🧷
// Infrastructure/TodoFileStore.cs
using System.Text;
using System.Text.Json;
namespace CampusTodo.Infrastructure;
public sealed class TodoFileStore
{
private readonly string _filePath;
private readonly JsonSerializerOptions _jsonOptions = new()
{
WriteIndented = true // 読みやすいJSONにする🪄
};
public TodoFileStore(string filePath)
{
_filePath = filePath;
}
public TodoFileData LoadOrNew()
{
if (!File.Exists(_filePath))
{
return NewEmpty();
}
try
{
var json = File.ReadAllText(_filePath, Encoding.UTF8);
var data = JsonSerializer.Deserialize<TodoFileData>(json, _jsonOptions);
return data ?? NewEmpty();
}
catch
{
// 壊れた JSON の可能性:いったん空で開始(落とさない)🧯
return NewEmpty();
}
}
public void Save(TodoFileData data)
{
EnsureDirectory();
var json = JsonSerializer.Serialize(data, _jsonOptions);
var tmp = _filePath + ".tmp";
var bak = _filePath + ".bak";
// 1) まず temp に書く✍️
File.WriteAllText(tmp, json, Encoding.UTF8); // 既存があれば上書きされる :contentReference[oaicite:4]{index=4}
// 2) 置き換え(バックアップも)🔁
if (File.Exists(_filePath))
{
File.Replace(tmp, _filePath, bak, ignoreMetadataErrors: true); // 置換+バックアップ :contentReference[oaicite:5]{index=5}
}
else
{
File.Move(tmp, _filePath);
}
}
private void EnsureDirectory()
{
var dir = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrWhiteSpace(dir))
{
Directory.CreateDirectory(dir);
}
}
private static TodoFileData NewEmpty()
=> new(SchemaVersion: 1, NextId: 1, Items: new List<TodoItemDto>());
}
ちょい注意⚠️ .NET 10 から
System.Text.Jsonは、特定条件で$type/$id/$refみたいな “メタデータ用の名前” と衝突するプロパティがあるとエラーを早めに出すようになってるよ〜(変な JSON を作らないため)🛡️ (Microsoft Learn) (普通の Todo ならまず踏まないけど、「Type」みたいな名前を多用するときは思い出してね🧠)
6-3) 保存パスを作るヘルパー📁✨
// Infrastructure/AppPaths.cs
namespace CampusTodo.Infrastructure;
public static class AppPaths
{
public static string TodoDataFilePath()
{
var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(baseDir, "CampusTodo", "campus-todo.json");
}
}
SpecialFolder は “特殊フォルダの種類” を表すやつだよ🗂️ (Microsoft Learn)
7) アプリに接続:起動時ロード&更新時オートセーブ🔁💾
ここは超シンプルにいこう〜!✨
- 起動時:ファイル読み込み → Service に初期データを渡す
- 変更(add/done/edit/delete)したら:保存する
7-1) Service に「入出力」メソッドを足す(最小改造)🧩
// Services/TodoService.cs(例:必要なところだけ)
using CampusTodo.Infrastructure;
public sealed class TodoService
{
private readonly List<TodoItem> _items;
private int _nextId;
public TodoService(IEnumerable<TodoItem> initialItems, int nextId)
{
_items = new List<TodoItem>(initialItems);
_nextId = nextId;
}
public IReadOnlyList<TodoItem> GetAll() => _items;
public TodoItem Add(string title, DateTime? dueAt)
{
var item = new TodoItem(_nextId++, title, createdAt: DateTime.Now, dueAt: dueAt);
_items.Add(item);
return item;
}
// done/edit/delete ...(この章では省略)
// 保存用スナップショット(DTOへ変換)
public TodoFileData ExportForSave()
{
var dtos = _items.Select(x => new TodoItemDto(
Id: x.Id,
Title: x.Title,
IsDone: x.IsDone,
CreatedAt: x.CreatedAt,
DueAt: x.DueAt
)).ToList();
return new TodoFileData(
SchemaVersion: 1,
NextId: _nextId,
Items: dtos
);
}
}
※ TodoItem 側は、ここまでで作ってきた形に合わせて読み替えてね〜(プロパティ名はプロジェクトの実物優先でOK)😊✨
7-2) Program でロードして、Controller から “更新のたびに保存” する🎮💾
(Controller/Service の設計はプロジェクトに合わせて微調整OKだよ〜!)
// Program.cs(例)
using CampusTodo.Infrastructure;
var store = new TodoFileStore(AppPaths.TodoDataFilePath());
// 起動時にロード📥
var data = store.LoadOrNew();
// Service 初期化🌱
var service = new TodoService(
initialItems: data.Items.Select(dto => new TodoItem(
dto.Id, dto.Title, dto.CreatedAt, dto.DueAt, dto.IsDone
)),
nextId: data.NextId
);
// 例:何か変更が起きたタイミングで保存する関数を用意🪄
void SaveNow()
{
store.Save(service.ExportForSave());
}
// あとは Controller の add/done/edit/delete のあとに SaveNow() を呼ぶだけ🎉
「毎回保存って重くない?」と思ったら: Todo 程度なら全然OKなことが多いよ〜😊 重くなってきたら「一定間隔」「終了時」「変更があったら数秒後にまとめて」みたいに進化させればOK🧠✨
8) ミニ演習✅🎓✨(この章の勝ち筋)
演習A:再起動しても残る🎉
- Todo を 3 件追加✍️
- アプリ終了👋
- 再起動🚀
- 3 件が残ってたらクリア✅
演習B:JSON をのぞいてみる👀📄
campus-todo.jsonをエディタで開く- ちゃんと
Itemsが入ってるのを確認✨ WriteIndented = trueのおかげで読みやすいはず〜🪄
9) よくあるつまずき集😵💫🧯
- 保存されない → 保存の呼び出しが「追加/完了/編集/削除」の後に入ってるかチェック✅
- 読み込みで落ちる
→
LoadOrNew()は例外を飲んで空にしてるので、落ちるなら別の場所で例外が出てるかも👀 - 文字化け
→
Encoding.UTF8を指定しているか確認(サンプルは指定済み)🧠
10) AI活用🤖💡(この章での使いどころ)
Copilot / Codex に投げるなら、こういうお願いが強いよ〜✨
- 「
TodoFileStoreにLoadOrNew()とSave()を作って。temp ファイル→置換の流れで」🧰💾 - 「
TodoItemとTodoItemDtoの相互変換コードだけ作って(命名はプロジェクト準拠で)」🔁 - 「読み込み失敗時の挙動案を3つ(初心者向けに)」🧯💬
最後にひとこと:生成されたコードは、必ず “保存先パス” と “例外時の挙動” だけは自分の目でチェックしてね〜!👀✨
次章予告📣✨
次はここまでの TodoFileStore を Repository にして差し替え可能にするよ〜!🔁🧲
「ファイル保存→SQLite保存」に移行するときに、上の層がほぼノーダメになるのが気持ちいいやつ🥳