コンテンツにスキップ

第4章:Minimal API の解説と使いどころ

  1. Minimal API とは何か?(MVC ベースとどう違うか)
  2. Minimal API の基本構文(MapGet, MapPost 等)
  3. DI を使ったハンドラ注入、MapGroup・エンドポイントグループ化などの構成パターン
  4. Minimal API の制約・注意点と MVC との比較(構造、可読性、機能面)
  5. どのようなケースで使うかの判断基準
  6. 参考ドキュメント

1. Minimal API とは何か?(MVC ベースとどう違うか)

Section titled “1. Minimal API とは何か?(MVC ベースとどう違うか)”

Minimal API は、ASP.NET Core (.NET 6 以降) で導入された シンプルな HTTP API 構築手法 です。
従来の MVC コントローラーを使用せず、 わずかなコードで REST エンドポイントを定義 できるのが特徴です。
具体的には、Program.cs(エントリーポイント)上で ルートと対応処理(ハンドラ)を直接コードでマッピング することで、コントローラーやアクションメソッドのボイラープレートを省略できます。

※この構成は、第1章:開発環境セットアップの 6. 初回プロジェクト作成 節で作成します。

ASP.NET Core テンプレートから作成した、Minimal API の最小コード例

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();

Hello World 実行画面

各エンドポイントにはラムダ式のほか、 ローカル関数、静的メソッド、インスタンスメソッドなど任意のデリゲートを指定可能 です。
例えば外部のクラスに static string SayHello() というメソッドがあれば、 app.MapGet("/hello", SayHello); のように渡すこともできます。
※全てのコードを Program.cs に書く必要はありません。より詳細なコード構成パターンに関しては本章の「3. DI を使ったハンドラ注入、MapGroup・エンドポイントグループ化などの構成パターン」節を参照ください。

public static class ExternalHandlers
{
public static string SayHello() => "Hello World!";
}
// 静的メソッドをそのままハンドラとして渡す
app.MapGet("/hello", ExternalHandlers.SayHello);

MVC コントローラー の場合は、クラス(Controller)とその中のメソッド(Action)としてエンドポイントを定義し、属性(例えば [HttpGet] など)やルーティング規約で HTTP リクエストに紐付けました。
一方、 Minimal API では 関数(デリゲート) を直接エンドポイントとしてマッピングします。

flowchart TB
    subgraph minimal ["Minimal API"]
        direction TB
        MA_Req["HTTP リクエスト"] --> MA_Map["app.MapGet('/path', handler)(ソースで明示定義)"]
        MA_Map --> MA_Fn["ハンドラ関数(ラムダ / メソッド)"]
        MA_Fn --> MA_Resp["HTTP レスポンス"]
    end
    subgraph mvc ["MVC"]
        direction TB
        MVC_Req["HTTP リクエスト"] --> MVC_Route["ルーティングエンジン(属性・規約で自動発見)"]
        MVC_Route --> MVC_Ctrl["XxxController クラス"]
        MVC_Ctrl --> MVC_Act["アクションメソッド"]
        MVC_Act --> MVC_Resp["HTTP レスポンス"]
    end

2. Minimal API の基本構文(MapGet, MapPost 等)

Section titled “2. Minimal API の基本構文(MapGet, MapPost 等)”

Minimal API では、 WebApplication オブジェクト(通常 app と変数名を付けます)に対し、 MapGetMapPost など HTTP メソッド別のマッピングメソッド を呼び出すことでエンドポイントを定義します。
/hello エンドポイント定義の例を再度見てみましょう。

public static class ExternalHandlers
{
public static string SayHello() => "Hello World!";
}
// 静的メソッドをそのままハンドラとして渡す
app.MapGet("/hello", ExternalHandlers.SayHello);

上記は GET リクエストのルート "/hello" に対し、 ExternalHandlers.SayHello を対応付けています。 ブラウザや HTTP クライアントで /hello にアクセスすると、このメソッドが実行され、文字列 "Hello World!" が HTTP レスポンスとして返されます。

パラメータ付きルート の定義もシンプルです。
例えば製品 ID を受け取る REST パスを作る場合は以下のように書けます。

app.MapGet("/products/{id}", (int id) => $"ProductId: {id}");

別途定義した static メソッドに渡す場合は以下のように書けます。

public static IResult GetById(int id)
{
var product = ProductStore.Products.FirstOrDefault(p => p.Id == id);
return product is not null ? Results.Ok(product) : Results.NotFound();
}
app.MapGet("/products/{id}", GetById);

"{id}" のようなルートパラメータを URL 中に定義すると、対応する型の引数(上記では int 型の id )として受け取れます。
この ルート値のバインド は MVC の Attribute Routing と同様ですが、Minimal API では 引数リストとルートテンプレートを突き合わせて自動バインド します。

HTTP メソッドの種類 も、 MapGet 以外に MapPost , MapPut , MapDelete などが用意されています。
一つの URL パスに対し複数のメソッドを受け付けたい場合は、 MapMethods("パス", new[] { "GET", "HEAD" }, handler) のような汎用メソッドを使用できます。

// GET と HEAD の両メソッドを受け付けるエンドポイント
app.MapMethods("/products/{id}", new[] { "GET", "HEAD" }, (int id, IProductRepository repo) =>
{
var product = repo.GetById(id);
return product is not null ? Results.Ok(product) : Results.NotFound();
});

Minimal API では、ハンドラが返す値をフレームワークが解析し、適切な HTTP レスポンスに変換します。
例えば 文字列 を返せば text/plain で返送され、 オブジェクト を返せば JSON にシリアライズされて返送されます(既定では System.Text.Json を使用)。
voidTask を返すハンドラで何も返さなければ 204 No Content となります。

コントローラーの IActionResult に相当する 汎用結果型 として、Minimal API では IResult(および具体実装の Results ヘルパー)が用意されています。
例えば明示的に HTTP ステータスを制御したいときは Results.NotFound()Results.Ok(data)Results.Created("/resource/123", obj) といった ファクトリーメソッド を return できます。
再度、先ほどの GetById の例を見てみましょう。

public static IResult GetById(int id)
{
var product = ProductStore.Products.FirstOrDefault(p => p.Id == id);
return product is not null ? Results.Ok(product) : Results.NotFound();
}
app.MapGet("/products/{id}", GetById);

上記では、 GET /products/{id} でリストから商品を検索し、見つからなければ 404 を返し、見つかれば 200 と JSON データを返しています。

また、ASP.NET Core 7 以降ではジェネリック版の TypedResults.Ok<T>(data) を利用でき、追加の属性付与などを行うことなく OpenAPI ドキュメントにレスポンス型を反映できます(詳しくは「4. Minimal API の制約・注意点と MVC との比較(構造、可読性、機能面)」節参照)。

public static Results<Ok<Product>, NotFound> GetById(int id)
{
var product = ProductStore.Products.FirstOrDefault(p => p.Id == id);
return product is not null ? TypedResults.Ok(product) : TypedResults.NotFound();
}

3. DI を使ったハンドラ注入、MapGroup・エンドポイントグループ化などの構成パターン

Section titled “3. DI を使ったハンドラ注入、MapGroup・エンドポイントグループ化などの構成パターン”

TODO DI の詳細は6章で扱います。

ASP.NET Core の DI 機能は Minimal API のハンドラでもシームレスに利用できます。
コントローラーではコンストラクタインジェクションで行う形式が一般的ですが、Minimal API では ハンドラ関数のパラメータ としてサービスを受け取ります。
具体的には、builder.Services に登録した任意のサービス型をハンドラの引数に含めれば、実行時に自動解決されます。

たとえば、データアクセス用のリポジトリサービス IProductRepository を DI コンテナーに登録してある場合のエンドポイント定義は次の通りです。

Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IProductRepository, ProductRepository>(); // DI 登録
var app = builder.Build();
app.MapGet("/products/{id}", (int id, IProductRepository repo) => // 登録した具象クラスをIProductRepositoryとして受け取る
{
var prod = repo.Find(id);
return prod is not null ? Results.Ok(prod) : Results.NotFound();
});
app.Run();

IProductRepository repo という形でパラメータに宣言するだけで、呼び出し時にフレームワークがコンテナーから repo インスタンスを供給します。
この仕組みにより、Minimal API でも ビジネスロジックをサービスクラスに委譲 し、ハンドラが膨らむことを防止できます。

Minimal API のハンドラ引数にはサービス以外にも、 HTTP リクエストからの各種データを直接バインド できます。
上記の id のようにルートからの取得はもちろん、 クエリ文字列ヘッダー もパラメータとして受け取れます。

例えば (int page, [FromHeader(Name="X-Custom")] string customHeader) とパラメータを書けば、page?page= クエリ値から、customHeader は HTTP ヘッダー X-Custom からそれぞれバインドされます。

// クエリ文字列とヘッダーをバインドする例
app.MapGet("/items", (int page, [FromHeader(Name = "X-Custom")] string customHeader, ILogger<Program> logger) =>
{
logger.LogInformation("page: {Page}, X-Custom: {CustomHeader}", page, customHeader);
return Results.Ok();
});

複合型(クラス)の引数がある場合、 JSON リクエストボディ からマッピングされます。
例えば下記 MapPost の例では Product 型のオブジェクト newProd として JSON ボディから自動デシリアライズされます。

// 複合型の引数は JSON リクエストボディから自動デシリアライズされる
app.MapPost("/products", (Product newProd, IProductRepository repo) =>
{
repo.Add(newProd);
return Results.Created($"/products/{newProd.Id}", newProd);
});

フォームデータファイルアップロードIFormFile)にも対応しており、必要に応じて [FromForm] 属性を付与して明示的にフォーム由来であることを指定できます。ファイル受信の詳細や検証方法は第7章(前編):Minimal API でのファイル受信を参照してください。

Program.cs
using Microsoft.AspNetCore.Mvc; // [FromForm] は暗黙の using に含まれないため明示的に必要
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery(); // Antiforgery サービスの登録(必須)
var app = builder.Build();
app.UseAntiforgery(); // Antiforgery トークン検証ミドルウェア(必須)
// フォームデータとファイルアップロードをバインドする例
app.MapPost("/upload", ([FromForm] string description, IFormFile file, ILogger<Program> logger) =>
{
logger.LogInformation("description: {Description}, fileName: {FileName}, fileSize: {FileSize}", description, file.FileName, file.Length);
return Results.Ok();
});
app.Run();
flowchart TD
    subgraph sources ["バインド元(引数の型と名前で自動判定)"]
        R["ルートパラメータ 例: /items/{id}"]
        Q["クエリ文字列 例: ?page=2"]
        B["リクエストボディ(JSON) ※複合型の引数"]
        H["ヘッダー [FromHeader] 属性を付与"]
        S["DI サービス 登録済みインターフェイス"]
    end
    Binder["Minimal API パラメータバインダー"]
    subgraph args ["ハンドラ関数の引数"]
        A1["int id (ルートから)"]
        A2["Product newProd (JSON ボディから)"]
        A3["IProductRepository repo (DI から)"]
        A4["string locale (ヘッダーから)"]
    end
    R --> Binder
    Q --> Binder
    B --> Binder
    H --> Binder
    S --> Binder
    Binder --> A1
    Binder --> A2
    Binder --> A3
    Binder --> A4

エンドポイントのグループ化(MapGroup)・バージョン管理

Section titled “エンドポイントのグループ化(MapGroup)・バージョン管理”

アプリが大きくなるにつれ、エンドポイントを 論理的にグループ化 したり 共通の設定をまとめて適用 したくなる場合があります。
Minimal API では .NET 7 から MapGroup メソッドが追加され、これにより ルートパスの共通部分を持つエンドポイントをひとまとめ にできます。

var group = app.MapGroup("/products");
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id}", Update);
group.MapDelete("/{id}", Delete);

活用例として、管理者用の API 群 /admin/... に認可を必須付与する場合、以下のように実装できます。

var adminGroup = app.MapGroup("/admin"); // admin グループを追加
adminGroup.RequireAuthorization("AdminPolicy"); // admin グループに AdminPolicy ポリシーを適用
adminGroup.MapGet("/users", GetUsers); // グループに登録(GET /admin/users)

上記例では app.MapGroup("/admin")"/admin" プレフィックスを持つグループを作成し、返ってきた adminGroupRouteGroupBuilder)に対して RequireAuthorization を呼び出しています。
これにより、このグループ配下の全エンドポイントに共通して認可ポリシー “AdminPolicy” が適用されます。
この状態で adminGroup.MapGet(...) のように通常通りエンドポイントを定義すれば、URL は自動的に /admin/users となり、認可も一括適用されます。

グループには他にも WithMetadata(...) で共通のメタデータ(例えば OpenAPI 用のタグや Deprecated 指定など)を付与したり、WithTags("Admin") で Swagger UI 上の分類名を設定する、といった使い方も可能です。
ネストしたグループ もサポートされており、var v1 = app.MapGroup("/api/v1"); var products = v1.MapGroup("/products"); のように階層構造でグループを作り、それぞれに設定を与えることもできます。

var v1 = app.MapGroup("/api/v1").WithGroupName("v1"); // v1 グループを追加
var products = v1.MapGroup("/products").WithTags("Products"); // Products 分類を v1 下に追加
products.MapGet("/", GetAll); // グループに登録(GET /api/v1/products)

エンドポイントが増えてきたら、機能(ドメイン)単位でフォルダを分割し、各機能に対応する拡張メソッドクラスを配置します。
Program.cs にはサービス登録とエンドポイントのマッピング呼び出しのみを残すのがポイントです。
この構成を実現するための一つのディレクトリ構造例を以下に示します。

MyApi/
├── Program.cs # エントリーポイント(サービス登録・ルート登録の呼び出しのみ)
├── Features/
│ ├── Products/ # 機能(ドメイン)単位でフォルダを切り、エンドポイント・サービス・モデルをまとめる
│ │ ├── ProductsEndpoints.cs # MapProducts など拡張メソッド定義
│ │ ├── ProductsService.cs # ビジネスロジック
│ │ └── ProductModels.cs # モデル定義(Product レコードなど)
│ └── Orders/
│ ├── OrdersEndpoints.cs
│ ├── OrdersService.cs
│ └── OrderModels.cs
└── MyApi.csproj

このとき Program.cs は下記のような見通しの良い構成になります。

Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapProducts(); // Features/Products/ProductsEndpoints.cs で定義
app.MapOrders(); // Features/Orders/OrdersEndpoints.cs で定義
app.Run();

この形を実現するための ProductsEndpoints.cs 実装例は下記の通りです。

// ProductsEndpoints.cs ─ 商品エンドポイントを拡張メソッドに分離
using Microsoft.AspNetCore.Http.HttpResults;
public static class ProductsEndpoints
{
public static IEndpointRouteBuilder MapProducts(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/products").WithTags("Products");
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id}", Update);
group.MapDelete("/{id}", Delete);
return app;
}
private static Ok<List<Product>> GetAll() =>
TypedResults.Ok(ProductStore.Products);
// 他のエンドポイント用メソッド定義
// ...
}

MVC でエリアやコントローラークラスに分割していたように、Minimal API でも 関心ごとにコードを分離 することで大規模開発に耐えうる設計が可能です。
これは Vertical Slice Architecture とも呼ばれ、各機能を自己完結的に実装することで、変更の影響範囲を限定しやすくなる利点があります。
※このアーキテクチャ採用およびディレクトリ構成は必ずこの通りにしなければならないといったものではなく、あくまで構成の一例となります。参考程度にご活用ください。

Minimal API は Microsoft.AspNetCore.OpenApi パッケージを使用した OpenAPI ドキュメントの自動生成に対応しています。

Minimal API では、メソッドチェーンで OpenAPI メタ情報を明示的に付加します。
また、戻り値の型に TypedResults を使用することで、レスポンス型が静的に解析され余分な設定を行うことなく OpenAPI 定義にレスポンス情報が反映されます。

Program.cs
builder.Services.AddOpenApi(); // Microsoft.AspNetCore.OpenApi パッケージ
var app = builder.Build();
app.MapOpenApi(); // /openapi/v1.json を公開
app.MapGet("/api/v1/products/{id}", (int id, IProductRepository repo) =>
repo.Find(id) is Product p ? TypedResults.Ok(p) : TypedResults.NotFound()) // TypedResults によりレスポンス型が静的に解析される
.WithName("GetProductById") // operationId
.WithSummary("商品を ID で取得します。")
.WithDescription("指定した ID に一致する商品を返します。存在しない場合は 404 を返します。")
.WithTags("Products") // Swagger UI 上のグループ名
.Produces<Product>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);

Swashbuckle.AspNetCore パッケージを使用して、Swagger UI を確認することも可能です。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new() { Title = "MinimalAPISample API", Version = "v1" });
options.SwaggerDoc("v2", new() { Title = "MinimalAPISample API", Version = "v2" });
options.DocInclusionPredicate((version, apiDescription) => apiDescription.GroupName == version);
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
options.SwaggerEndpoint("/swagger/v2/swagger.json", "v2");
});
}
app.MapProductsV1();
app.MapProductsV2();
app.Run();

Swagger画面

4. Minimal API の制約・注意点と MVC との比較(構造、可読性、機能面)

Section titled “4. Minimal API の制約・注意点と MVC との比較(構造、可読性、機能面)”

Minimal API は少ないコードで動く反面、エンドポイントが増えた場合に コードがスケーラブルに整理されていないと読みづらくなる 懸念があります。
MVC ではコントローラーやフォルダ構成(Areas など)で自然と分類されますが、Minimal API では 開発者自身が意識して整理 する必要があります。
極端に言うと全エンドポイントを Program.cs に直書きするのは非現実的であるため、例えば拡張メソッド(静的クラス+メソッド)にまとめるといったパターンを取る必要があります。

整理していない例(すべて Program.cs に直書き)

// Program.cs ─ エンドポイントが増えるにつれファイルが肥大化する
var builder = WebApplication.CreateBuilder(args);
// ... サービス登録 ...
var app = builder.Build();
app.MapGet("/api/v1/products", (IProductRepository r) => r.GetAll());
app.MapGet("/api/v1/products/{id}", (int id, IProductRepository r) => r.Find(id));
app.MapPost("/api/v1/products", (Product p, IProductRepository r) => { r.Add(p); return Results.Created($"/products/{p.Id}", p); });
app.MapPut("/api/v1/products/{id}", (int id, Product p, IProductRepository r) => { r.Update(id, p); return Results.NoContent(); });
app.MapDelete("/api/v1/products/{id}",(int id, IProductRepository r) => { r.Delete(id); return Results.NoContent(); });
app.MapGet("/api/v2/products", (string? search, IProductRepository r) => r.GetAll(search));
// ... さらに続く ...
app.Run();

拡張メソッドによるファイル分割例

拡張メソッドを使って Program クラスのエンドポイント定義を複数ファイルに分割できます。

// Program.cs ─ エントリーポイントのみに絞る
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapProductsV1(); // ProductsV1Handler.cs で定義
app.MapProductsV2(); // ProductsV2Handler.cs で定義
app.Run();
// ProductsV1Handler.cs ─ 商品 v1 エンドポイントを拡張メソッドに分離
public static class ProductsV1Handler
{
public static void MapProductsV1(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/products").WithGroupName("v1").WithTags("Products");
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id}", Update);
group.MapDelete("/{id}", Delete);
}
private static Ok<List<Product>> GetAll() =>
TypedResults.Ok(ProductStore.Products);
// 他のエンドポイント用メソッド定義
// ...
}
// ProductsV2Handler.cs ─ 商品 v2 エンドポイントを拡張メソッドに分離
public static class ProductsV2Handler
{
public static void MapProductsV2(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v2/products").WithGroupName("v2").WithTags("Products");
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id}", Update);
group.MapDelete("/{id}", Delete);
}
private static Ok<ProductListResult> GetAll(string? search)
{
var result = string.IsNullOrWhiteSpace(search)
? ProductStore.Products
: ProductStore.Products.Where(p => p.Name.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList();
return TypedResults.Ok(new ProductListResult(result.Count, result));
}
// 他のエンドポイント用メソッド定義
// ...
}

Minimal API では、 ASP.NET Core 10 から組み込みのバリデーション(入力検証)機能 が利用可能になりました。
System.ComponentModel.DataAnnotations 名前空間の属性を使って検証ルールを宣言し、AddValidation() を呼び出すだけで有効化できます。

builder.Services.AddValidation() を呼び出してサービスを登録するだけで、フレームワークが自動的にエンドポイントフィルターを追加し、リクエストごとにバリデーションを実行します。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation(); // 組み込みバリデーションを有効化
var app = builder.Build();

DataAnnotations によるバリデーション定義

Section titled “DataAnnotations によるバリデーション定義”

検証ルールは、ハンドラに渡すモデルクラスやレコードのプロパティに DataAnnotations 属性 で宣言します。(第3章の「4. モデルバインディング / 入力検証 (Validation)」節で説明したものと同等のものです。)
ハンドラ引数がクラスまたはレコード型の場合、フレームワークがそのプロパティに付与された属性を自動的に評価します。

// バリデーション属性を持つ Product レコード
public record Product(
int? Id,
[Required] string Name,
[Range(1, 1000)] int Quantity,
[MaxLength(200)] string? Description
);
app.MapPost("/products", (Product newProd, IProductRepository repo) =>
{
repo.Add(newProd);
return Results.Created($"/products/{newProd.Id}", newProd);
});

検証失敗時には、ランタイムが自動的に 400 Bad Request を返し、どのフィールドがどの理由で失敗したかの詳細がレスポンスボディに含まれます。
主な DataAnnotations 属性の例は以下の通りです。

属性用途
[Required]必須フィールド(null・空文字を許可しない)
[Range(min, max)]数値の最小値と最大値を指定
[MaxLength(n)]文字列の最大長を指定
[MinLength(n)]文字列の最小長を指定
[StringLength(max)]文字列の長さの上限(下限も指定可)を制約
[EmailAddress]メールアドレス形式を検証
[Url]URL 形式を検証
[RegularExpression(pattern)]正規表現パターンに一致するかを検証

IValidatableObject によるカスタム検証

Section titled “IValidatableObject によるカスタム検証”

複数フィールドをまたいだ複雑な検証ロジックは、IValidatableObject インターフェイスを実装することで記述できます。

public record CreateOrderRequest(
[Required] string ProductName,
[Range(1, 100)] int Quantity,
DateTime ShipBy
) : IValidatableObject
{
// 複数フィールドを組み合わせた検証
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (ShipBy < DateTime.UtcNow.AddDays(1))
{
yield return new ValidationResult(
"ShipBy は明日以降の日付を指定してください。",
[nameof(ShipBy)]);
}
}
}

検証エラーのレスポンスカスタマイズ

Section titled “検証エラーのレスポンスカスタマイズ”

AddProblemDetails を使用すると、検証エラー時に返る 400 レスポンスの内容をカスタマイズできます。
CustomizeProblemDetails コールバックで、タイトルや追加フィールドを任意に変更できます。

builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
if (context.ProblemDetails.Status == 400)
{
context.ProblemDetails.Title = "入力値に問題があります。";
context.ProblemDetails.Extensions["traceId"] = Guid.NewGuid().ToString();
}
};
});

MVC の ActionFilterExceptionFilter などによるパイプライン拡張は、Minimal API では直接はありません。しかし .NET 7 で エンドポイントフィルター(EndpointFilter) という概念が追加され、 簡易的なフィルター処理 が可能になりました。
例えばエンドポイントの前後処理を共通化したり、エラー発生時の処理を一括定義するなど、ActionFilter 相当のことは実現できます。

ただしこちらもコード上でチェーンする形で仕込む必要があり、属性一つで付与できる MVC よりは 能動的な設定 が必要です。

MVC の ActionFilter 例(前後処理のロギング)

// フィルタークラスの実装
public class LoggingActionFilter : IActionFilter
{
private readonly ILogger<LoggingActionFilter> _logger;
public LoggingActionFilter(ILogger<LoggingActionFilter> logger)
{
_logger = logger;
}
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation("アクション実行前: {Action}", context.ActionDescriptor.DisplayName);
}
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogInformation("アクション実行後: {Action}", context.ActionDescriptor.DisplayName);
}
}
// Program.cs ですべてのコントローラーにグローバル適用
builder.Services.AddControllers(options =>
{
options.Filters.Add<LoggingActionFilter>();
});
builder.Services.AddScoped<LoggingActionFilter>();
// 特定のコントローラーのみに属性で適用
[ServiceFilter(typeof(LoggingActionFilter))]
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id) { /* ... */ }
}

Minimal API の EndpointFilter 例(前後処理のロギング)

// IEndpointFilter を実装したフィルタークラス
public class LoggingEndpointFilter : IEndpointFilter
{
private readonly ILogger<LoggingEndpointFilter> _logger;
public LoggingEndpointFilter(ILogger<LoggingEndpointFilter> logger)
{
_logger = logger;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
_logger.LogInformation("エンドポイント実行前: {Path}", context.HttpContext.Request.Path);
var result = await next(context);
_logger.LogInformation("エンドポイント実行後: {Path}", context.HttpContext.Request.Path);
return result;
}
}
// 特定のエンドポイントへの適用(AddEndpointFilter でチェーン)
app.MapGet("/products/{id}", (int id, IProductRepository repo) =>
repo.Find(id) is Product p ? TypedResults.Ok(p) : TypedResults.NotFound())
.AddEndpointFilter<LoggingEndpointFilter>();
// グループ単位での一括適用
var group = app.MapGroup("/api");
group.AddEndpointFilter<LoggingEndpointFilter>();
group.MapGet("/products/{id}", (int id, IProductRepository repo) =>
repo.Find(id) is Product p ? TypedResults.Ok(p) : TypedResults.NotFound());
group.MapPost("/products", (Product newProd, IProductRepository repo) =>
{
repo.Add(newProd);
return Results.Created($"/products/{newProd.Id}", newProd);
});

Minimal API は 高パフォーマンス を標榜しており、MVC に比べてわずかにオーバーヘッドが少ないです。
特に アプリ起動時間(cold start)メモリ消費 において、コントローラー駆動より Minimal API のほうが有利なケースがあります。

これは内部的にコントローラー探索や属性解析にリフレクション(実行時に型情報を動的に調べる仕組み)を使わないこと、パイプラインがシンプルであることによります。
特に .NET の AOT コンパイル(Ahead-of-Time) を用いるシナリオでは、Minimal API が選ばれます。

ASP.NET Core のすべての機能が Native AOT に対応しているわけではありません。
MVC コントローラーは、内部的にリフレクションを多用するため Native AOT に対応していません。
したがって、 Native AOT でパブリッシュする場合は Minimal API を使用する必要があります

.NET 8 以降では ASP.NET Core Web API (Native AOT) プロジェクトテンプレート(webapiaot)が用意されており、AOT 向けに最適化された WebApplication.CreateSlimBuilder() を使用した Minimal API プロジェクトをすぐに作成できます。

5. どのようなケースで使うかの判断基準

Section titled “5. どのようなケースで使うかの判断基準”

Minimal API はシンプルさと高速性を重視する代わりに従来 MVC が持っていた高度なテンプレート群のようなものは限られるため、プロジェクト構成から自身で定めることが MVC と大きく異なるポイントとなります。
以下に比較表を提示します。

観点Minimal APIMVC コントローラー
コード構造プロジェクトのエントリーポイントや拡張メソッドで、エンドポイントを関数単位で定義できます。整理された構造を保つために、アーキテクチャやディレクトリ構造を自身で決める必要がありますコントローラークラスとアクションメソッドにより階層的に構造化され定義されます
ルーティングMapGet("/route", handler) で明示的に定義します。属性(Route, HttpGet 等)や規約に基づきフレームワークが発見します(リフレクションを使用)。
ボイラープレート特に存在せず、定型コードが不要です。テンプレート/スキャフォールドによる雛形を活用できます。 [ApiController] public class XController : ControllerBase { ... } といった枠組みが必要です。
パフォーマンス軽量で起動・処理が高速であり、AOT コンパイル適合性が高いです。わずかにオーバーヘッドが増えます(多機能ゆえ)。ネイティブ AOT は非対応の場合があります。
主な用途マイクロサービス、サーバーレス API、シンプルな CRUD サービスに向いています。また、既存のフレームワークにとらわれず最新機能を使いたい場合にも活用できます。ビューを伴うアプリケーションに向いています。従来からの MVC 資産や高度な Web API 規約(OData 等)を必要とする場合にも選択されます。

この比較を踏まえ、 Minimal API を採用すべきケース従来の MVC コントローラーを採用すべきケース の判断についてまとめます。

flowchart TD
    Start(["新規 API を作成する"])
    Start --> Q1{"ビューを伴う Web アプリか?"}
    Q1 -->|はい| MVC1["MVC + コントローラー(Razor Views)を選択"]
    Q1 -->|いいえ| Q2{"高度なモデル検証・フィルター(ActionFilter 等)が必要か?"}
    Q2 -->|はい| MVC2["MVC コントローラー([ApiController])を選択"]
    Q2 -->|いいえ| Q3{"既存の MVC リソース(コード・ライブラリ)が多数あるか?"}
    Q3 -->|はい| MVC3["既存 MVC を踏襲または段階的に移行"]
    Q3 -->|いいえ| Minimal["Minimal API を選択(新規開発に推奨)"]

これから新規に作成するプロジェクトやマイクロサービス群、Python や TypeScript からのスキルトランスファーでは、MVC のような重厚な枠組みよりも軽量かつ柔軟性の高い Minimal API がマッチします。
サーバーレス(Azure Functions 等) との親和性も高く、無駄な処理を省いた Minimal API はコールドスタート時間短縮に寄与します。 一方で、サーバーサイドレンダリングの HTML が必要な場合や、既存資産(認証認可の仕組み、フィルター、一部モデルバインドや O/R マッパーとの連携など)を活かす場合は MVC コントローラーの採用が検討されます。