dotnet-minimal-apis について
ASP.NET Core Minimal API の構築と整理を支援する、.NET 開発者向けのスキルです。ルートグループ、フィルター、型付きレスポンス、パラメーターバインド、OpenAPI を扱います。
主な機能
- 01ルートグループとネストしたルート
- 02エンドポイントフィルターパイプライン
- 03TypedResults とレスポンススキーマ
- 04OpenAPI 3.1 の統合
- 05パラメーターバインドと JSON 設定
- 06エンドポイントの整理パターン
活用シーン
- 01関連する API エンドポイントの整理
- 02共通の検証・ログフィルターの適用
- 03OpenAPI 用レスポンススキーマの生成
- 04拡大する Minimal API プロジェクトの構成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-minimal-apisインストール後、Claude Code で/dotnet-minimal-apisを入力して読み込まれたか確認します。
詳細
dotnet-minimal-apis
Minimal API は、新しい ASP.NET Core HTTP API プロジェクトにおいて Microsoft が推奨するアプローチです。軽量なラムダベースのプログラミングモデル、組み込みの OpenAPI サポート、横断的関心事に対応するエンドポイントフィルター、大規模な API を整理するためのルートグループを提供します。
対象外: API のバージョニング戦略については、[skill:dotnet-api-versioning] を参照してください。入力検証のフレームワークとパターンについては、[skill:dotnet-input-validation] を参照してください。アーキテクチャパターン(垂直スライス、CQRS、クリーンアーキテクチャ)については、[skill:dotnet-architecture-patterns] を参照してください。認証と認可の実装については、[skill:dotnet-api-security] を参照してください。OpenAPI ドキュメントの生成とカスタマイズについては、[skill:dotnet-openapi] を参照してください。
相互参照:大規模な API の整理については [skill:dotnet-architecture-patterns]、リクエストの検証については [skill:dotnet-input-validation]、バージョニング戦略については [skill:dotnet-api-versioning]、OpenAPI のカスタマイズについては [skill:dotnet-openapi] を参照してください。
ルートグループ
ルートグループは、関連するエンドポイントを共通のプレフィックスの下にまとめ、共通の設定(フィルター、メタデータ、認可)を一度だけ適用します。共通のプレフィックスを使うことで、MapGet/MapPost の繰り返しのチェーンを置き換えます。
var app = builder.Build();
// Group endpoints under /api/products with shared configuration
var products = app.MapGroup("/api/products")
.WithTags("Products")
.RequireAuthorization();
products.MapGet("/", async (AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync()));
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
products.MapPost("/", async (CreateProductDto dto, AppDbContext db) =>
{
var product = new Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
});
products.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
{
if (await db.Products.FindAsync(id) is not Product product)
return TypedResults.NotFound();
db.Products.Remove(product);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
ネストされたグループ
グループをネストして、プレフィックスとフィルターを組み合わせることができます。
var api = app.MapGroup("/api")
.AddEndpointFilter<RequestLoggingFilter>();
var v1 = api.MapGroup("/v1");
var products = v1.MapGroup("/products").WithTags("Products");
var orders = v1.MapGroup("/orders").WithTags("Orders");
// Registers as: GET /api/v1/products
products.MapGet("/", GetProducts);
// Registers as: POST /api/v1/orders
orders.MapPost("/", CreateOrder);
エンドポイントフィルター
エンドポイントフィルターは、横断的関心事(ロギング、検証、認可情報の付加)に対応するパイプラインを提供します。MVC のアクションフィルターに似ていますが、Minimal API 専用です。
IEndpointFilter インターフェイス
public sealed class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// Extract the argument of type T from the endpoint parameters
var argument = context.Arguments
.OfType<T>()
.FirstOrDefault();
if (argument is null)
return TypedResults.BadRequest("Request body is required");
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
{
return TypedResults.ValidationProblem(
result.ToDictionary());
}
return await next(context);
}
}
フィルターの適用
// Apply to a single endpoint
products.MapPost("/", CreateProduct)
.AddEndpointFilter<ValidationFilter<CreateProductDto>>();
// Apply to an entire route group
var products = app.MapGroup("/api/products")
.AddEndpointFilter<RequestLoggingFilter>();
// Inline filter using a lambda
products.MapGet("/{id:int}", GetProductById)
.AddEndpointFilter(async (context, next) =>
{
var id = context.GetArgument<int>(0);
if (id <= 0)
return TypedResults.BadRequest("ID must be positive");
return await next(context);
});
フィルターの実行順序
フィルターは登録順に実行されます(最初に登録されたものが最外層になります)。すべてのフィルターを通過した後に、エンドポイントハンドラーが実行されます。
Request -> Filter1 -> Filter2 -> Filter3 -> Handler
Response <- Filter1 <- Filter2 <- Filter3 <-
TypedResults
Minimal API の戻り値には、Results(インターフェイスファクトリー)ではなく、常に TypedResults(静的ファクトリー)を使用してください。TypedResults は具体的な型を返すため、OpenAPI メタデータジェネレーターがビルド時に調べることができ、正確なレスポンススキーマが自動的に生成されます。
// PREFERRED: TypedResults -- concrete return types, auto-generates OpenAPI metadata
products.MapGet("/{id:int}", async Task<Results<Ok<Product>, NotFound>> (
int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
// AVOID: Results -- returns IResult, OpenAPI generator cannot infer response types
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound());
共用体の戻り値の型
1 つのエンドポイントで考えられるすべてのレスポンス型を宣言するには、Results<T1, T2, ...> を使用してください。複数のレスポンスコードを含む正確な OpenAPI ドキュメントを生成できます。
products.MapPost("/", async Task<Results<Created<Product>, ValidationProblem, Conflict>> (
CreateProductDto dto, AppDbContext db) =>
{
if (await db.Products.AnyAsync(p => p.Sku == dto.Sku))
return TypedResults.Conflict();
var product = new Product { Name = dto.Name, Sku = dto.Sku, Price = dto.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
});
OpenAPI 3.1 の統合
.NET 10 では、Microsoft.AspNetCore.OpenApi を介した組み込みの OpenAPI 3.1 サポートが追加されます。Minimal API は、TypedResults、パラメーターのバインディング、属性から OpenAPI メタデータを自動的に生成します。
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // Serves /openapi/v1.json
}
メタデータの拡充
products.MapGet("/{id:int}", GetProductById)
.WithName("GetProductById")
.WithSummary("Get a product by its ID")
.WithDescription("Returns the product details for the specified ID, or 404 if not found.")
.Produces<Product>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound);
高度な OpenAPI のカスタマイズ(ドキュメントトランスフォーマー、オペレーショントランスフォーマー、スキーマのカスタマイズ)については、[skill:dotnet-openapi] を参照してください。
大規模化に対応する整理パターン
API が少数のエンドポイントを超えて拡大したら、エンドポイントを個別の静的クラスまたは拡張メソッドに整理してください。
拡張メソッドパターン
// ProductEndpoints.cs
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/products")
.WithTags("Products");
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id:int}", Update);
group.MapDelete("/{id:int}", Delete);
return group;
}
private static async Task<Ok<List<Product>>> GetAll(AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync());
private static async Task<Results<Ok<Product>, NotFound>> GetById(
int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product p
? TypedResults.Ok(p)
: TypedResults.NotFound();
private static async Task<Created<Product>> Create(
CreateProductDto dto, AppDbContext db)
{
var product = new Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
}
private static async Task<Results<NoContent, NotFound>> Update(
int id, UpdateProductDto dto, AppDbContext db)
{
var product = await db.Products.FindAsync(id);
if (product is null) return TypedResults.NotFound();
product.Name = dto.Name;
product.Price = dto.Price;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
private static async Task<Results<NoContent, NotFound>> Delete(
int id, AppDbContext db)
{
var product = await db.Products.FindAsync(id);
if (product is null) return TypedResults.NotFound();
db.Products.Remove(product);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
}
// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapCustomerEndpoints();
Carter ライブラリ
エンドポイントモジュールの自動検出を優先するプロジェクトでは、Carter ライブラリが ICarterModule インターフェイスを提供します。
// <PackageReference Include="Carter" Version="8.*" />
public sealed class ProductModule : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products").WithTags("Products");
group.MapGet("/", async (AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync()));
group.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product p
? TypedResults.Ok(p)
: TypedResults.NotFound());
}
}
// Program.cs
builder.Services.AddCarter();
var app = builder.Build();
app.MapCarter(); // Auto-discovers and registers all ICarterModule implementations
垂直スライスによる整理
垂直スライスアーキテクチャを使用するプロジェクト([skill:dotnet-architecture-patterns] を参照)では、各機能のエンドポイント、ハンドラー、モデルを 1 つのディレクトリにまとめます。
Features/
Products/
GetProducts.cs # Endpoint + handler + response DTO
CreateProduct.cs # Endpoint + handler + request/response DTOs
UpdateProduct.cs
DeleteProduct.cs
ProductEndpoints.cs # Route group registration
JSON の設定
Minimal API は既定で System.Text.Json を使用します。すべての Minimal API エンドポイントに対して、JSON オプションをグローバルに設定します。
// ConfigureHttpJsonOptions applies to Minimal APIs ONLY, not MVC controllers
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
注意点: ConfigureHttpJsonOptions は Minimal API の JSON シリアル化のみを設定します。MVC コントローラーは別のパイプラインを使用するため、builder.Services.AddControllers().AddJsonOptions(...) で設定してください。これらを取り違えても、設定は反映されません。
パラメーターのバインディング
Minimal API は、型と属性の注釈に基づいて、ルート、クエリ、ヘッダー、本文、DI からパラメーターを自動的にバインドします。
// Route parameter (from URL segment)
app.MapGet("/products/{id:int}", (int id) => ...);
// Query string
app.MapGet("/products", ([FromQuery] int page, [FromQuery] int pageSize) => ...);
// Header
app.MapGet("/products", ([FromHeader(Name = "X-Correlation-Id")] string correlationId) => ...);
// Body (JSON deserialized)
app.MapPost("/products", (CreateProductDto dto) => ...);
// DI-injected services (resolved automatically)
app.MapGet("/products", (AppDbContext db, ILogger<Program> logger) => ...);
// AsParameters: bind a complex object from multiple sources
app.MapGet("/products", ([AsParameters] ProductQuery query) => ...);
public record ProductQuery(
[FromQuery] int Page = 1,
[FromQuery] int PageSize = 20,
[FromQuery] string? SortBy = null);
エージェントの注意点
TypedResultsが利用できる場合は、Resultsを使用しないでください --Results.Ok(value)はIResultを返すため、OpenAPI ジェネレーターはレスポンススキーマを推論できません。自動スキーマ生成を有効にするには、TypedResults.Ok(value)を使用してください。ConfigureHttpJsonOptionsが適用されるのは Minimal API のみであることを忘れないでください -- MVC コントローラーには、別途.AddControllers().AddJsonOptions()が必要です。- すべてのエンドポイントに検証ロジックをインラインで適用しないでください -- エンドポイントフィルターを使用するか、検証パターンを一元化する方法については [skill:dotnet-input-validation] を参照してください。
- フィルターを誤った順序で登録しないでください -- 最初に登録したフィルターが最も外側になります。広範囲に適用するフィルター(ロギング)を先に、対象を絞ったフィルター(検証)をハンドラーに近い位置に配置してください。
- すべてのエンドポイントを
Program.csに配置しないでください -- エンドポイントが数個を超えたら、拡張メソッドクラスまたは Carter モジュールに整理してください。
前提条件
- .NET 8.0+(ルートグループとエンドポイントフィルターを備えた Minimal API の LTS 基準)
- 組み込みの OpenAPI 3.1、SSE、検証サポートを利用する場合は .NET 10.0
- OpenAPI ドキュメント生成用の
Microsoft.AspNetCore.OpenApi - エンドポイントモジュールを自動検出する場合は
Carter(任意)
ナレッジソース
このスキルの Minimal API パターンは、次のガイダンスに基づいています。
- David Fowler -- AspNetCoreDiagnosticScenarios (github.com/davidfowl/AspNetCoreDiagnosticScenarios)。ASP.NET Core のリクエストパイプライン設計、ミドルウェアのベストプラクティス、診断に関するアンチパターンの権威ある情報源です。
これらの情報源は、上記のパターンとその根拠に役立てています。このスキルは、特定の個人を代表したり、その個人の意見を代弁したりするものではありません。