次の方法で共有


チュートリアル: ASP.NET Core を使って最小 API を作成する

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

作成者: Rick Anderson および Tom Dykstra

Minimal API は、依存関係が最小限の HTTP API を作成するために設計されています。 ASP.NET Core での最小限のファイル、機能、依存関係のみを含むマイクロサービスやアプリに最適です。

このチュートリアルでは、ASP.NET Core で最小 API を構築するための基本について説明します。 ASP.NET Core で API を作成するもう 1 つの方法は、コントローラーを使用することです。 最小 API とコントローラー ベースの API の選択に関するヘルプについては、API の概要に関する記事をご覧ください。 より多くの機能を含む、コントローラーに基づく API プロジェクトの作成に関するチュートリアルについては、Web API の作成に関する記事をご覧ください。

概要

このチュートリアルでは、次の API を作成します。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
POST /todoitems 新しいアイテムを追加します。 To Do アイテム To Do アイテム
PUT /todoitems/{id} 既存のアイテムを更新します。 To Do アイテム None
DELETE /todoitems/{id}     アイテムを削除します。 None なし

必須コンポーネント

API プロジェクトを作成する

  • Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。

  • [新しいプロジェクトの作成] ダイアログで次を行います。

    • [テンプレートの検索] ボックスに、「Empty」と入力します。
    • [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。

    Visual Studio の [新しいプロジェクトの作成]

  • プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。

  • [追加情報] ダイアログで、次を行います。

    • [.NET 9.0 (プレビュー)] を選択します
    • [最上位レベルのステートメントを使用しない] をオフにする
    • [作成]

    追加情報

コードを確認する

Program.cs ファイルには、次のコードが含まれています。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

上記のコードでは次の操作が行われます。

  • 事前に構成された既定値で WebApplicationBuilderWebApplication を作成します。
  • Hello World! を返す HTTP GET エンドポイント / を作成します。

アプリを実行する

Ctrl + F5 キーを押して、デバッガーなしで実行します。

Visual Studio に次のダイアログが表示されます。

このプロジェクトは SSL を使用するように構成されています。ブラウザーでの SSL の警告を避けるには、IIS Express が生成した自己署名証明書を信頼することを選択します。IIS Express の SSL 証明書を信頼しますか?

IIS Express SSL 証明書を信頼する場合、[はい] を選択します。

次のダイアログが表示されます。

セキュリティ警告のダイアログ

開発証明書を信頼することに同意する場合は、 [はい] を選択します。

Firefox ブラウザーを信頼する方法の詳細については、「Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 証明書エラー」を参照してください。

Visual Studio によって Kestrel Web サーバーが起動され、ブラウザー ウィンドウが開きます。

ブラウザーに Hello World! が表示されます。 Program.cs ファイルには、最小限の完成されたアプリが含まれています。

ブラウザー ウィンドウを閉じます。

NuGet パッケージを追加する

このチュートリアルで使用するデータベースと診断をサポートするには、NuGet パッケージを追加する必要があります。

  • [ツール] メニューで [NuGet パッケージ マネージャー] > [ソリューションの NuGet パッケージの管理] の順に選択します。
  • [参照] タブを選択します。
  • [プレリリースを含める] を選択します。
  • 検索ボックスに「Microsoft.EntityFrameworkCore.InMemory」と入力し、Microsoft.EntityFrameworkCore.InMemory を選択します。
  • 右側のウィンドウで [プロジェクト] チェックボックスをオンにして、 [インストール] を選択します。
  • 上記の手順にしたがって、Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore パッケージを追加します。

モデルおよびデータベース コンテキスト クラス

  • project フォルダーで、次のコードを含む Todo.cs という名前のファイルを作成します。
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

上記のコードでは、このアプリのモデルを作成します。 "モデル" は、アプリが管理するデータを表すクラスです。

  • 次のコードのファイルを、TodoDb.cs という名前で作成します。
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

上記のコードでは、データベース コンテキストが定義されています。これは、データ モデルの Entity Framework 機能を調整するメイン クラスです。 このクラスは Microsoft.EntityFrameworkCore.DbContext クラスから派生したクラスです。

API コードを追加する

  • Program.cs ファイルの内容を次のコードに置き換えます。
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

次の強調表示されたコードにより、データベース コンテキストが依存関係の挿入 (DI) コンテナーに追加され、データベース関連の例外が表示されるようになります。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

DI コンテナーは、データベース コンテキストやその他のサービスへのアクセスを提供します。

このチュートリアルでは、Endpoints Explorer と .http ファイルを使って API をテストします。

データの POST をテストする

次の Program.cs のコードにより、データをメモリ内データベースに追加する HTTP POST のエンドポイント /todoitems が作成されます。

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

アプリを実行します。 / エンドポイントが存在しなくなったため、ブラウザーに 404 エラーが表示されます。

POST エンドポイントは、アプリにデータを追加するために使われます。

  • [ビュー]>[その他のウィンドウ]>[Endpoints Explorer] (エンドポイント エクスプローラー) の順に選択します。

  • POST エンドポイントを右クリックし、[要求の生成] を選びます。

    Endpoints Explorer のコンテキスト メニュー。[要求の生成] メニュー項目が強調表示されています。

    次の例のような内容の TodoApi.http という新しいファイルがプロジェクト フォルダー内に作成されます。

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • 最初の行では、すべてのエンドポイントに使われる変数を作成します。
    • 次の行では、POST 要求を定義しています。
    • トリプル ハッシュタグ (###) 行は要求の区切り記号であり、この後に続くのは別の要求向けのものです。
  • POST 要求にはヘッダーと本文が必要です。 要求のこれらの部分を定義するには、POST 要求行の直後に次の行を追加します。

    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    

    先ほどのコードでは、Content-Type ヘッダーと JSON 要求の本文を追加しています。 TodoApi.http ファイルは次の例のようになりますが、実際のポート番号に置き換えてください。

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • アプリを実行します。

  • POST 要求行の上にある [要求の送信] リンクを選択します。

    実行リンクが強調表示された .http ファイル ウィンドウ。

    POST 要求がアプリに送信され、応答が [応答] ペインに表示されます。

    POST 要求の応答が表示されている .http ファイル ウィンドウ。

GET エンドポイントを検証する

サンプル アプリでは、MapGet の呼び出しにより、いくつかの GET エンドポイントを実装しています。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

GET エンドポイントをテストする

ブラウザーから GET エンドポイントを呼び出すか、Endpoints Explorer を使ってアプリをテストします。 以下の手順は Endpoints Explorer の場合です。

  • Endpoints Explorer で最初の GET エンドポイントを右クリックし、[要求の生成] を選びます。

    TodoApi.http ファイルに以下の内容が追加されます。

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • 新しい GET 要求行の上にある [要求の送信] リンクを選択します。

    GET 要求がアプリに送信され、応答が [応答] ペインに表示されます。

  • 応答本文は次の JSON のようになります。

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • エンドポイント エクスプローラー/todoitems/{id} GET エンドポイントを右クリックし、[要求の生成] を選択します。 TodoApi.http ファイルに以下の内容が追加されます。

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • {id}1 で置き換え

  • 新しい GET 要求行の上にある [要求の送信] リンクを選択します。

    GET 要求がアプリに送信され、応答が [応答] ペインに表示されます。

  • 応答本文は次の JSON のようになります。

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

このアプリではメモリ内データベースが使用されます。 アプリを再起動すると、GET 要求はデータを返しません。 データが返されない場合、データをアプリに POST して、GET 要求をもう一度試します。

戻り値

ASP.NET Core は自動的にオブジェクトを JSON にシリアル化して、応答メッセージの本文に JSON を書き込みます。 ハンドルされない例外がないと仮定すると、この戻り値の型の応答コードは 200 OK です。 ハンドルされない例外は 5xx エラーに変換されます。

戻り値の型は、さまざまな HTTP 状態コードを表すことができます。 たとえば、GET /todoitems/{id} は、次の 2 つの異なる状態値を返す可能性があります。

  • 要求された ID に一致するアイテムがない場合、このメソッドにより 404 ステータス NotFound エラー コードが返されます。
  • それ以外の場合、メソッドは JSON 応答本文で 200 を返します。 戻り値が item の場合、HTTP 200 応答が返されます。

PUT エンドポイントを検証する

サンプル アプリは、MapPut を使用して単一の PUT エンドポイントを実装しています。

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

このメソッドは、HTTP PUT を使用していることを除き、MapPost メソッドに似ています。 応答が成功すると、204 (コンテンツなし) が返されます。 HTTP 仕様に従って、PUT 要求では、変更だけでなく、更新されたエンティティ全体を送信するようクライアントに求めます。 部分的な更新をサポートするには、HTTP PATCH を使用します。

PUT エンドポイントをテストする

このサンプルでは、アプリを起動するたびに開始することが必要なメモリ内データベースが使われています。 PUT 呼び出しを実行する前に、データベース内にアイテムが存在している必要があります。 GET を呼び出して、PUT 呼び出しを実行する前にデータベース内にアイテムが確実に存在していることを確認します。

Id = 1 の To Do 項目を更新し、その名前を "feed fish" に設定します。

  • Endpoints ExplorerPUT エンドポイントを右クリックし、[要求の生成] を選びます。

    TodoApi.http ファイルに以下の内容が追加されます。

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • PUT 要求行の {id}1 に置き換えます。

  • PUT 要求行の直後に以下の行を追加します。

    Content-Type: application/json
    
    {
      "name": "feed fish",
      "isComplete": false
    }
    

    先ほどのコードでは、Content-Type ヘッダーと JSON 要求の本文を追加しています。

  • 新しい PUT 要求行の上にある [要求の送信] リンクを選びます。

    PUT 要求がアプリに送信され、応答が [応答] ペインに表示されます。 応答本文は空であり、状態コードは 204 です。

DELETE エンドポイントの検証とテスト

サンプル アプリは、MapDelete を使用して単一の DELETE エンドポイントを実装しています。

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
  • Endpoints ExplorerDELETE エンドポイントを右クリックし、[要求の生成] を選びます。

    DELETE 要求が TodoApi.http に追加されます。

  • DELETE 要求行の {id}1 に置き換えます。 DELETE 要求は次の例のようになります。

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • DELETE 要求の [要求の送信] リンクを選択します。

    DELETE 要求がアプリに送信され、応答が [応答] ペインに表示されます。 応答本文は空であり、状態コードは 204 です。

MapGroup API を使う

サンプル アプリ コードでは、エンドポイントを設定するたびに todoitems URL プレフィックスが繰り返されます。 API には共通の URL プレフィックスを持つエンドポイントのグループがあることが多く、MapGroup メソッドはそのようなグループの整理に役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorizationWithMetadata のようなメソッドを 1 回呼び出すだけで、エンドポイントのグループ全体をカスタマイズできます。

Program.cs の内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

上記のコードには、次の変更が加えられています。

  • var todoItems = app.MapGroup("/todoitems"); を追加して、URL プレフィックス /todoitems を使ってグループを設定します。
  • すべての app.Map<HttpVerb> メソッドを todoItems.Map<HttpVerb> に変更します。
  • Map<HttpVerb> メソッド呼び出しから URL プレフィックス /todoitems を削除します。

エンドポイントをテストして、同じように動作することを確認します。

TypedResults API を使う

Results ではなく TypedResults を返すと、テストのしやすさや、エンドポイントを記述するための OpenAPI の応答型メタデータが自動的に返されるなど、いくつかの利点があります。 詳しくは、「TypedResults と Results」をご覧ください。

Map<HttpVerb> メソッドは、ラムダ式を使う代わりにルート ハンドラー メソッドを呼び出すことができます。 例を確認するには、次のコードを使って Program.cs を更新します。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

これで Map<HttpVerb> のコードは、ラムダ式ではなくメソッドを呼び出すようになりました。

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

これらのメソッドは、IResult を実装し TypedResults で定義されるオブジェクトを返します。

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

単体テストはこれらのメソッドを呼び出して、正しい型を返すかどうかをテストできます。 たとえば、メソッドが GetAllTodos である場合:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

単体テスト コードは、ハンドラー メソッドから Ok<Todo[]> 型のオブジェクトが返されることを確認できます。 次に例を示します。

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

過剰な投稿を防止する

現在、サンプル アプリでは Todo オブジェクト全体が公開されています。 運用環境アプリケーション内の運用環境アプリでは、入力され、返されるデータを制限するためにモデルのサブセットがよく使われます。 その背景には複数の理由があり、セキュリティは主なものです。 モデルのサブセットは、通常、データ転送オブジェクト (DTO)、入力モデル、またはビュー モデルと呼ばれます。 この記事では DTO を使用しています。

DTO を使用すると、次のことができます。

  • 過剰な投稿を防止する。
  • クライアントが表示しないことになっているプロパティを非表示にする。
  • ペイロード サイズを減らすには、いくつかのプロパティを省略します。
  • 入れ子になったオブジェクトを含むオブジェクト グラフをフラット化する。 フラット化されたオブジェクト グラフは、クライアントにとってより便利になる可能性があります。

DTO のアプローチを実演するために、Todo クラスを更新して、シークレット フィールドを含めます。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

シークレット フィールドは、このアプリでは非表示にする必要がありますが、管理アプリの場合は公開することを選択できます。

シークレット フィールドを投稿および取得できることを確認します。

次のコードのファイルを、TodoItemDTO.cs という名前で作成します。

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

この DTO モデルを使うには、Program.cs ファイルの内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

シークレット フィールドを除くすべてのフィールドを投稿および取得できることを確認します。

完成したサンプルを使用したトラブルシューティング

解決できない問題が発生した場合は、コードを完成したプロジェクトと比較します。 完成したプロジェクトを表示またはダウンロードします (ダウンロード方法)。

次のステップ

詳細情報

Minimal API のクイック リファレンスを参照してください

Minimal API は、依存関係が最小限の HTTP API を作成するために設計されています。 ASP.NET Core での最小限のファイル、機能、依存関係のみを含むマイクロサービスやアプリに最適です。

このチュートリアルでは、ASP.NET Core で最小 API を構築するための基本について説明します。 ASP.NET Core で API を作成するもう 1 つの方法は、コントローラーを使用することです。 最小 API とコントローラー ベースの API の選択に関するヘルプについては、API の概要に関する記事をご覧ください。 より多くの機能を含む、コントローラーに基づく API プロジェクトの作成に関するチュートリアルについては、Web API の作成に関する記事をご覧ください。

概要

このチュートリアルでは、次の API を作成します。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
POST /todoitems 新しいアイテムを追加します。 To Do アイテム To Do アイテム
PUT /todoitems/{id} 既存のアイテムを更新します。 To Do アイテム None
DELETE /todoitems/{id}     アイテムを削除します。 None なし

必須コンポーネント

API プロジェクトを作成する

  • Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。

  • [新しいプロジェクトの作成] ダイアログで次を行います。

    • [テンプレートの検索] ボックスに、「Empty」と入力します。
    • [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。

    Visual Studio の [新しいプロジェクトの作成]

  • プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。

  • [追加情報] ダイアログで、次を行います。

    • [.NET 7.0] を選ぶ
    • [最上位レベルのステートメントを使用しない] をオフにする
    • [作成]

    追加情報

コードを確認する

Program.cs ファイルには、次のコードが含まれています。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

上記のコードでは次の操作が行われます。

  • 事前に構成された既定値で WebApplicationBuilderWebApplication を作成します。
  • Hello World! を返す HTTP GET エンドポイント / を作成します。

アプリを実行する

Ctrl + F5 キーを押して、デバッガーなしで実行します。

Visual Studio に次のダイアログが表示されます。

このプロジェクトは SSL を使用するように構成されています。ブラウザーでの SSL の警告を避けるには、IIS Express が生成した自己署名証明書を信頼することを選択します。IIS Express の SSL 証明書を信頼しますか?

IIS Express SSL 証明書を信頼する場合、[はい] を選択します。

次のダイアログが表示されます。

セキュリティ警告のダイアログ

開発証明書を信頼することに同意する場合は、 [はい] を選択します。

Firefox ブラウザーを信頼する方法の詳細については、「Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 証明書エラー」を参照してください。

Visual Studio によって Kestrel Web サーバーが起動され、ブラウザー ウィンドウが開きます。

ブラウザーに Hello World! が表示されます。 Program.cs ファイルには、最小限の完成されたアプリが含まれています。

NuGet パッケージを追加する

このチュートリアルで使用するデータベースと診断をサポートするには、NuGet パッケージを追加する必要があります。

  • [ツール] メニューで [NuGet パッケージ マネージャー] > [ソリューションの NuGet パッケージの管理] の順に選択します。
  • [参照] タブを選択します。
  • 検索ボックスに「Microsoft.EntityFrameworkCore.InMemory」と入力し、Microsoft.EntityFrameworkCore.InMemory を選択します。
  • 右ペインにある [プロジェクト] チェックボックスをオンにします。
  • [バージョン] ドロップダウンで、使用できる最新バージョン 7 (たとえば 7.0.17) を選び、[インストール] を選びます。
  • 上記の手順に従って、使用できる最新バージョン 7 を含む Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore パッケージを追加します。

モデルおよびデータベース コンテキスト クラス

project フォルダーで、次のコードを含む Todo.cs という名前のファイルを作成します。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

上記のコードでは、このアプリのモデルを作成します。 "モデル" は、アプリが管理するデータを表すクラスです。

次のコードのファイルを、TodoDb.cs という名前で作成します。

using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

上記のコードでは、データベース コンテキストが定義されています。これは、データ モデルの Entity Framework 機能を調整するメイン クラスです。 このクラスは Microsoft.EntityFrameworkCore.DbContext クラスから派生したクラスです。

API コードを追加する

Program.cs ファイルの内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

次の強調表示されたコードにより、データベース コンテキストが依存関係の挿入 (DI) コンテナーに追加され、データベース関連の例外が表示されるようになります。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

DI コンテナーは、データベース コンテキストやその他のサービスへのアクセスを提供します。

Swagger を使って API テスト UI を作成する

選択できる Web API テスト ツールは多数あり、任意のツールを使ってこのチュートリアルの入門 API テスト手順を実行できます。

このチュートリアルでは、.NET パッケージ NSwag.AspNetCore を利用します。これには、OpenAPI 仕様に準拠したテスト UI を生成するための Swagger ツールが統合されています。

  • NSwag: Swagger を ASP.NET Core アプリケーションに直接統合し、ミドルウェアと構成を提供する .NET ライブラリ。
  • Swagger: OpenAPI 仕様に準拠した API テスト ページを生成する OpenAPIGenerator や SwaggerUI などのオープンソース ツールのセット。
  • OpenAPI 仕様: コントローラーとモデル内の XML と属性の注釈に基づいて、API の機能を説明するドキュメント。

ASP.NET で OpenAPI と NSwag を使う方法の詳細については、「Swagger/OpenAPI を使用する ASP.NET Core Web API のドキュメント」を参照してください。

Swagger ツールをインストールする

  • 次のコマンドを実行します。

    dotnet add package NSwag.AspNetCore
    

前のコマンドを実行すると、Swagger ドキュメントと UI を生成するツールを含む NSwag.AspNetCore パッケージが追加されます。

Swagger ミドルウェアを構成する

  • var app = builder.Build();app が定義される前に、次の強調表示されたコードを追加します

    using Microsoft.EntityFrameworkCore;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
    builder.Services.AddDatabaseDeveloperPageExceptionFilter();
    
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddOpenApiDocument(config =>
    {
        config.DocumentName = "TodoAPI";
        config.Title = "TodoAPI v1";
        config.Version = "v1";
    });
    var app = builder.Build();
    

上のコードでは、次のようになります。

  • builder.Services.AddEndpointsApiExplorer();: API Explorer を有効にします。これは、HTTP API に関するメタデータを提供するサービスです。 API Explorer は、Swagger ドキュメントを生成するために Swagger によって使われます。

  • builder.Services.AddOpenApiDocument(config => {...});: Swagger OpenAPI ドキュメント ジェネレーターをアプリケーション サービスに追加し、タイトルやバージョンなど、API に関する詳細情報を提供するように構成します。 より堅牢な API の詳細については、「NSwag と ASP.NET Core の概要」を参照してください

  • var app = builder.Build();app が定義された後の次の行に、次の強調表示されたコードを追加します

    var app = builder.Build();
    if (app.Environment.IsDevelopment())
    {
        app.UseOpenApi();
        app.UseSwaggerUi(config =>
        {
            config.DocumentTitle = "TodoAPI";
            config.Path = "/swagger";
            config.DocumentPath = "/swagger/{documentName}/swagger.json";
            config.DocExpansion = "list";
        });
    }
    

    前のコードを使うと、生成された JSON ドキュメントと Swagger UI を Swagger ミドルウェアで提供できるようになります。 Swagger は開発環境でのみ有効です。 運用環境で Swagger を有効にすると、API の構造と実装に関する機密情報が漏えいする可能性があります。

データの POST をテストする

次の Program.cs のコードにより、データをメモリ内データベースに追加する HTTP POST のエンドポイント /todoitems が作成されます。

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

アプリを実行します。 / エンドポイントが存在しなくなったため、ブラウザーに 404 エラーが表示されます。

POST エンドポイントは、アプリにデータを追加するために使われます。

  • アプリがまだ実行されている状態で、ブラウザーで https://localhost:<port>/swagger に移動し、Swagger によって生成された API テスト ページを表示します。

    Swagger で生成された API テスト ページ

  • Swagger API テスト ページで、[Post /todoitems]>[Try it out] を選びます。

  • [Request body] フィールドには、API のパラメーターを反映した生成結果の形式例が表示されることに注意してください。

  • 要求本文に、「オプションの id を指定せずに、To Do アイテムの JSON」と入力します。

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • [実行] を選択します。

    Swagger と Post

Swagger には、[Execute] ボタンの下に [Responses] ペインがあります。

Swagger と Post 応答

役に立つ詳細事項をいくつか紹介します。

  • cURL: Swagger には、Unix/Linux 構文での cURL コマンド例が用意されています。これは、Git for Windows の Git Bash など、Unix/Linux 構文を使う任意の bash シェルを使ってコマンド ラインで実行できます。
  • 要求 URL: API 呼び出しに対して Swagger UI の JavaScript コードによって作成される HTTP 要求の簡略化された表現。 実際の要求には、ヘッダー、クエリ パラメーター、要求本文などの詳細が含まれる場合があります。
  • サーバーの応答: 応答の本文とヘッダーが含まれています。 応答本文には、id1 に設定されたことが示されています。
  • 応答コード: 201 HTTP 状態コードが返され、要求が正常に処理され、新しいリソースが作成されたことを示します。

GET エンドポイントを検証する

サンプル アプリでは、MapGet の呼び出しにより、いくつかの GET エンドポイントを実装しています。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

GET エンドポイントをテストする

ブラウザーまたは Swagger からエンドポイントを呼び出してアプリをテストします。

  • Swagger で [GET /todoitems]>[Try it out]>[Execute] を選びます。

  • または、ブラウザーで URI http://localhost:<port>/todoitems を入力して GET /todoitems を呼び出します。 たとえば、http://localhost:5001/todoitems のように指定します。

GET /todoitems への呼び出しにより、次のような応答が生成されます。

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Swagger で GET /todoitems/{id} を呼び出して、特定の ID からデータを返します。

    • [GET /todoitems]>[Try it out] を選びます。
    • id フィールドを 1 に設定し、[Execute] を選びます。
  • または、ブラウザーで URI https://localhost:<port>/todoitems/1 を入力して GET /todoitems を呼び出します。 たとえば、https://localhost:5001/todoitems/1 のように指定します。

  • 応答本文は次のようになります。

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

このアプリではメモリ内データベースが使用されます。 アプリを再起動すると、GET 要求はデータを返しません。 データが返されない場合、データをアプリに POST して、GET 要求をもう一度試します。

戻り値

ASP.NET Core は自動的にオブジェクトを JSON にシリアル化して、応答メッセージの本文に JSON を書き込みます。 ハンドルされない例外がないと仮定すると、この戻り値の型の応答コードは 200 OK です。 ハンドルされない例外は 5xx エラーに変換されます。

戻り値の型は、さまざまな HTTP 状態コードを表すことができます。 たとえば、GET /todoitems/{id} は、次の 2 つの異なる状態値を返す可能性があります。

  • 要求された ID に一致するアイテムがない場合、このメソッドにより 404 ステータス NotFound エラー コードが返されます。
  • それ以外の場合、メソッドは JSON 応答本文で 200 を返します。 戻り値が item の場合、HTTP 200 応答が返されます。

PUT エンドポイントを検証する

サンプル アプリは、MapPut を使用して単一の PUT エンドポイントを実装しています。

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

このメソッドは、HTTP PUT を使用していることを除き、MapPost メソッドに似ています。 応答が成功すると、204 (コンテンツなし) が返されます。 HTTP 仕様に従って、PUT 要求では、変更だけでなく、更新されたエンティティ全体を送信するようクライアントに求めます。 部分的な更新をサポートするには、HTTP PATCH を使用します。

PUT エンドポイントをテストする

このサンプルでは、アプリを起動するたびに開始することが必要なメモリ内データベースが使われています。 PUT 呼び出しを実行する前に、データベース内にアイテムが存在している必要があります。 GET を呼び出して、PUT 呼び出しを実行する前にデータベース内にアイテムが確実に存在していることを確認します。

Id = 1 の To Do 項目を更新し、その名前を "feed fish" に設定します。

Swagger を使って PUT 要求を送信します。

  • [Put /todoitems/{id}]>[Try it out] を選びます。

  • id フィールドを 1 に設定します。

  • 要求本文を次の JSON に設定します。

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • [実行] を選択します。

DELETE エンドポイントの検証とテスト

サンプル アプリは、MapDelete を使用して単一の DELETE エンドポイントを実装しています。

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

Swagger を使って DELETE 要求を送信します。

  • [DELETE /todoitems/{id}]>[Try it out] を選びます。

  • ID フィールドを 1 に設定し、[Execute] を選びます。

    DELETE 要求がアプリに送信され、応答が [Responses] ペインに表示されます。 応答本文は空であり、[Server response] の状態コードは 204 です。

MapGroup API を使う

サンプル アプリ コードでは、エンドポイントを設定するたびに todoitems URL プレフィックスが繰り返されます。 API には共通の URL プレフィックスを持つエンドポイントのグループがあることが多く、MapGroup メソッドはそのようなグループの整理に役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorizationWithMetadata のようなメソッドを 1 回呼び出すだけで、エンドポイントのグループ全体をカスタマイズできます。

Program.cs の内容を次のコードに置き換えます。

using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

上記のコードには、次の変更が加えられています。

  • var todoItems = app.MapGroup("/todoitems"); を追加して、URL プレフィックス /todoitems を使ってグループを設定します。
  • すべての app.Map<HttpVerb> メソッドを todoItems.Map<HttpVerb> に変更します。
  • Map<HttpVerb> メソッド呼び出しから URL プレフィックス /todoitems を削除します。

エンドポイントをテストして、同じように動作することを確認します。

TypedResults API を使う

Results ではなく TypedResults を返すと、テストのしやすさや、エンドポイントを記述するための OpenAPI の応答型メタデータが自動的に返されるなど、いくつかの利点があります。 詳しくは、「TypedResults と Results」をご覧ください。

Map<HttpVerb> メソッドは、ラムダ式を使う代わりにルート ハンドラー メソッドを呼び出すことができます。 例を確認するには、次のコードを使って Program.cs を更新します。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

これで Map<HttpVerb> のコードは、ラムダ式ではなくメソッドを呼び出すようになりました。

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

これらのメソッドは、IResult を実装し TypedResults で定義されるオブジェクトを返します。

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

単体テストはこれらのメソッドを呼び出して、正しい型を返すかどうかをテストできます。 たとえば、メソッドが GetAllTodos である場合:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

単体テスト コードは、ハンドラー メソッドから Ok<Todo[]> 型のオブジェクトが返されることを確認できます。 次に例を示します。

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

過剰な投稿を防止する

現在、サンプル アプリでは Todo オブジェクト全体が公開されています。 運用環境アプリケーション内の運用環境アプリでは、入力され、返されるデータを制限するためにモデルのサブセットがよく使われます。 その背景には複数の理由があり、セキュリティは主なものです。 モデルのサブセットは、通常、データ転送オブジェクト (DTO)、入力モデル、またはビュー モデルと呼ばれます。 この記事では DTO を使用しています。

DTO を使用すると、次のことができます。

  • 過剰な投稿を防止する。
  • クライアントが表示しないことになっているプロパティを非表示にする。
  • ペイロード サイズを減らすには、いくつかのプロパティを省略します。
  • 入れ子になったオブジェクトを含むオブジェクト グラフをフラット化する。 フラット化されたオブジェクト グラフは、クライアントにとってより便利になる可能性があります。

DTO のアプローチを実演するために、Todo クラスを更新して、シークレット フィールドを含めます。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

シークレット フィールドは、このアプリでは非表示にする必要がありますが、管理アプリの場合は公開することを選択できます。

シークレット フィールドを投稿および取得できることを確認します。

次のコードのファイルを、TodoItemDTO.cs という名前で作成します。

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

この DTO モデルを使うには、Program.cs ファイルの内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}


class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

シークレット フィールドを除くすべてのフィールドを投稿および取得できることを確認します。

完成したサンプルを使用したトラブルシューティング

解決できない問題が発生した場合は、コードを完成したプロジェクトと比較します。 完成したプロジェクトを表示またはダウンロードします (ダウンロード方法)。

次のステップ

詳細情報

Minimal API のクイック リファレンスを参照してください

Minimal API は、依存関係が最小限の HTTP API を作成するために設計されています。 ASP.NET Core での最小限のファイル、機能、依存関係のみを含むマイクロサービスやアプリに最適です。

このチュートリアルでは、ASP.NET Core で最小 API を構築するための基本について説明します。 ASP.NET Core で API を作成するもう 1 つの方法は、コントローラーを使用することです。 最小 API とコントローラー ベースの API の選択に関するヘルプについては、API の概要に関する記事をご覧ください。 より多くの機能を含む、コントローラーに基づく API プロジェクトの作成に関するチュートリアルについては、Web API の作成に関する記事をご覧ください。

概要

このチュートリアルでは、次の API を作成します。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
POST /todoitems 新しいアイテムを追加します。 To Do アイテム To Do アイテム
PUT /todoitems/{id} 既存のアイテムを更新します。 To Do アイテム None
DELETE /todoitems/{id}     アイテムを削除します。 None なし

必須コンポーネント

API プロジェクトを作成する

  • Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。

  • [新しいプロジェクトの作成] ダイアログで次を行います。

    • [テンプレートの検索] ボックスに、「Empty」と入力します。
    • [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。

    Visual Studio の [新しいプロジェクトの作成]

  • プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。

  • [追加情報] ダイアログで、次を行います。

    • [.NET 6.0] を選びます
    • [最上位レベルのステートメントを使用しない] をオフにする
    • [作成]

コードを確認する

Program.cs ファイルには、次のコードが含まれています。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

上記のコードでは次の操作が行われます。

  • 事前に構成された既定値で WebApplicationBuilderWebApplication を作成します。
  • Hello World! を返す HTTP GET エンドポイント / を作成します。

アプリを実行する

Ctrl + F5 キーを押して、デバッガーなしで実行します。

Visual Studio に次のダイアログが表示されます。

このプロジェクトは SSL を使用するように構成されています。ブラウザーでの SSL の警告を避けるには、IIS Express が生成した自己署名証明書を信頼することを選択します。IIS Express の SSL 証明書を信頼しますか?

IIS Express SSL 証明書を信頼する場合、[はい] を選択します。

次のダイアログが表示されます。

セキュリティ警告のダイアログ

開発証明書を信頼することに同意する場合は、 [はい] を選択します。

Firefox ブラウザーを信頼する方法の詳細については、「Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 証明書エラー」を参照してください。

Visual Studio によって Kestrel Web サーバーが起動され、ブラウザー ウィンドウが開きます。

ブラウザーに Hello World! が表示されます。 Program.cs ファイルには、最小限の完成されたアプリが含まれています。

NuGet パッケージを追加する

このチュートリアルで使用するデータベースと診断をサポートするには、NuGet パッケージを追加する必要があります。

  • [ツール] メニューで [NuGet パッケージ マネージャー] > [ソリューションの NuGet パッケージの管理] の順に選択します。
  • [参照] タブを選択します。
  • 検索ボックスに「Microsoft.EntityFrameworkCore.InMemory」と入力し、Microsoft.EntityFrameworkCore.InMemory を選択します。
  • 右ペインにある [プロジェクト] チェックボックスをオンにします。
  • [バージョン] ドロップダウンで、使用できる最新バージョン 7 (たとえば 6.0.28) を選び、[インストール] を選びます。
  • 上記の手順に従って、使用できる最新バージョン 7 を含む Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore パッケージを追加します。

モデルおよびデータベース コンテキスト クラス

project フォルダーで、次のコードを含む Todo.cs という名前のファイルを作成します。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

上記のコードでは、このアプリのモデルを作成します。 "モデル" は、アプリが管理するデータを表すクラスです。

次のコードのファイルを、TodoDb.cs という名前で作成します。

using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

上記のコードでは、データベース コンテキストが定義されています。これは、データ モデルの Entity Framework 機能を調整するメイン クラスです。 このクラスは Microsoft.EntityFrameworkCore.DbContext クラスから派生したクラスです。

API コードを追加する

Program.cs ファイルの内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

次の強調表示されたコードにより、データベース コンテキストが依存関係の挿入 (DI) コンテナーに追加され、データベース関連の例外が表示されるようになります。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

DI コンテナーは、データベース コンテキストやその他のサービスへのアクセスを提供します。

Swagger を使って API テスト UI を作成する

選択できる Web API テスト ツールは多数あり、任意のツールを使ってこのチュートリアルの入門 API テスト手順を実行できます。

このチュートリアルでは、.NET パッケージ NSwag.AspNetCore を利用します。これには、OpenAPI 仕様に準拠したテスト UI を生成するための Swagger ツールが統合されています。

  • NSwag: Swagger を ASP.NET Core アプリケーションに直接統合し、ミドルウェアと構成を提供する .NET ライブラリ。
  • Swagger: OpenAPI 仕様に準拠した API テスト ページを生成する OpenAPIGenerator や SwaggerUI などのオープンソース ツールのセット。
  • OpenAPI 仕様: コントローラーとモデル内の XML と属性の注釈に基づいて、API の機能を説明するドキュメント。

ASP.NET で OpenAPI と NSwag を使う方法の詳細については、「Swagger/OpenAPI を使用する ASP.NET Core Web API のドキュメント」を参照してください。

Swagger ツールをインストールする

  • 次のコマンドを実行します。

    dotnet add package NSwag.AspNetCore
    

前のコマンドを実行すると、Swagger ドキュメントと UI を生成するツールを含む NSwag.AspNetCore パッケージが追加されます。

Swagger ミドルウェアを構成する

  • Program.cs の先頭に次の using ステートメントを追加します。

    using NSwag.AspNetCore;
    
  • var app = builder.Build();app が定義される前に、次の強調表示されたコードを追加します

    using NSwag.AspNetCore;
    using Microsoft.EntityFrameworkCore;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
    builder.Services.AddDatabaseDeveloperPageExceptionFilter();
    
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddOpenApiDocument(config =>
    {
        config.DocumentName = "TodoAPI";
        config.Title = "TodoAPI v1";
        config.Version = "v1";
    });
    
    var app = builder.Build();
    

上のコードでは、次のようになります。

  • builder.Services.AddEndpointsApiExplorer();: API Explorer を有効にします。これは、HTTP API に関するメタデータを提供するサービスです。 API Explorer は、Swagger ドキュメントを生成するために Swagger によって使われます。

  • builder.Services.AddOpenApiDocument(config => {...});: Swagger OpenAPI ドキュメント ジェネレーターをアプリケーション サービスに追加し、タイトルやバージョンなど、API に関する詳細情報を提供するように構成します。 より堅牢な API の詳細については、「NSwag と ASP.NET Core の概要」を参照してください

  • var app = builder.Build();app が定義された後の次の行に、次の強調表示されたコードを追加します

    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        app.UseOpenApi();
        app.UseSwaggerUi(config =>
        {
            config.DocumentTitle = "TodoAPI";
            config.Path = "/swagger";
            config.DocumentPath = "/swagger/{documentName}/swagger.json";
            config.DocExpansion = "list";
        });
    }
    
    

    前のコードを使うと、生成された JSON ドキュメントと Swagger UI を Swagger ミドルウェアで提供できるようになります。 Swagger は開発環境でのみ有効です。 運用環境で Swagger を有効にすると、API の構造と実装に関する機密情報が漏えいする可能性があります。

データの POST をテストする

次の Program.cs のコードにより、データをメモリ内データベースに追加する HTTP POST のエンドポイント /todoitems が作成されます。

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

アプリを実行します。 / エンドポイントが存在しなくなったため、ブラウザーに 404 エラーが表示されます。

POST エンドポイントは、アプリにデータを追加するために使われます。

  • アプリがまだ実行されている状態で、ブラウザーで https://localhost:<port>/swagger に移動し、Swagger によって生成された API テスト ページを表示します。

    Swagger で生成された API テスト ページ

  • Swagger API テスト ページで、[Post /todoitems]>[Try it out] を選びます。

  • [Request body] フィールドには、API のパラメーターを反映した生成結果の形式例が表示されることに注意してください。

  • 要求本文に、「オプションの id を指定せずに、To Do アイテムの JSON」と入力します。

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • [実行] を選択します。

    Swagger と Post データ

Swagger には、[Execute] ボタンの下に [Responses] ペインがあります。

Swagger と Post 応答ペイン

役に立つ詳細事項をいくつか紹介します。

  • cURL: Swagger には、Unix/Linux 構文での cURL コマンド例が用意されています。これは、Git for Windows の Git Bash など、Unix/Linux 構文を使う任意の bash シェルを使ってコマンド ラインで実行できます。
  • 要求 URL: API 呼び出しに対して Swagger UI の JavaScript コードによって作成される HTTP 要求の簡略化された表現。 実際の要求には、ヘッダー、クエリ パラメーター、要求本文などの詳細が含まれる場合があります。
  • サーバーの応答: 応答の本文とヘッダーが含まれています。 応答本文には、id1 に設定されたことが示されています。
  • 応答コード: 201 HTTP 状態コードが返され、要求が正常に処理され、新しいリソースが作成されたことを示します。

GET エンドポイントを検証する

サンプル アプリでは、MapGet の呼び出しにより、いくつかの GET エンドポイントを実装しています。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
app.MapGet("/", () => "Hello World!");

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

GET エンドポイントをテストする

ブラウザーまたは Swagger からエンドポイントを呼び出してアプリをテストします。

  • Swagger で [GET /todoitems]>[Try it out]>[Execute] を選びます。

  • または、ブラウザーで URI http://localhost:<port>/todoitems を入力して GET /todoitems を呼び出します。 たとえば、http://localhost:5001/todoitems のように指定します。

GET /todoitems への呼び出しにより、次のような応答が生成されます。

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Swagger で GET /todoitems/{id} を呼び出して、特定の ID からデータを返します。

    • [GET /todoitems]>[Try it out] を選びます。
    • id フィールドを 1 に設定し、[Execute] を選びます。
  • または、ブラウザーで URI https://localhost:<port>/todoitems/1 を入力して GET /todoitems を呼び出します。 例: https://localhost:5001/todoitems/1

  • 応答本文は次のようになります。

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

このアプリではメモリ内データベースが使用されます。 アプリを再起動すると、GET 要求はデータを返しません。 データが返されない場合、データをアプリに POST して、GET 要求をもう一度試します。

戻り値

ASP.NET Core は自動的にオブジェクトを JSON にシリアル化して、応答メッセージの本文に JSON を書き込みます。 ハンドルされない例外がないと仮定すると、この戻り値の型の応答コードは 200 OK です。 ハンドルされない例外は 5xx エラーに変換されます。

戻り値の型は、さまざまな HTTP 状態コードを表すことができます。 たとえば、GET /todoitems/{id} は、次の 2 つの異なる状態値を返す可能性があります。

  • 要求された ID に一致するアイテムがない場合、このメソッドにより 404 ステータス NotFound エラー コードが返されます。
  • それ以外の場合、メソッドは JSON 応答本文で 200 を返します。 戻り値が item の場合、HTTP 200 応答が返されます。

PUT エンドポイントを検証する

サンプル アプリは、MapPut を使用して単一の PUT エンドポイントを実装しています。

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

このメソッドは、HTTP PUT を使用していることを除き、MapPost メソッドに似ています。 応答が成功すると、204 (コンテンツなし) が返されます。 HTTP 仕様に従って、PUT 要求では、変更だけでなく、更新されたエンティティ全体を送信するようクライアントに求めます。 部分的な更新をサポートするには、HTTP PATCH を使用します。

PUT エンドポイントをテストする

このサンプルでは、アプリを起動するたびに開始することが必要なメモリ内データベースが使われています。 PUT 呼び出しを実行する前に、データベース内にアイテムが存在している必要があります。 GET を呼び出して、PUT 呼び出しを実行する前にデータベース内にアイテムが確実に存在していることを確認します。

Id = 1 の To Do 項目を更新し、その名前を "feed fish" に設定します。

Swagger を使って PUT 要求を送信します。

  • [Put /todoitems/{id}]>[Try it out] を選びます。

  • id フィールドを 1 に設定します。

  • 要求本文を次の JSON に設定します。

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • [実行] を選択します。

DELETE エンドポイントの検証とテスト

サンプル アプリは、MapDelete を使用して単一の DELETE エンドポイントを実装しています。

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

Swagger を使って DELETE 要求を送信します。

  • [DELETE /todoitems/{id}]>[Try it out] を選びます。

  • ID フィールドを 1 に設定し、[Execute] を選びます。

    DELETE 要求がアプリに送信され、応答が [Responses] ペインに表示されます。 応答本文は空であり、[Server response] の状態コードは 204 です。

過剰な投稿を防止する

現在、サンプル アプリでは Todo オブジェクト全体が公開されています。 運用環境アプリケーション内の運用環境アプリでは、入力され、返されるデータを制限するためにモデルのサブセットがよく使われます。 その背景には複数の理由があり、セキュリティは主なものです。 モデルのサブセットは、通常、データ転送オブジェクト (DTO)、入力モデル、またはビュー モデルと呼ばれます。 この記事では DTO を使用しています。

DTO を使用すると、次のことができます。

  • 過剰な投稿を防止する。
  • クライアントが表示しないことになっているプロパティを非表示にする。
  • ペイロード サイズを減らすには、いくつかのプロパティを省略します。
  • 入れ子になったオブジェクトを含むオブジェクト グラフをフラット化する。 フラット化されたオブジェクト グラフは、クライアントにとってより便利になる可能性があります。

DTO のアプローチを実演するために、Todo クラスを更新して、シークレット フィールドを含めます。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

シークレット フィールドは、このアプリでは非表示にする必要がありますが、管理アプリの場合は公開することを選択できます。

シークレット フィールドを投稿および取得できることを確認します。

次のコードのファイルを、TodoItemDTO.cs という名前で作成します。

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

この DTO モデルを使うには、Program.cs ファイルの内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}


class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

シークレット フィールドを除くすべてのフィールドを投稿および取得できることを確認します。

Minimal API をテストする

Minimal API アプリのテストの例については、この GitHub サンプルを参照してください。

Azure に発行する

Azure へのデプロイについては、「クイックスタート: ASP.NET Web アプリをデプロイする」を参照してください。

その他の技術情報

Minimal API は、依存関係が最小限の HTTP API を作成するために設計されています。 ASP.NET Core での最小限のファイル、機能、依存関係のみを含むマイクロサービスやアプリに最適です。

このチュートリアルでは、ASP.NET Core で最小 API を構築するための基本について説明します。 ASP.NET Core で API を作成するもう 1 つの方法は、コントローラーを使用することです。 最小 API とコントローラー ベースの API の選択に関するヘルプについては、API の概要に関する記事をご覧ください。 より多くの機能を含む、コントローラーに基づく API プロジェクトの作成に関するチュートリアルについては、Web API の作成に関する記事をご覧ください。

概要

このチュートリアルでは、次の API を作成します。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
POST /todoitems 新しいアイテムを追加します。 To Do アイテム To Do アイテム
PUT /todoitems/{id} 既存のアイテムを更新します。 To Do アイテム None
DELETE /todoitems/{id}     アイテムを削除します。 None なし

必須コンポーネント

API プロジェクトを作成する

  • Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。

  • [新しいプロジェクトの作成] ダイアログで次を行います。

    • [テンプレートの検索] ボックスに、「Empty」と入力します。
    • [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。

    Visual Studio の [新しいプロジェクトの作成]

  • プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。

  • [追加情報] ダイアログで、次を行います。

    • [.NET 8.0 (長期的なサポート)] を選択します
    • [最上位レベルのステートメントを使用しない] をオフにする
    • [作成]

    追加情報

コードを確認する

Program.cs ファイルには、次のコードが含まれています。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

上記のコードでは次の操作が行われます。

  • 事前に構成された既定値で WebApplicationBuilderWebApplication を作成します。
  • Hello World! を返す HTTP GET エンドポイント / を作成します。

アプリを実行する

Ctrl + F5 キーを押して、デバッガーなしで実行します。

Visual Studio に次のダイアログが表示されます。

このプロジェクトは SSL を使用するように構成されています。ブラウザーでの SSL の警告を避けるには、IIS Express が生成した自己署名証明書を信頼することを選択します。IIS Express の SSL 証明書を信頼しますか?

IIS Express SSL 証明書を信頼する場合、[はい] を選択します。

次のダイアログが表示されます。

セキュリティ警告のダイアログ

開発証明書を信頼することに同意する場合は、 [はい] を選択します。

Firefox ブラウザーを信頼する方法の詳細については、「Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 証明書エラー」を参照してください。

Visual Studio によって Kestrel Web サーバーが起動され、ブラウザー ウィンドウが開きます。

ブラウザーに Hello World! が表示されます。 Program.cs ファイルには、最小限の完成されたアプリが含まれています。

ブラウザー ウィンドウを閉じます。

NuGet パッケージを追加する

このチュートリアルで使用するデータベースと診断をサポートするには、NuGet パッケージを追加する必要があります。

  • [ツール] メニューで [NuGet パッケージ マネージャー] > [ソリューションの NuGet パッケージの管理] の順に選択します。
  • [参照] タブを選択します。
  • 検索ボックスに「Microsoft.EntityFrameworkCore.InMemory」と入力し、Microsoft.EntityFrameworkCore.InMemory を選択します。
  • 右側のウィンドウで [プロジェクト] チェックボックスをオンにして、 [インストール] を選択します。
  • 上記の手順にしたがって、Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore パッケージを追加します。

モデルおよびデータベース コンテキスト クラス

  • project フォルダーで、次のコードを含む Todo.cs という名前のファイルを作成します。
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

上記のコードでは、このアプリのモデルを作成します。 "モデル" は、アプリが管理するデータを表すクラスです。

  • 次のコードのファイルを、TodoDb.cs という名前で作成します。
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

上記のコードでは、データベース コンテキストが定義されています。これは、データ モデルの Entity Framework 機能を調整するメイン クラスです。 このクラスは Microsoft.EntityFrameworkCore.DbContext クラスから派生したクラスです。

API コードを追加する

  • Program.cs ファイルの内容を次のコードに置き換えます。
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

次の強調表示されたコードにより、データベース コンテキストが依存関係の挿入 (DI) コンテナーに追加され、データベース関連の例外が表示されるようになります。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

DI コンテナーは、データベース コンテキストやその他のサービスへのアクセスを提供します。

このチュートリアルでは、Endpoints Explorer と .http ファイルを使って API をテストします。

データの POST をテストする

次の Program.cs のコードにより、データをメモリ内データベースに追加する HTTP POST のエンドポイント /todoitems が作成されます。

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

アプリを実行します。 / エンドポイントが存在しなくなったため、ブラウザーに 404 エラーが表示されます。

POST エンドポイントは、アプリにデータを追加するために使われます。

  • [ビュー]>[その他のウィンドウ]>[Endpoints Explorer] (エンドポイント エクスプローラー) の順に選択します。

  • POST エンドポイントを右クリックし、[要求の生成] を選びます。

    Endpoints Explorer のコンテキスト メニュー。[要求の生成] メニュー項目が強調表示されています。

    次の例のような内容の TodoApi.http という新しいファイルがプロジェクト フォルダー内に作成されます。

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • 最初の行では、すべてのエンドポイントに使われる変数を作成します。
    • 次の行では、POST 要求を定義しています。
    • トリプル ハッシュタグ (###) 行は要求の区切り記号であり、この後に続くのは別の要求向けのものです。
  • POST 要求にはヘッダーと本文が必要です。 要求のこれらの部分を定義するには、POST 要求行の直後に次の行を追加します。

    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    

    先ほどのコードでは、Content-Type ヘッダーと JSON 要求の本文を追加しています。 TodoApi.http ファイルは次の例のようになりますが、実際のポート番号に置き換えてください。

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • アプリを実行します。

  • POST 要求行の上にある [要求の送信] リンクを選択します。

    実行リンクが強調表示された .http ファイル ウィンドウ。

    POST 要求がアプリに送信され、応答が [応答] ペインに表示されます。

    POST 要求の応答が表示されている .http ファイル ウィンドウ。

GET エンドポイントを検証する

サンプル アプリでは、MapGet の呼び出しにより、いくつかの GET エンドポイントを実装しています。

API 説明 要求本文 応答本文
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To Do アイテム
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

GET エンドポイントをテストする

ブラウザーから GET エンドポイントを呼び出すか、Endpoints Explorer を使ってアプリをテストします。 以下の手順は Endpoints Explorer の場合です。

  • Endpoints Explorer で最初の GET エンドポイントを右クリックし、[要求の生成] を選びます。

    TodoApi.http ファイルに以下の内容が追加されます。

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • 新しい GET 要求行の上にある [要求の送信] リンクを選択します。

    GET 要求がアプリに送信され、応答が [応答] ペインに表示されます。

  • 応答本文は次の JSON のようになります。

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • エンドポイント エクスプローラー/todoitems/{id} GET エンドポイントを右クリックし、[要求の生成] を選択します。 TodoApi.http ファイルに以下の内容が追加されます。

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • {id}1 で置き換え

  • 新しい GET 要求行の上にある [要求の送信] リンクを選択します。

    GET 要求がアプリに送信され、応答が [応答] ペインに表示されます。

  • 応答本文は次の JSON のようになります。

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

このアプリではメモリ内データベースが使用されます。 アプリを再起動すると、GET 要求はデータを返しません。 データが返されない場合、データをアプリに POST して、GET 要求をもう一度試します。

戻り値

ASP.NET Core は自動的にオブジェクトを JSON にシリアル化して、応答メッセージの本文に JSON を書き込みます。 ハンドルされない例外がないと仮定すると、この戻り値の型の応答コードは 200 OK です。 ハンドルされない例外は 5xx エラーに変換されます。

戻り値の型は、さまざまな HTTP 状態コードを表すことができます。 たとえば、GET /todoitems/{id} は、次の 2 つの異なる状態値を返す可能性があります。

  • 要求された ID に一致するアイテムがない場合、このメソッドにより 404 ステータス NotFound エラー コードが返されます。
  • それ以外の場合、メソッドは JSON 応答本文で 200 を返します。 戻り値が item の場合、HTTP 200 応答が返されます。

PUT エンドポイントを検証する

サンプル アプリは、MapPut を使用して単一の PUT エンドポイントを実装しています。

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

このメソッドは、HTTP PUT を使用していることを除き、MapPost メソッドに似ています。 応答が成功すると、204 (コンテンツなし) が返されます。 HTTP 仕様に従って、PUT 要求では、変更だけでなく、更新されたエンティティ全体を送信するようクライアントに求めます。 部分的な更新をサポートするには、HTTP PATCH を使用します。

PUT エンドポイントをテストする

このサンプルでは、アプリを起動するたびに開始することが必要なメモリ内データベースが使われています。 PUT 呼び出しを実行する前に、データベース内にアイテムが存在している必要があります。 GET を呼び出して、PUT 呼び出しを実行する前にデータベース内にアイテムが確実に存在していることを確認します。

Id = 1 の To Do 項目を更新し、その名前を "feed fish" に設定します。

  • Endpoints ExplorerPUT エンドポイントを右クリックし、[要求の生成] を選びます。

    TodoApi.http ファイルに以下の内容が追加されます。

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • PUT 要求行の {id}1 に置き換えます。

  • PUT 要求行の直後に以下の行を追加します。

    Content-Type: application/json
    
    {
      "name": "feed fish",
      "isComplete": false
    }
    

    先ほどのコードでは、Content-Type ヘッダーと JSON 要求の本文を追加しています。

  • 新しい PUT 要求行の上にある [要求の送信] リンクを選びます。

    PUT 要求がアプリに送信され、応答が [応答] ペインに表示されます。 応答本文は空であり、状態コードは 204 です。

DELETE エンドポイントの検証とテスト

サンプル アプリは、MapDelete を使用して単一の DELETE エンドポイントを実装しています。

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
  • Endpoints ExplorerDELETE エンドポイントを右クリックし、[要求の生成] を選びます。

    DELETE 要求が TodoApi.http に追加されます。

  • DELETE 要求行の {id}1 に置き換えます。 DELETE 要求は次の例のようになります。

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • DELETE 要求の [要求の送信] リンクを選択します。

    DELETE 要求がアプリに送信され、応答が [応答] ペインに表示されます。 応答本文は空であり、状態コードは 204 です。

MapGroup API を使う

サンプル アプリ コードでは、エンドポイントを設定するたびに todoitems URL プレフィックスが繰り返されます。 API には共通の URL プレフィックスを持つエンドポイントのグループがあることが多く、MapGroup メソッドはそのようなグループの整理に役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorizationWithMetadata のようなメソッドを 1 回呼び出すだけで、エンドポイントのグループ全体をカスタマイズできます。

Program.cs の内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

上記のコードには、次の変更が加えられています。

  • var todoItems = app.MapGroup("/todoitems"); を追加して、URL プレフィックス /todoitems を使ってグループを設定します。
  • すべての app.Map<HttpVerb> メソッドを todoItems.Map<HttpVerb> に変更します。
  • Map<HttpVerb> メソッド呼び出しから URL プレフィックス /todoitems を削除します。

エンドポイントをテストして、同じように動作することを確認します。

TypedResults API を使う

Results ではなく TypedResults を返すと、テストのしやすさや、エンドポイントを記述するための OpenAPI の応答型メタデータが自動的に返されるなど、いくつかの利点があります。 詳しくは、「TypedResults と Results」をご覧ください。

Map<HttpVerb> メソッドは、ラムダ式を使う代わりにルート ハンドラー メソッドを呼び出すことができます。 例を確認するには、次のコードを使って Program.cs を更新します。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

これで Map<HttpVerb> のコードは、ラムダ式ではなくメソッドを呼び出すようになりました。

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

これらのメソッドは、IResult を実装し TypedResults で定義されるオブジェクトを返します。

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

単体テストはこれらのメソッドを呼び出して、正しい型を返すかどうかをテストできます。 たとえば、メソッドが GetAllTodos である場合:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

単体テスト コードは、ハンドラー メソッドから Ok<Todo[]> 型のオブジェクトが返されることを確認できます。 次に例を示します。

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

過剰な投稿を防止する

現在、サンプル アプリでは Todo オブジェクト全体が公開されています。 運用環境アプリケーション内の運用環境アプリでは、入力され、返されるデータを制限するためにモデルのサブセットがよく使われます。 その背景には複数の理由があり、セキュリティは主なものです。 モデルのサブセットは、通常、データ転送オブジェクト (DTO)、入力モデル、またはビュー モデルと呼ばれます。 この記事では DTO を使用しています。

DTO を使用すると、次のことができます。

  • 過剰な投稿を防止する。
  • クライアントが表示しないことになっているプロパティを非表示にする。
  • ペイロード サイズを減らすには、いくつかのプロパティを省略します。
  • 入れ子になったオブジェクトを含むオブジェクト グラフをフラット化する。 フラット化されたオブジェクト グラフは、クライアントにとってより便利になる可能性があります。

DTO のアプローチを実演するために、Todo クラスを更新して、シークレット フィールドを含めます。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

シークレット フィールドは、このアプリでは非表示にする必要がありますが、管理アプリの場合は公開することを選択できます。

シークレット フィールドを投稿および取得できることを確認します。

次のコードのファイルを、TodoItemDTO.cs という名前で作成します。

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

この DTO モデルを使うには、Program.cs ファイルの内容を次のコードに置き換えます。

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

シークレット フィールドを除くすべてのフィールドを投稿および取得できることを確認します。

完成したサンプルを使用したトラブルシューティング

解決できない問題が発生した場合は、コードを完成したプロジェクトと比較します。 完成したプロジェクトを表示またはダウンロードします (ダウンロード方法)。

次のステップ

詳細情報

Minimal API のクイック リファレンスを参照してください