ルートグループ
エンドポイントが増えると、同じ種類のエンドポイントに重複が多いことに気づきます。パスがすべて /api/todos で始まる、今後ログイン必須にする、ドキュメント上でも同じカテゴリにまとめる、などです。この節の新しい概念はルートグループ(route group)で、共通点を一か所にまとめられます。
using Microsoft.AspNetCore.Http.HttpResults;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
List<Todo> todos = [new(1, "Buy milk", false)];
var nextId = 2;
var api = app.MapGroup("/api");
var todosApi = api.MapGroup("/todos").WithTags("Todos");
todosApi.MapGet("/", () => todos);
todosApi.MapGet("/{id:int}", Results<Ok<Todo>, NotFound> (int id) =>
{
var todo = todos.Find(t => t.Id == id);
return todo is null ? TypedResults.NotFound() : TypedResults.Ok(todo);
});
todosApi.MapPost("/", Created<Todo> (CreateTodo input) =>
{
var todo = new Todo(nextId++, input.Title, Done: false);
todos.Add(todo);
return TypedResults.Created($"/api/todos/{todo.Id}", todo);
});
todosApi.MapDelete("/{id:int}", Results<NoContent, NotFound> (int id) =>
todos.RemoveAll(t => t.Id == id) > 0 ? TypedResults.NoContent() : TypedResults.NotFound());
api.MapGet("/health", () => new { Status = "ok" }).WithTags("System");
app.Run();
record CreateTodo(string Title);
record Todo(int Id, string Title, bool Done);この章では第 08 章の CRUD の例を基にルートグループを説明します。第 06 章の入力検証と第 09 章の共通エラー処理はいったん省き、エンドポイントの登録先の変化に集中します。これらの機能は MapGroup と組み合わせられます。省略しているため、この章でリソースが見つからない場合の 404 レスポンスには本文がありません。
実行と確認
dotnet runTodo のエンドポイントはすべて /api/todos の下になります。
curl http://localhost:5080/api/todos[{"id":1,"title":"Buy milk","done":false}]curl http://localhost:5080/api/todos/1{"id":1,"title":"Buy milk","done":false}curl -i -X POST http://localhost:5080/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"Write report"}'HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/todos/2
{"id":2,"title":"Write report","done":false}ヘルスチェックのエンドポイントは /api/health です。
curl http://localhost:5080/api/health{"status":"ok"}古いアドレス /todos はもう存在せず、404 を返します。
MapGroup でグループを作る
using Microsoft.AspNetCore.Http.HttpResults;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
List<Todo> todos = [new(1, "Buy milk", false)];
var nextId = 2;
var api = app.MapGroup("/api");
var todosApi = api.MapGroup("/todos").WithTags("Todos");
todosApi.MapGet("/", () => todos);
todosApi.MapGet("/{id:int}", Results<Ok<Todo>, NotFound> (int id) =>
{
var todo = todos.Find(t => t.Id == id);
return todo is null ? TypedResults.NotFound() : TypedResults.Ok(todo);
});
todosApi.MapPost("/", Created<Todo> (CreateTodo input) =>
{
var todo = new Todo(nextId++, input.Title, Done: false);
todos.Add(todo);
return TypedResults.Created($"/api/todos/{todo.Id}", todo);
});
todosApi.MapDelete("/{id:int}", Results<NoContent, NotFound> (int id) =>
todos.RemoveAll(t => t.Id == id) > 0 ? TypedResults.NoContent() : TypedResults.NotFound());
api.MapGet("/health", () => new { Status = "ok" }).WithTags("System");
app.Run();
record CreateTodo(string Title);
record Todo(int Id, string Title, bool Done);19 行目の app.MapGroup("/api") はグループを作成します。戻り値の api は「/api で始まるすべてのルート」を表します。
21 行目では api にさらに MapGroup("/todos") を呼び、入れ子グループを作成します。完全なプレフィックスは /api/todos です。
その後 todosApi で MapGet や MapPost を呼ぶときは、相対パスを記述します。
| 登録コード | 実際のルート |
|---|---|
todosApi.MapGet("/", ...) | GET /api/todos |
todosApi.MapGet("/{id:int}", ...) | GET /api/todos/{id} |
todosApi.MapPost("/", ...) | POST /api/todos |
todosApi.MapDelete("/{id:int}", ...) | DELETE /api/todos/{id} |
api.MapGet("/health", ...) | GET /api/health |
グループオブジェクトの使い方は app とほとんど同じで、MapGet、MapPost、MapGroup などを呼び出せます。これが便利な点です。新しい API を覚える必要はなく、呼び出す対象を変えるだけです。
各ルートに完全なパスを直接書かないのはなぜでしょうか。プレフィックスは一つの決定事項であり、一度だけ書くべきです。API を /api/v2 に変更する場合、グループを使えば 19 行目の一か所だけを直せば済みます。プレフィックスが各エンドポイントに散らばっていると、一つずつ変更しなければならず、修正漏れで URL が不統一になる可能性があります。
注意
35 行目の Created のアドレス $"/api/todos/{todo.Id}" は、完全なパスを引き続き手動で指定しています。グループのプレフィックスは自動でここに追加されません。プレフィックスを変更するときは、このアドレスも忘れずに変更してください。
グループにメタデータを追加する
using Microsoft.AspNetCore.Http.HttpResults;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
List<Todo> todos = [new(1, "Buy milk", false)];
var nextId = 2;
var api = app.MapGroup("/api");
var todosApi = api.MapGroup("/todos").WithTags("Todos");
todosApi.MapGet("/", () => todos);
todosApi.MapGet("/{id:int}", Results<Ok<Todo>, NotFound> (int id) =>
{
var todo = todos.Find(t => t.Id == id);
return todo is null ? TypedResults.NotFound() : TypedResults.Ok(todo);
});
todosApi.MapPost("/", Created<Todo> (CreateTodo input) =>
{
var todo = new Todo(nextId++, input.Title, Done: false);
todos.Add(todo);
return TypedResults.Created($"/api/todos/{todo.Id}", todo);
});
todosApi.MapDelete("/{id:int}", Results<NoContent, NotFound> (int id) =>
todos.RemoveAll(t => t.Id == id) > 0 ? TypedResults.NoContent() : TypedResults.NotFound());
api.MapGet("/health", () => new { Status = "ok" }).WithTags("System");
app.Run();
record CreateTodo(string Title);
record Todo(int Id, string Title, bool Done);グループの役割はパスプレフィックスだけではありません。21 行目の .WithTags("Todos") は OpenAPI のタグをグループに加えます。グループ内のすべてのエンドポイントがタグを引き継ぎます。41 行目ではヘルスチェックのエンドポイントだけに「System」タグを追加しています。
/scalar ページを開くと、左側のエンドポイント一覧がタグごとに二つに分かれます。「Todos」には四つ、「System」には一つ表示されます。/openapi/v1.json では、四つの Todo エンドポイントの tags が ["Todos"]、/api/health が ["System"] になります。
技術詳細
前の章ではタグを設定していなかったため、Scalar はプロジェクト名(例:FirstSteps)をすべてのエンドポイントの既定タグとして使っていました。
WithTags のような呼び出しは、エンドポイントにメタデータ(metadata)を付けます。処理ロジックを変更せず、フレームワークの別の部分が読み取る「タグ」を付け加えます。グループに追加したメタデータは、そのグループ内のすべてのエンドポイントに適用されます。このためグループは設定を一括するのに適しています。後の章では、次のような設定をグループに追加します。
- 「認可」の章:
todosApi.RequireAuthorization()でグループ内のすべてのエンドポイントをログイン必須にします。 - 「CORS」の章:エンドポイントのグループにクロスオリジンアクセスを許可します。
一度の設定でグループ全体に適用され、新しく追加したエンドポイントも自動で設定を引き継ぎます。追加を忘れてセキュリティホールが生まれるのを防げます。
FastAPI との比較
MapGroup は FastAPI の APIRouter(prefix="/todos", tags=["Todos"]) に相当します。FastAPI では最後に app.include_router() で router を登録する必要があります。ASP.NET Core のグループは app.MapGroup() で作成した時点でアプリに登録され、21 行目のようにさらに入れ子にできます。
まとめ
app.MapGroup("/prefix")でルートグループを作ります。グループに登録するエンドポイントは相対パスを使い、実際のルートにはプレフィックスが自動で追加されます。- グループは入れ子にできます。
api.MapGroup("/todos")のプレフィックスは/api/todosです。 WithTagsなどのメタデータをグループに追加すると、その中のすべてのエンドポイントに適用されます。認可や CORS などもグループ単位で設定できます。- プレフィックスと共通設定を一度だけ書けば、変更時の漏れを防げます。ただし
Createdなどで手書きした完全アドレスは自分で同期する必要があります。
この章は「リクエストとレスポンス」段階の最後です。これで、引数を完全に扱い、検証を行い、適切なレスポンスを返し、構造を整理した API を作れるようになりました。次の「アプリケーションの骨格」段階は依存性注入から始め、例のメモリ内リストを実際のサービスに置き換えます。前の章:ステータスコードとエラー処理。
