第8章 ライトCQRSの最小形① DTOを分ける📦✨
この章は「CQRSをいきなりガチ構成にしないで、まずは最小の一歩を踏む」回だよ〜!🫶 結論からいうと、“読む用”と“書く用”でDTO(データの箱)を分けるだけでも、設計が一気にスッキリするの🥳
8-1. そもそもDTOってなに?📦🙂
DTOはざっくり言うと、
- APIが受け取る形(入力)
- APIが返す形(出力)
この“受け渡し専用のデータの箱”のことだよ〜📮✨ そして大事なのは、
- **DBのEntity(EFのモデル)**とは別物にしやすい
- 画面に都合のいい形にしてOK
- 仕様変更の衝撃を小さくできる
ってところ!😺
DTOは「運ぶ箱」📦 Entityは「保存する形」🧱 Domainは「ルールの中心」🏛️ …みたいに分けて考えると迷いにくいよ〜!
8-2. 「読むDTO」と「書くDTO」を分けると何が嬉しいの?🎁✨
✅メリット1:事故が減る(勝手に更新されない)🧯
たとえば「ToDoを作る(Create)」の入力に IsDone が入ってたら…
ユーザーが勝手に「完了済み」で作れちゃうかも😱
👉 作成入力に“作成に必要なものだけ”を置くと、自然に安全になるよ!
✅メリット2:APIが読みやすくなる(意図が見える)👀✨
CreateTodoCommand って名前なら
「これは作成の意図だな✍️」って一瞬でわかる!
✅メリット3:将来のCQRS分離がラクになる🚀
この章ではまだ「Handler分離」しないけど、 DTOが分かれてるだけで次章(Handler分離)が超ラクになるよ〜!🥹✨
8-3. 最小の“型”を決めよう(命名テンプレ)🏷️✨

まずはこれだけでOK!💕
- 書く(Command):
CreateXxxCommand/UpdateXxxCommand/DeleteXxxCommand - 読む(Query):
GetXxxQuery/SearchXxxQuery - 返す(Read DTO):
XxxDto/XxxDetailDto/XxxListItemDto
「Commandは動詞っぽく」「Queryは取得っぽく」 これだけでも迷子になりにくいよ🧭😺
8-4. ハンズオン:ToDoで“DTO分離”だけやってみよ🧩✨
ここでは “DTOを分けるだけ” をやるよ! (Handler分離は次章でやるから、今はController直呼びでもOK😌)
① DTO(Command / Query / Read DTO)を作る📦
namespace TodoApp.Features.Todos;
// 書く:作成(Command)
public sealed record CreateTodoCommand(
string Title,
DateOnly? DueDate
);
// 書く:更新(Command)
public sealed record UpdateTodoCommand(
Guid Id,
string Title,
DateOnly? DueDate
);
// 書く:完了(Command)
public sealed record CompleteTodoCommand(
Guid Id
);
// 読む:1件取得(Query)
public sealed record GetTodoQuery(
Guid Id
);
// 読む:一覧取得(Query) ※最小形
public sealed record GetTodoListQuery(
bool? IsDone
);
// 返す:詳細表示用(Read DTO)
public sealed record TodoDetailDto(
Guid Id,
string Title,
bool IsDone,
DateOnly? DueDate
);
// 返す:一覧表示用(Read DTO)
public sealed record TodoListItemDto(
Guid Id,
string Title,
bool IsDone
);
// 書いた結果:作成時の最小レスポンス(IDだけ返すのがライトCQRSの基本)
public sealed record CreateTodoResult(
Guid Id
);
ポイントだよ〜👇😺✨
- CreateTodoCommandに
IdやIsDoneを入れない(作成時に不要なものは持たせない)🧯 - 一覧用は
TodoListItemDtoみたいに 軽い形にする⚡ - 詳細用は
TodoDetailDtoみたいに 必要な情報を揃える📄
recordはDTOと相性が良いよ(値の比較がしやすい&ボイラープレートが減る)✨ ただし EFのEntityにはrecordは向かないことが多いので、Entityは普通のclassにしておくのが無難だよ〜🧱 (Microsoft Learn)
② Controller(またはMinimal API)で「入力と出力が別」なのを体感する👀✨
Controller例(超ざっくり)
「DTOが分かれてると、APIの意図が見える」って体感する用だよ🥳
using Microsoft.AspNetCore.Mvc;
using TodoApp.Features.Todos;
namespace TodoApp.Controllers;
[ApiController]
[Route("api/todos")]
public sealed class TodosController : ControllerBase
{
// ここではまだ雑にRepository/DbContext直でもOK(次章で綺麗にする✨)
[HttpPost]
public ActionResult<CreateTodoResult> Create([FromBody] CreateTodoCommand command)
{
// 例:保存してID発行した想定
var id = Guid.NewGuid();
return CreatedAtAction(nameof(Get), new { id }, new CreateTodoResult(id));
}
[HttpGet("{id:guid}")]
public ActionResult<TodoDetailDto> Get(Guid id)
{
// 例:取得した想定(本当はDBから取る)
return Ok(new TodoDetailDto(id, "買い物", false, null));
}
[HttpGet]
public ActionResult<IReadOnlyList<TodoListItemDto>> List([FromQuery] bool? isDone)
{
var items = new List<TodoListItemDto>
{
new(Guid.NewGuid(), "洗濯", false),
new(Guid.NewGuid(), "課題", true),
};
if (isDone is not null)
items = items.Where(x => x.IsDone == isDone).ToList();
return Ok(items);
}
}
ここでの“狙い”はこれ👇💡
- POSTはCreateTodoCommandを受け取る(書く)✍️
- **GETはTodoDetailDto/TodoListItemDtoを返す(読む)**👀
- つまり APIの入口と出口がちゃんと違うって体感できる!
8-5. recordの2つの書き方:どっちがいい?🤔✨
DTOにはだいたいこの2パターンがあるよ👇
A) 位置引数record(さっきの例)🧩
- 短く書けてラク✨
- ただしコメントや属性を細かく付けたい時に長くなりがち😅
B) requiredプロパティ型record(属性つけやすい)🏷️
- 「必須」を表現しやすい
- バリデーション(次の章以降)にも繋げやすい✨
namespace TodoApp.Features.Todos;
public sealed record CreateTodoCommand
{
public required string Title { get; init; }
public DateOnly? DueDate { get; init; }
}
required は「初期化で必ずセットしてね」をコンパイル時に促せる機能だよ〜📌 (Microsoft Learn)
8-6. よくあるミス集(これ避けるだけで強くなる)🧠🛡️
❌ミス1:1個のDTOを全APIで使い回す
- CreateにもUpdateにもGetにも同じDTO…
- だんだん「何のためのプロパティ?」って混乱する😵💫
✅ 用途ごとにDTO分ける(この章の結論!)📦✨
❌ミス2:Writeモデル(Entity)をそのまま返す
- DB都合の項目まで外に出ちゃう
- 将来の変更でAPIが壊れやすい💥
✅ 返すのはRead DTOにする📄✨
❌ミス3:CreateにUpdate用の項目が混ざる
例:Createに Id / IsDone / CreatedAt が入ってる…😱
✅ Createに必要な最小だけ(Titleとか)✍️✨
8-7. ミニ演習(15〜30分)⏱️💪
🎯お題:ToDoのDTOを「用途ごと」に分けよう📦
次の3つだけ作ればOK!
CreateTodoCommand(入力)✍️GetTodoQuery(入力)👀TodoDetailDto(出力)📄
できたら、Swagger(またはAPIテスター)で POSTとGETのスキーマが別になってるのを見てニヤニヤしよ😺✨
8-8. AI(Copilot / Codex)に頼るときのプロンプト例🤖🪄
DTO雛形を作らせる(まずは量産)
- 「ToDoアプリの
CreateTodoCommand / UpdateTodoCommand / GetTodoQuery / TodoDetailDto / TodoListItemDtoをC#のrecordで作って。CreateにはIdやIsDoneを含めないで。」
命名を整える(読みやすさUP)
- 「CQRSの命名として違和感があるDTO名を指摘して、より良い名前に直して。」
“混ぜてないか”チェック(事故防止)
- 「このCreateDTOに更新用・表示用の項目が混ざってないかレビューして。」
8-9. まとめ(この章で一番大事なこと)🎀
- “書くDTO(Command)”と“読むDTO(Query/Read DTO)”は分ける📦
- まずは分けるだけで、APIが読みやすくなるし事故も減る🧯
- 次章で、このDTOたちを Handlerに分離して「ライトCQRSの形」を完成させるよ〜!🧑🍳✨
ちなみに、C# 14 は .NET 10 / Visual Studio 2026あたりで使える機能が増えてるので、今の書き方(record中心)でも気持ちよく書けるよ〜😺✨ (Microsoft Learn) あと、.NET 10のASP.NET CoreはMinimal APIのバリデーション周りも改善が入ってるから、DTO分離は後々さらに効いてくるよ〜🔍✨ (Microsoft Learn)