第11章:Commandの基本① “戻り値を欲張らない”✍️
この章はひとことで言うと、**「Commandは“書いた”ことだけ返して、表示はQueryに任せよ〜!」**って話だよ〜😺🫶 (CQRSの“読み/書き分離”を気持ちよく保つための、超重要ポイント!)
この章のゴール🎯
- Command(更新系)で 返すべき情報が最小でいい理由がわかる🙆♀️
- **Create/Update/Delete の“ちょうどいい返し方”**がわかる📮
- C#(Minimal API想定)で Createは201 + Location + IDにできる✅ ※201とLocationの意味はHTTP仕様にちゃんと書いてあるよ📌 (greenbytes.de)
1) なんで「全部返す」はダメになりがち?😵💫
Commandで「作ったデータ全部返すね!」をやると、地味に事故が増えるの…💥
よくある事故3点セット🧨
-
責務が混ざる 更新(Command)なのに表示用の形(Query)を作り始めて、いつの間にか“混ぜ実装”に逆戻り😇
-
戻り値が肥大化して、変更が怖くなる 画面都合の項目(集計、JOIN、派生列…)を混ぜた瞬間に、Command側が「UIの都合」に引っ張られる🌀
-
セキュリティ事故が起きやすい “返してはいけない項目”をうっかり混ぜがち(内部フラグ、管理用情報など)🙈
2) Commandの戻り値は「成功/失敗 + IDくらい」でOK👌✨
Commandは“状態を変える”のが仕事。 だから戻り値も、基本はこれで十分だよ👇
Commandが返していい代表例✅
- Create:新しいID(Guidなど)
- Update/Delete:基本は「成功した」だけ(HTTP的には 204 No Content が相性◎)
- どうしても必要なら:Version(ETag相当)/更新番号くらい(後で同時更新対策に使える)📌
逆に、Commandが返しがちな「欲張りセット」😅
- 作成直後の“詳細画面用DTO”全部
- 一覧画面用の集計つきDTO
- 関連データ全部入り(子要素やJOIN結果まで)
👉 これらは Queryで取ろう👀✨
3) HTTPとしての「ちょうどいい返し方」📡✨

✅ Create(POST)
- 201 Created
- Locationヘッダに「作ったもののURL」を入れる 201は「Locationで作ったリソースを示す」って仕様に書いてあるよ📌 (greenbytes.de)
- Bodyは IDだけでも全然OK🙆♀️(欲張らない!)
✅ Update(PUT/PATCH)
- 204 No Content が定番(更新できたなら中身いらない)✨
✅ Delete(DELETE)
- 204 No Content が定番✨
4) 実装してみよう(Minimal API版)🧩🚀
ここでは ToDo を例にするね📝 ポイントは 「HandlerはIDを返す」「APIはCreatedAtRouteでLocationを付ける」 だよ!
Minimal APIの
CreatedAtRouteは、Location付きの201を作るための定番メソッドだよ📌 (Microsoft Learn)
4-1) DTO / Command / Response(最小)📦
public sealed record CreateTodoRequest(string Title);
// Command(更新の依頼)
public sealed record CreateTodoCommand(string Title);
// Commandの結果(IDだけ!)
public sealed record CreateTodoResult(Guid Id);
4-2) Handler(IDを返すだけ)🧑🍳✨
(DBはまだ何でもOK。ここでは雰囲気だけ!)
public interface ICommandHandler<TCommand, TResult>
{
Task<TResult> Handle(TCommand command, CancellationToken ct);
}
public sealed class CreateTodoHandler : ICommandHandler<CreateTodoCommand, CreateTodoResult>
{
// ここにDbContextやRepositoryが入る想定(詳細は後の章でOK)
public async Task<CreateTodoResult> Handle(CreateTodoCommand command, CancellationToken ct)
{
// 例:IDを発行して保存したことにする
var id = Guid.NewGuid();
// TODO: 保存処理(後の章でEF Coreなど)
await Task.CompletedTask;
return new CreateTodoResult(id);
}
}
4-3) API(Location付き201 + BodyはIDだけ)📮🎉
app.MapGet("/todos/{id:guid}", (Guid id) =>
{
// ここはQuery側:詳細表示用DTOを返す場所(この章では省略)
return Results.Ok(new { id, title = "dummy", isDone = false });
})
.WithName("GetTodoById");
1) コマンド(更新系)の基本:状態を変えて 201 Created を返す 🏗️
前回は「読み取り (Query)」だったけど、今回は「書き込み (Command)」だよ!
app.MapPost("/todos", async (
CreateTodoRequest req,
ICommandHandler<CreateTodoCommand, CreateTodoResult> handler,
CancellationToken ct) =>
{
// Command実行(書く!)
var result = await handler.Handle(new CreateTodoCommand(req.Title), ct);
// 201 + Location + Bodyは最小(IDだけ)
return Results.CreatedAtRoute(
routeName: "GetTodoById",
routeValues: new { id = result.Id },
value: new { id = result.Id }
);
});
これで:
- レスポンスは 201 Created
- ヘッダに Location: /todos/{id}
- Bodyは
{ id: ... }だけ✨ → “欲張らない”完成!😺🎊
5) 「でも画面が作成直後に詳細を表示したい…」問題😅
あるある!めちゃある!😂
解決策は基本この2つ💡
-
作成後にQueryを1回叩く(おすすめ)
POST /todos→ IDだけもらうGET /todos/{id}→ 表示用のDTOを取る CQRS的に超きれい✨
-
どうしても1回で済ませたいなら 「画面に必要な最小限のRead DTO」だけ返す(※“全部”はやめる) でも、これは例外扱いにして、基本は(1)が安定だよ🫶
6) エラーの返し方も“欲張らない”🧯
- 失敗時は ProblemDetails 形式で返すと、APIがスッキリしやすいよ✨ ASP.NET Coreのエラー取り扱いはProblemDetails中心で整理されてる📌 (Microsoft Learn)
- しかも .NET 10 では Minimal APIのバリデーション周りも強化されてる(Validation対応の記述があるよ)📌 (Microsoft Learn)
この章では深追いしないでOK! 次の第12章で「Validationの分離(入口で守る)」をしっかりやろうね🔍✨
ミニ演習💪😺
演習A:欲張りCommandをダイエット🥗
POSTの戻り値が「詳細DTO全部」になってる想定でOK- 戻り値を IDだけに直す
- 詳細表示は
GET /todos/{id}で取得する形に変える
演習B:SwaggerでLocationを見る👀
POST /todosを叩いて- レスポンスの Locationヘッダ を確認✅
- そのURLを叩いて
GETが取れるのを確認✅
AIに手伝ってもらうプロンプト例🤖💬
設計レビュー(戻り値欲張ってない?)
- 「このPOSTのレスポンス、CQRS的に欲張りすぎかレビューして。最小案も出して」
201 + Location をきれいに作る
- 「Minimal APIでCreatedAtRouteを使って、201とLocationヘッダを正しく返す例を作って」
ついでに命名整える
- 「CreateXxxCommand / CreateXxxResult の命名をC#の慣習で自然に整えて」
まとめ🎀
- Commandは “書く”に集中:戻り値は 成功/失敗 + ID(+必要ならVersion) くらいでOK✍️✨
- 表示用の情報は Queryが担当👀
- Createは 201 + Location + ID で“きれいに分離”できる📮🎉 (greenbytes.de)
次の第12章で、Command入口のValidationを分離してさらに気持ちよくするよ〜🔍💕