本文へスキップ
.NET 10 Minimal APIs 実践ガイドのアイキャッチ画像
C# Tips

.NET 10 Minimal APIs 実践ガイド

公開: 更新: 約6分で読めます

ASP.NET Core の Minimal APIs は、.NET 6 での導入から改良を重ね、.NET 10 の時点で本格的な業務システムにも十分耐える成熟度に達しています。ルーティング、依存性注入、認証・認可、OpenAPI ドキュメント生成といった従来 MVC コントローラで培われてきた機能の多くを、より少ない記述量で表現できます。本稿では、Minimal APIs を実運用に載せるうえで押さえておきたい要素を、コード例を交えて整理します。

基本構造とルーティング

Minimal APIs のエントリポイントは WebApplication です。WebApplicationBuilder でサービスとパイプラインを構成し、MapGet / MapPost などのメソッドでルートとハンドラを対応付けます。ハンドラはラムダ式でもメソッド参照でも記述できます。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();

app.MapGet("/health", () => "OK");
app.MapGet("/users/{id:int}", GetUserById);

app.Run();

static async Task<IResult> GetUserById(int id, IUserService service)
{
    var user = await service.GetAsync(id);
    return user is null ? TypedResults.NotFound() : TypedResults.Ok(user);
}

ルートテンプレートには {id:int} のようにルート制約を付与でき、型やパターンに合致しないリクエストはハンドラに到達する前に 404 として処理されます。ハンドラの戻り値を Task<IResult> にしておくと、成功と失敗で異なるレスポンスを返す分岐が明快になります。

ルートグループでまとめる

エンドポイントが増えてくると、共通のプレフィックスやポリシーを一括で管理したくなります。MapGroup を使うと、URL の接頭辞、認可要件、フィルタ、OpenAPI のタグなどをグループ単位で適用できます。

var users = app.MapGroup("/api/users")
    .RequireAuthorization()
    .WithTags("Users");

users.MapGet("/", GetAllUsers);
users.MapGet("/{id:int}", GetUserById);
users.MapPost("/", CreateUser);

// バージョニングもグループの入れ子で表現できます
var v1 = app.MapGroup("/api/v1");
var v2 = app.MapGroup("/api/v2");

グループに設定した内容は配下のすべてのエンドポイントへ伝播するため、認可の付け忘れといった設定漏れを構造的に防げます。ネスト(入れ子)も可能で、バージョンや機能単位での階層化に向いています。

.NET 10 Minimal APIs のリクエスト処理の流れを示した図。HTTPリクエストがルートグループ、エンドポイントフィルタ、ハンドラを経て TypedResults を返し、OpenAPI ドキュメントが自動生成される様子を表している
Minimal APIs ではリクエストがルートグループとフィルタを通ってハンドラに届き、TypedResults として返る流れになります

パラメータバインディングと TypedResults

ハンドラの引数は、ソースを明示しなくても慣例に沿って自動的にバインドされます。ルートテンプレートに一致する名前はルート値から、単純型のクエリ文字列はクエリから、複合型のボディは JSON から、登録済みのサービスは DI コンテナから解決されます。ソースを明示したい場合は属性を付けます。

app.MapGet("/search", (
    [FromQuery] string keyword,
    [FromQuery] int page,
    IUserService service) =>
        service.SearchAsync(keyword, page));

app.MapPost("/api/users", (
    [FromBody] CreateUserRequest request,
    IUserService service) =>
        service.CreateAsync(request));

レスポンスは Results でも生成できますが、実運用では戻り値の型が具体化される TypedResults を推奨します。TypedResults は静的型付けされているため、テストでのアサーションが書きやすく、後述する OpenAPI のレスポンス型推論にも寄与します。

app.MapPost("/api/users", async (
    CreateUserRequest request,
    IUserService service) =>
{
    var created = await service.CreateAsync(request);
    return TypedResults.Created($"/api/users/{created.Id}", created);
});

複数のステータスを返す場合は Results<Created<User>, ValidationProblem> のように共用体型を戻り値に指定でき、返しうるレスポンスの種類がシグネチャから読み取れるようになります。

検証とエンドポイントフィルタ

.NET 10 では、[Required] や [Range] などのデータ注釈に基づく検証を Minimal APIs でも組み込みで有効化できます。AddValidation を登録すると、検証属性を持つパラメータが自動的に検査され、不正な場合は ValidationProblem が返されます。

builder.Services.AddValidation();

public record CreateUserRequest(
    [property: Required, StringLength(50)] string Name,
    [property: Required, EmailAddress] string Email);

属性で表現しきれない業務ルールは、エンドポイントフィルタで実装します。フィルタはハンドラの前後に処理を差し込める仕組みで、検証、監査ログ、例外の整形などの横断的関心事をハンドラ本体から切り離せます。

app.MapPost("/api/orders", CreateOrder)
    .AddEndpointFilter(async (context, next) =>
    {
        var order = context.GetArgument<OrderRequest>(0);
        if (order.Quantity <= 0)
        {
            return TypedResults.ValidationProblem(new Dictionary<string, string[]>
            {
                ["quantity"] = ["数量は 1 以上で指定してください。"]
            });
        }
        return await next(context);
    });

フィルタはグループにも適用でき、複数を連ねると登録順にパイプラインを形成します。共通の入力検証はグループフィルタ、個別ルール単体のフィルタ、という使い分けが実用的です。

認証・認可と OpenAPI

認証と認可はコントローラと同じ基盤を利用します。認証スキームとポリシーを登録し、エンドポイントまたはグループに RequireAuthorization を付与します。

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});

app.MapDelete("/api/users/{id:int}", DeleteUser)
    .RequireAuthorization("AdminOnly");

OpenAPI ドキュメントは、外部パッケージに頼らず Microsoft.AspNetCore.OpenApi による組み込み生成を利用できます。AddOpenApi と MapOpenApi を追加すれば、JSON 形式のドキュメントが自動的に公開されます。TypedResults や検証属性から返却型や制約が推論されるため、注釈を最小限に保てます。

builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi(); // /openapi/v1.json を公開

app.MapGet("/api/users/{id:int}", GetUserById)
    .WithName("GetUserById")
    .WithSummary("ユーザー情報を取得します");

Swagger UI や Scalar などの表示 UI が必要な場合は、生成されたドキュメントを別途 UI パッケージに読み込ませる構成が一般的です。ドキュメントの生成と表示を分離することで、生成部分はフレームワーク標準に寄せられます。

レート制限と出力キャッシュ

公開 API では、過剰なアクセスを抑えるレート制限が欠かせません。組み込みのレート制限ミドルウェアでポリシーを定義し、エンドポイント単位で適用できます。

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
    });
});

var app = builder.Build();
app.UseRateLimiter();

app.MapGet("/api/products", GetProducts)
    .RequireRateLimiting("fixed");

更新頻度の低い読み取り系エンドポイントには、出力キャッシュを併用するとバックエンドの負荷を軽減できます。キャッシュポリシーは有効期限やキーの構成を柔軟に指定できます。

builder.Services.AddOutputCache();

var app = builder.Build();
app.UseOutputCache();

app.MapGet("/api/products", GetProducts)
    .CacheOutput(policy => policy.Expire(TimeSpan.FromMinutes(5)));

コントローラとの使い分け

Minimal APIs がすべての場面で最適というわけではありません。エンドポイント数が限られた API、マイクロサービス、サーバーレス関数のように起動の軽さと記述量の少なさが効く場面では有力な選択肢です。一方で、モデルバインダのカスタマイズ、アクションフィルタの体系的な適用、大規模なアプリケーション部品の分割といった要件が中心になる場合は、コントローラベースの構成が管理しやすいことがあります。

両者は排他的ではなく、同一プロジェクト内に共存できます。中核となる複雑なドメインはコントローラで、補助的なエンドポイントや内部向け API は Minimal APIs で、といった併用も現実的です。判断の軸は「Minimal かどうか」ではなく、対象とするエンドポイント群の複雑さと、チームが保守しやすい構造かどうかに置くとよいでしょう。ハンドラをメソッドとして切り出し、機能単位のファイルへ整理しておけば、Minimal APIs でも見通しは保てます。

エンハンスド株式会社では、Minimal APIs を含む .NET / ASP.NET Core を用いた Web API の設計・実装から、既存 API のモダナイゼーション、そして開発内製化の支援までを一貫して行っています。API 基盤の刷新や社内での開発体制構築をご検討の際は、お気軽にご相談ください。

参考リンク

この記事をシェア

コピーしました

関連記事