dotnet-webapi について
ASP.NET Core HTTP API の開発・保守向けスキルです。既存プロジェクトの規約に沿った実装を促し、DTO、HTTP セマンティクス、OpenAPI ドキュメント、エラー処理を扱います。
主な機能
- 01コントローラーと最小 API の方式判定
- 02リクエスト・レスポンス DTO の設計
- 03HTTP ステータスとセマンティクスの指針
- 04OpenAPI エンドポイントドキュメント
- 05一元的なエラー処理の指針
活用シーン
- 01API エンドポイントの追加・変更
- 02エンドポイントの OpenAPI メタデータ設定
- 03DTO とステータスコードの標準化
- 04API エラー処理ミドルウェアの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill dotnet-webapiインストール後、Claude Code で/dotnet-webapiを入力して読み込まれたか確認します。
詳細
ASP.NET Core Web API
適切な HTTP セマンティクス、OpenAPI ドキュメント、エラー処理を備えた、構成の整った ASP.NET Core Web API エンドポイントを作成します。
使用する場面
次のような ASP.NET Core HTTP API の作業で、このスキルを使用します。
- コントローラーまたは最小 API で実装された Web API エンドポイントの追加や変更。
- OpenAPI/Swagger メタデータとエンドポイントドキュメントの設定。
- リクエスト/レスポンス DTO の定義と、一貫性のある HTTP ステータスコードの動作の実現。
.httpファイルなど、リクエストベースの API テスト用アーティファクトの追加。- 一元化された API エラー処理ミドルウェアまたは例外マッピングの設定。
使用しない場面
次の作業には、このスキルを使用しないでください。
- 一般的な C# コーディングスタイルや、API 以外のリファクタリング。
- EF Core のデータモデリングやクエリ最適化の作業。
optimizing-ef-core-queriesを使用してください。 - フロントエンド、Razor、Blazor の UI 変更。
- gRPC サービス。
- SignalR ハブやリアルタイムメッセージングのフロー。
入力/前提条件
このスキルを適用する前に、既存の API スタイルや構成に合わせるために必要なプロジェクトのコンテキストを確認します。
- ASP.NET Core のエントリーポイント。通常は
Program.csです。 - 既存のコントローラー。特に
ControllerBaseを継承するクラスや、[ApiController]を使用するクラス。 app.MapGet、app.MapPost、app.MapPut、app.MapDeleteなどの既存の最小 API 登録。- プロジェクトで既に使用されている関連 DTO、モデル、検証、エラー処理の型。
- 変更を検証するために利用できるビルド、実行、テストのコマンド。
ユーザーが新しいエンドポイントを求めている場合は、スタイルが混在しないよう、既存の規約に沿って実装するために、まず現在のプロジェクト構成を確認します。
ワークフロー
ステップ 1:API のスタイルを判断する
コードを書く前に、プロジェクト内の既存のエンドポイントのパターンを調べます。
ControllerBaseを継承するクラス、または[ApiController]が付与されたクラスを検索します。Program.csまたはエンドポイントのファイルで、app.MapGet、app.MapPostなどを検索します。- プロジェクトですでにコントローラーが使用されている場合は、コントローラーを使用します。
- プロジェクトですでに最小 APIが使用されている場合は、最小 API を使用します。
- どちらも存在しない新しいプロジェクトでは、ユーザーが明示的にコントローラーを求めていない限り、最小 API をデフォルトで使用します。
同じプロジェクトでスタイルを混在させないでください。
ステップ 2:リクエストとレスポンスの型を定義する
API の入力と出力には専用の型を作成します。リクエストやレスポンスの本文で EF Core エンティティを直接公開しないでください。
すべての DTO に sealed record を使用します。 レコードは不変性を強制し、値に基づく等価性を提供し、簡潔なコードを実現します。意図しない継承を防ぎ、JIT の非仮想化(CA1852)を可能にするため、シールしてください。
命名規則:
| 役割 | 規則 | 例 |
|---|---|---|
| 入力(作成) | Create{Entity}Request | CreateProductRequest |
| 入力(更新) | Update{Entity}Request | UpdateProductRequest |
| 出力(単一) | {Entity}Response | ProductResponse |
| 出力(リスト) | {Entity}ListResponse | ProductListResponse |
すべての DTO に XML ドキュメントコメントを付けます: API で公開するすべてのリクエスト型とレスポンス型に <summary> XML ドキュメントコメントを追加します。これらのコメントは生成された OpenAPI 仕様に自動的に含まれるため、追加のメタデータ呼び出しなしで、より充実したドキュメントを作成できます。
参考:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/openapi-comments
日付と時刻の値 — DateTimeOffset を使用します: DTO に日付または時刻のプロパティを含める場合は、常に DateTime ではなく DateTimeOffset を使用します。
DateTimeOffset は UTC オフセットを保持し、曖昧なタイムゾーン変換を回避して、API の利用者が期待するオフセット情報を含む ISO 8601 形式で JSON にシリアライズします。
参考:https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset JSON シリアライズのオプション — 既定では既存の動作を維持します: 既存の API では、プロジェクトですでに使用されている場合や、ユーザーから明示的に要求された場合を除き、より厳格なシリアライズ/デシリアライズ設定を導入しないでください。プロパティ名の大文字と小文字を区別する設定や、数値を厳密に処理する設定は、既存のクライアントを壊す可能性があります。新規プロジェクト、または厳格な JSON 処理が明示的に要求された場合は、悪意のあるリクエストを処理するリスクを最小限に抑えるため、次のようなオプションを設定してください。
// Apply these settings only for new projects, when the existing project already
// uses them, or when the user explicitly requests stricter JSON behavior.
builder.Services.ConfigureHttpJsonOptions(options =>
{
// disallow reading numbers from JSON strings
options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
// match properties with exact casing during deserialization
options.SerializerOptions.PropertyNameCaseInsensitive = false;
// reject duplicate JSON property names during deserialization
options.SerializerOptions.AllowDuplicateProperties = false;
// omit null properties from serialized output
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
列挙型プロパティ — 既定では文字列としてシリアライズします: ユーザーから整数としてシリアライズするよう明示的に要求された場合を除き、すべての列挙型プロパティは文字列としてシリアライズします。文字列としてシリアライズされた列挙型は人間が読みやすく、値の並べ替えに対して壊れにくく、より優れた OpenAPI ドキュメントを生成できます。JsonStringEnumConverter の設定については、ステップ 4 を参照してください。
レスポンス DTO — 簡潔で不変の出力を実現するため、位置指定の sealed レコードを使用します。
/// <summary>Represents a product returned by the API.</summary>
public sealed record ProductResponse(
int Id,
string Name,
decimal Price,
Category Category,
bool IsAvailable,
DateTimeOffset CreatedAt);
リクエスト DTO — データ注釈を自然に機能させるため、init プロパティを持つ sealed レコードを使用します。
/// <summary>Payload for creating a new product.</summary>
public sealed record CreateProductRequest
{
[Required, MaxLength(200)]
public required string Name { get; init; }
[Range(0.01, 999999.99)]
public required decimal Price { get; init; }
public required Category Category { get; init; }
}
Update{Entity}Request レコードも同じパターンに従い、更新に必要な追加プロパティ(例:IsAvailable)を加えます。
Minimal API の検証 — 明示的に登録します: データ注釈([Required]、[MaxLength]、[Range] など)による検証は MVC コントローラーでは自動的に行われますが、minimal API では明示的なオプトインが必要です。minimal API を使用する .NET 10 以降 のプロジェクトでは、Program.cs に検証サービスを追加します。
builder.Services.AddValidation();
これにより、ハンドラーの実行前にデータ注釈が付与されたパラメーターを検証するエンドポイントフィルターが設定されます。検証に失敗した場合は、検証問題の詳細を含むレスポンスとともに 400 Bad Request が返されます。
参考:https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0
DTO に変更可能なクラス({ get; set; })を使用しないでください。変更可能な DTO は、構築後に誤って変更される可能性があり、レコードが提供する自己文書化された不変性も失われます。
ステップ 3:エンドポイントを実装する
コントローラーを使用する場合も最小 API を使用する場合も、以下の HTTP 規約に一貫して従ってください。
最小 API エンドポイントの整理: 最小 API を使用するプロジェクトでは、静的クラスと静的な Map<Resource> メソッドを使い、リソースごとにエンドポイントを整理してください。このパターンでは、エンドポイント定義がリソースの種類ごとにまとまるため、API の成長に伴ってもコードを保守しやすく、見通しもよくなります。
パターンの構成:
- リソースごとに静的クラスを 1 つ作成します(例:
ProductEndpoints、CategoryEndpoints)。 - 静的な
Map<Resource>(this WebApplication app)拡張メソッドを定義します。 - メソッド内で、そのリソースのエンドポイントに対して
MapGet、MapPost、MapPut、MapDeleteなどを呼び出します。 Program.csで、各リソースのMapメソッドを順番に呼び出します。
最小 API の戻り値の型 — TypedResults を優先する:
Results ファクトリーではなく、常に TypedResults を優先してください。TypedResults は戻り値の型情報をメソッドシグネチャに含めるため、OpenAPI ジェネレーターがより豊富なメタデータを自動的に取得できます。
ハンドラーが複数の結果の型(例:Ok または NotFound)を返す場合は、ラムダに明示的な Results<T1, T2> 戻り値の型を注釈してください。これにより、コンパイラーに共通の型を指定しながら TypedResults を使用できます。
async Task<Results<Ok<ProductResponse>, NotFound>> (int id, ...) => ...
明示的な戻り値の型注釈なしに、TypedResults.Ok(x) と TypedResults.NotFound() をそのまま三項演算子で使用しないでください。Ok<T> と NotFound は異なる型であり、コンパイラーが推論できる共通の基底型がありません。その結果、コンパイラーは RequestDelegate(HttpContext) との一致にフォールバックし、CS1593: Delegate 'RequestDelegate' does not take N arguments が発生します。
フォールバック — Results ファクトリー: ハンドラーに多数の条件分岐(結果の型が 7 つ以上)がある場合は、Results ファクトリー(Results.Ok()、Results.NotFound())を使用できます。戻り値は IResult となり、コンパイル時の OpenAPI 推論は犠牲になりますが、シグネチャはよりシンプルになります。
ステータスコード:
| 操作 | 成功時 | 一般的なエラー |
|---|---|---|
| GET(単一) | 200 OK | 404 Not Found |
| GET(一覧) | 200 OK | — |
| POST(作成) | Location ヘッダー付きの 201 Created | 400 Bad Request、409 Conflict |
| PUT(完全更新) | 200 OK | 400 Bad Request、404 Not Found |
| PATCH(部分更新/アクション) | 200 OK | 400 Bad Request、404 Not Found |
| DELETE | 204 No Content | 404 Not Found、409 Conflict |
POST の 201 レスポンス: 新しく作成したリソースを指す Location ヘッダーを必ず返してください。
- コントローラー:
CreatedAtAction(nameof(GetById), new { id = ... }, response)を使用します - 最小 API:
TypedResults.Created($"/api/products/{id}", response)を使用します
CancellationToken: すべてのエンドポイントのシグネチャで CancellationToken を受け取り、すべての非同期呼び出し(サービスメソッド、EF Core クエリ、HttpClient 呼び出し)に渡してください。これにより、クライアントが切断されたときにサーバーは処理を停止できます。
// Controller example
[HttpGet("{id}")]
public async Task<ActionResult<ProductResponse>> GetById(
int id, CancellationToken cancellationToken)
{
var product = await _productService.GetByIdAsync(id, cancellationToken);
return product is null ? NotFound() : Ok(product);
}
// Minimal API example — TypedResults with explicit return type (recommended)
app.MapGet("/api/products/{id}", async Task<Results<Ok<ProductResponse>, NotFound>> (
int id, IProductService service, CancellationToken cancellationToken) =>
{
var product = await service.GetByIdAsync(id, cancellationToken);
return product is null ? TypedResults.NotFound() : TypedResults.Ok(product);
});
ステップ 4:OpenAPI を設定する
ASP.NET Core Web API には、すべて OpenAPI ドキュメントを用意する必要があります。追加する前に、プロジェクトで OpenAPI がすでに設定されているか確認してください。
.NET 9 以降のプロジェクトでは、組み込みの ASP.NET Core OpenAPI サポートを使用します(開発環境で builder.Services.AddOpenApi() + app.MapOpenApi())。これだけで十分です。追加のパッケージは必要ありません。
.NET 9 以降のプロジェクトには、Swashbuckle.* NuGet パッケージ(Swashbuckle.AspNetCore、Swashbuckle.AspNetCore.SwaggerUI、Swashbuckle.AspNetCore.SwaggerGen など)を追加しないでください。 Swashbuckle は、.NET 9 以降および .NET 10 の OpenAPI 型との互換性に関する既知の問題があります。.NET 8 以前を対象とするプロジェクトでは、Swashbuckle を使用できます。プロジェクトにすでに Swashbuckle がインストールされている場合、ユーザーから削除の依頼がない限り、そのままにしてください。
参考:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview
エンドポイントの OpenAPI メタデータ: 生成されるドキュメントがルートの一覧だけにならないよう、説明的なメタデータを追加してください。最小 API では、メタデータのメソッドをチェーンして指定します。
app.MapGet("/api/products/{id}", handler)
.WithName("GetProductById")
.WithSummary("Get a product by ID")
.WithDescription("Returns the full product details including category.")
.Produces<ProductResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
列挙型のシリアル化(既定では文字列): API レスポンスと OpenAPI スキーマの両方で、列挙型が読みやすい文字列として表示されるように、JSON シリアル化を設定してください。ユーザーが整数による列挙型のシリアル化を明示的に希望している場合を除き、必ずこの設定を追加します。最小 API とコントローラーでは使用するオプション型が異なるため、両方に設定してください。
// Minimal APIs
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
// Controllers / MVC
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
ステップ 5:エラー処理を設定する
グローバル例外ハンドラーを使用し、個々のエンドポイントに try-catch ブロックが不要な構成にします。すべてのエラーレスポンスで RFC 7807 Problem Details を返してください。
.NET 8 以降のプロジェクトでは、組み込みの例外ハンドラーミドルウェアを優先して使用します。
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
app.UseStatusCodePages();
プロジェクトで例外からステータスコードへのカスタムマッピング(例:NotFoundException に対して 404 を返すこと)が必要な場合は、IExceptionHandler を実装します。
internal sealed class ApiExceptionHandler(ILogger<ApiExceptionHandler> logger)
: IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
var (statusCode, title) = exception switch
{
KeyNotFoundException => (StatusCodes.Status404NotFound, "Not Found"),
ArgumentException => (StatusCodes.Status400BadRequest, "Bad Request"),
InvalidOperationException => (StatusCodes.Status409Conflict, "Conflict"),
_ => (0, (string?)null)
};
if (statusCode == 0)
return false; // Let the default handler deal with it
// Important: returning true below suppresses the exception diagnostics middleware
// for this exception, so ensure it is logged/telemetrized before returning.
logger.LogWarning(exception, "Handled API exception: {Title}", title);
httpContext.Response.StatusCode = statusCode;
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = statusCode,
Title = title,
// Do not use exception.Message here — it may leak sensitive internal details.
// Use a safe, user-facing message instead.
Detail = title,
Instance = httpContext.Request.Path
}, cancellationToken);
return true;
}
}
登録します。
builder.Services.AddExceptionHandler<ApiExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
ファイルの配置: プロジェクトの構成を一貫させるため、例外ハンドラーのクラスは必ず Middleware/ フォルダーに配置してください。プロジェクトのルートには配置しないでください。
ステップ 6:サービスレイヤーを使用する
コントローラーやエンドポイントハンドラーにデータストアを直接注入しないでください。データアクセスロジックと、エンティティとリクエスト/レスポンス型間のマッピングを担うサービスインターフェイスと sealed 実装クラスを作成します。
すべてのサービスに必ずインターフェイスを定義してください。これにより、モックを使った単体テストが可能になり、依存性逆転の原則にも従えます。
// Services/IProductService.cs
public interface IProductService
{
Task<IReadOnlyList<ProductResponse>> GetAllAsync(CancellationToken ct);
Task<ProductResponse?> GetByIdAsync(int id, CancellationToken ct);
Task<ProductResponse> CreateAsync(CreateProductRequest request, CancellationToken ct);
}
// Services/ProductService.cs
public sealed class ProductService(...) : IProductService
{
// Data access logic, entity-to-DTO mapping
}
具象型ではなく、インターフェイスを登録します。
// In Program.cs
builder.Services.AddScoped<IProductService, ProductService>();
EF Core のデータアクセスパターン(マイグレーション、Fluent API による構成、AsNoTracking、シードデータ)については、optimizing-ef-core-queries スキルを参照してください。
ステップ 7:.http テストファイルを作成する
エンドポイントを実装した後、すべての新しいエンドポイントの呼び出し方を示す .http ファイルをプロジェクトのルートに作成してください。これは、継続的に更新されるドキュメントとして、また手軽な手動テスト用ハーネスとして機能します。
@baseUrl = http://localhost:5000
### Get all products
GET {{baseUrl}}/api/products
### Get product by ID
GET {{baseUrl}}/api/products/1
### Create a product
POST {{baseUrl}}/api/products
Content-Type: application/json
{
"name": "Wireless Mouse",
"price": 29.99,
"category": "Electronics"
}
### Delete a product
DELETE {{baseUrl}}/api/products/1
現実的な本文を含むリクエストを、エンドポイントごとに少なくとも 1 つ記述してください。存在しない ID など、エラーになるケースも示します。ポート番号は launchSettings.json に合わせてください。
ステップ 8:ビルドして検証する
dotnet buildを実行し、エラーと警告がどちらも 0 件であることを確認します。- アプリを起動し、OpenAPI ドキュメントを読み込めることを確認します(既定値:
/openapi/v1.json)。 .httpファイルのリクエストを実行し、ステータスコードが正しいことを確認します。
検証
- すべてのエンドポイントが、ステップ 3 の表に従って正しい HTTP ステータスコードを返す
- POST エンドポイントが
Locationヘッダー付きで201 Createdを返す - DELETE エンドポイントが
204 No Contentを返す - すべてのエンドポイントシグネチャに
CancellationTokenが含まれている -
CancellationTokenが、すべての下流の非同期呼び出しに渡されている - OpenAPI ドキュメントが生成され、新しいエンドポイントがすべて含まれている
- エンドポイントに、OpenAPI 用の概要/説明メタデータが設定されている
- JSON レスポンスと OpenAPI スキーマで、列挙値が文字列として出力される(ユーザーが整数としてのシリアル化を明示的に要求した場合を除く)
- エラーレスポンスが RFC 7807 Problem Details 形式を使用している
- ドメインエンティティが API のリクエスト/レスポンスボディに直接公開されていない
- API で公開されるすべての DTO に
<summary>XML ドキュメントコメントがある - 日付と時刻のプロパティに
DateTimeではなくDateTimeOffsetを使用している - 新しい各エンドポイントへのリクエストを記述した
.httpファイルがある -
dotnet buildがエラー 0 件、警告 0 件で成功する - すべての DTO が
sealed record型である(可変クラスではない) - Minimal API のハンドラーが、明示的な
Results<T1, T2>戻り値の型とともにTypedResultsを使用している - すべてのサービスに対応するインターフェイスがあり、DI に登録されている
- 例外ハンドラーが
Middleware/フォルダーに配置されている
よくある落とし穴
| 落とし穴 | 解決策 |
|---|---|
| ドメインエンティティを API レスポンスとして公開する | リクエスト/レスポンス用に個別の sealed record 型を作成します。エンティティを公開すると、ナビゲーションプロパティや内部フィールドまで漏れてしまいます。 |
CancellationToken を忘れる | すべてのエンドポイントに追加し、非同期呼び出しチェーン全体に渡します。 |
POST に対して 200 OK を返す | Location ヘッダー付きで 201 Created を返します。 |
| OpenAPI メタデータが不足している | すべてのエンドポイントに .WithName()、.WithSummary()、.WithDescription()、.Produces<T>() をチェーンします。 |
| エンドポイントにデータストアを直接注入する | 分離とテスト容易性のため、インターフェイスを備えたサービスレイヤーを使用します。 |
| コントローラーと Minimal API のスタイルを混在させる | プロジェクトごとにどちらか一方を選び、一貫性を保ちます。 |
明示的な戻り値の型なしで、三項演算子内に TypedResults を使用する | Ok<T> と NotFound には共通の基底型がありません。Task<Results<Ok<T>, NotFound>> として注釈するか、Results ファクトリーメソッドを使用します。 |
| DTO に可変クラスを使用する | 位置指定構文(レスポンス)または init プロパティ(リクエスト)を使った sealed record を使用します。 |
| インターフェイスなしでサービスを登録する | IService を定義し、AddScoped<IService, Service>() で登録します。 |
新しい .NET 9 以降のプロジェクトに Swashbuckle.* パッケージを追加する | 組み込みの AddOpenApi() + MapOpenApi() を使用します。Swashbuckle.AspNetCore、Swashbuckle.AspNetCore.SwaggerUI、その他の Swashbuckle パッケージは追加しないでください。 |
| DTO に XML ドキュメントコメントがない | すべてのリクエスト型とレスポンス型に <summary> XML ドキュメントコメントを追加します。これらは自動的に生成された OpenAPI 仕様に反映されます。 |
日付/時刻プロパティに DateTime を使用する | 代わりに DateTimeOffset を使用します。UTC オフセットが保持され、タイムゾーンの曖昧さを避けられ、JSON に正しくシリアル化されます。 |
| 列挙値を整数としてシリアル化する | JsonStringEnumConverter を設定し、列挙値が既定で文字列としてシリアル化されるようにします。整数としてのシリアル化は、ユーザーが明示的に要求した場合にのみ使用してください。 |
詳細情報
- ASP.NET Core Web API の概要 — Web API を構築するための基本概念
- ASP.NET Core の OpenAPI — .NET 9 以降に組み込まれている OpenAPI サポート
- XML コメントからの OpenAPI — XML ドキュメントコメントが OpenAPI 仕様に反映される仕組み
- Minimal API の概要 — ルーティング、パラメーターのバインド、レスポンスの型
- ASP.NET Core API でのエラー処理 — Problem Details と例外処理
- DateTimeOffset — API の日付/時刻の値に推奨される型