第13章:実務っぽい形へ①(Command/Queryオブジェクト化)📦🚚
この章はね、「引数が増えてきてつらい〜😵💫」をCommand/Queryを“ひとつの箱📦”にまとめることで解決しちゃう回だよ〜!✨ (いまの主流は .NET 10 + C# 14 あたりだよ〜🧡) (Microsoft Learn)
1) この章のゴール🎯✨
できるようになったら勝ち〜!🏆💕
- ✅
CreateTodoCommand/GetTodosQueryみたいな「1機能=1オブジェクト」を作れる - ✅ 「引数爆発💥」を回避できる
- ✅ バリデーション(入力チェック)を“置く場所”の感覚がつかめる
- ✅ 命名&フォルダ構成で迷わなくなる🗂️
- ✅ 次章(Handler化)に自然につながる🚀
2) なぜ “オブジェクト化” が効くの?🤔💡
2-1. 引数が増えると起こる悲劇😇💥
たとえば追加がこうなると…
AddTodo(title, dueDate, priority, tags, memo, createdBy, ...)- 途中から「どれがどれ?😵」
- 呼び出し側で「順番ミス」や「null地獄」
- 将来パラメータ追加で全部壊れがち🪓
2-2. “箱📦”にすると一気にラクになる✨

- 呼び出しが読みやすい👀✨(プロパティ名が説明になる)
- 追加項目が増えても破壊が少ない🧱
- その箱に “最低限のルール” を入れられる(簡単バリデーション)🧷
- ログやテストで「入力が何だったか」を扱いやすい🧪
3) Command/Queryオブジェクトの基本ルール📌✨
ここは超大事〜!🫶
-
🛠️ Command:何かを変更する(作る/更新/削除/確定…)
- 例:
CreateTodoCommand,CompleteTodoCommand
- 例:
-
🔍 Query:見るだけ(取得/検索/一覧…)
- 例:
GetTodosQuery,GetTodoByIdQuery
- 例:
そして命名はコレが最強🧡
- Command:
動詞 + 対象(Create/Update/Delete/Complete + Todo) - Query:
Get/List/Search + 対象
4) まずは作ってみよう!C#での書き方✍️✨
4-1. いちばん実務寄り:record で“入力DTO”を作る📦
Command/Queryは「運ぶもの」なので、recordが相性いいよ〜😊 (イミュータブル寄りで事故りにくい🧊)
public sealed record CreateTodoCommand
{
public required string Title { get; init; }
public DateOnly? DueDate { get; init; }
public int? Priority { get; init; } // 例: 1〜5
public string[] Tags { get; init; } = [];
public string? Memo { get; init; }
}
Queryも同じ感じでOK〜🔍✨
public sealed record GetTodosQuery
{
public string? Search { get; init; }
public bool IncludeCompleted { get; init; } = false;
public int? Limit { get; init; } = 50;
}
ポイント💡
requiredで「必須」を表現できる(入れ忘れ事故が減る)✅- 配列は
= []で空にしておくと null が消える🧹✨
5) どう呼び出す?(ConsoleでもMinimal APIでも同じ発想)🧠✨
5-1. “引数じゃなくて箱📦を渡す”に変える
Before(つらい)😵💫
AddTodo(title, dueDate, priority, tags, memo)
After(読みやすい)😍
var cmd = new CreateTodoCommand
{
Title = "レポート提出",
DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(3)),
Priority = 4,
Tags = ["school", "urgent"],
Memo = "参考文献も忘れずに"
};
await todoCommands.Create(cmd);
Commands側のメソッドもこうするよ〜🛠️
public sealed class TodoCommands
{
public async Task Create(CreateTodoCommand command)
{
// 次の章で “Handler化” して、ここが薄くなるイメージだよ〜✨
// ここではまず「箱で受け取る」だけでOK👌
await Task.CompletedTask;
}
}
6) Minimal APIだともっと気持ちいい😍(自動で箱に詰めてくれる📦)
POST(Command)は Body(JSON) から “箱📦” に入れてくれるよ〜✨ Minimal APIのパラメータバインドがそれをやってくれる感じ! (Microsoft Learn)
app.MapPost("/todos", async (CreateTodoCommand cmd, TodoCommands commands) =>
{
await commands.Create(cmd);
return Results.Ok();
});
GET(Query)は、クエリ文字列から “箱📦” に詰めたいよね? .NETのMinimal APIは複合型のバインドもできるし、まとめ方も用意されてるよ〜🔍✨ (Microsoft Learn)
using Microsoft.AspNetCore.Http.HttpResults;
app.MapGet("/todos", async ([AsParameters] GetTodosQuery query, TodoQueries queries) =>
{
var list = await queries.GetList(query);
return Results.Ok(list);
});
7) “バリデーションどこに置く問題” を初心者向けにスッキリ整理🧷✨
ここ、悩みがち〜!でも最初はこう覚えよ😊
7-1. 3つの置き場所(ざっくり)🧠
- 入口(Endpoint/Controller):型の形・必須・簡単な範囲チェック
- ユースケース層(Commands/Handlers):ビジネス的にダメ(例:期限が過去は不可)
- ドメイン(Entity等):絶対守る不変条件(例:Titleは空にしない)
7-2. この教材の“落とし所”✅(最初はこれでOK)
- ドメイン:最後の砦(空文字禁止とか)
例:Commandに「超軽い自己チェック」を付ける(やりすぎないのがコツ🧡)
public sealed record CreateTodoCommand
{
public required string Title { get; init; }
public DateOnly? DueDate { get; init; }
public string[] Validate()
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(Title))
errors.Add("Title is required.");
if (DueDate is not null && DueDate < DateOnly.FromDateTime(DateTime.Today))
errors.Add("DueDate must be today or later.");
return errors.ToArray();
}
}
入口でこう使う感じ〜✨
app.MapPost("/todos", async (CreateTodoCommand cmd, TodoCommands commands) =>
{
var errors = cmd.Validate();
if (errors.Length > 0) return Results.BadRequest(new { errors });
await commands.Create(cmd);
return Results.Ok();
});
ここで大事なのは… **「バリデーションは“全部ここ!”じゃなくて、役割で分ける」**だよ〜🫶✨
8) 命名&フォルダ構成(迷子にならない地図🗺️)🗂️✨
“機能単位”で固めるのが実務で強いよ〜💪
-
Features/Todos/Commands/CreateTodo/CreateTodoCommand.cs
-
Features/Todos/Queries/GetTodos/GetTodosQuery.cs
または小さめプロジェクトなら、まずはこうでもOK😊
Todos/Commands/Todos/Queries/
コツ💡 **「探す場所が一発で分かる」**のが正義✨ 後でHandler化したとき、同じフォルダに
CreateTodoHandler.csを置けて超気持ちいいよ〜🥰
9) ミニ演習📝🍰(手を動かすと一気に理解できる!)
演習A:引数爆発のメソッドを“箱📦化”してみよ✨
- いまある
AddTodo(title, dueDate, priority, tags, memo)を探す👀 CreateTodoCommandを作る- メソッドを
Create(CreateTodoCommand cmd)に変更 - 呼び出し側を全部修正(ここで気持ちよさを味わう😆)
演習B:Queryも同様に箱化🔍
GetTodos(search, includeCompleted, limit)をGetTodosQueryに置き換える- テストで「limit省略時は50」みたいな仕様を1個だけ固定✅
10) AI拡張(Copilot/Codex)に頼むときの“勝ちテンプレ”🤖✨
10-1. 生成用プロンプト(そのまま使ってOK)🪄
- 「このメソッドの引数を
CreateTodoCommandにまとめて。recordで、必須はrequiredにして。null回避で配列は空配列初期化して。」
10-2. レビュー用プロンプト(事故防止🧷)
- 「このCommand/Queryオブジェクト化で、**CQS違反(Queryが副作用)**になってない? それと、必須/任意の区別が適切か見て!」
11) この章のチェックリスト✅✨(ここまでできた?)
- ✅ Command/Queryを「1機能=1箱📦」で表現できた
- ✅ 引数が減って読みやすくなった😍
- ✅ 必須/任意が
requiredや初期値で表現できた - ✅ バリデーションを“入口/ユースケース/ドメイン”の感覚で分けられた
12) 次章へのつながり👣✨
次はいよいよ Handler化だよ〜!👩🍳📨
この章で作った CreateTodoCommand / GetTodosQuery を、Handlerが受け取って実行する形にすると…
- Endpointがもっと薄くなる🧼✨
- 「1機能=1ファイル」がハッキリして強い🧱
- 依存関係の向き(Dependency Rule)にも自然に入れる🧭
Visual Studio側も、最近のリリースはAI統合がより深くなってるので、この進め方と相性いいよ〜🤖🧡 (Microsoft Learn)