次の方法で共有


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

Note

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

Warning

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

Important

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

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

リック・アンダーソントム・ダイクストラ

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

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

Overview

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

API Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
POST /todoitems 新しいアイテムを追加します。 To-do item To-do item
PUT /todoitems/{id} 既存のアイテムを更新します。 To-do item None
DELETE /todoitems/{id}     アイテムを削除します。 None None

Prerequisites

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

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

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

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

    Visual Studio で新しいプロジェクトを作成する

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

  • [追加情報] ダイアログで、次の 手順 を実行します。

    • .NET 9.0 を選択する
    • [最上位レベルのステートメントを使用しない] をオフにする
    • Select Create

    Additional information

コードを確認する

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

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

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

app.Run();

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

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

アプリを実行する

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 パッケージの管理] を選択します。
  • 「Browse」タブを選択します。
  • [ プレリリースを含める] を選択します。
  • 検索ボックスに 「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 コンテナーは、データベース コンテキストやその他のサービスへのアクセスを提供します。

このチュートリアルでは 、エンドポイント エクスプローラーと .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 エンドポイントを右クリックし、[要求の生成] を選択します。

    エンドポイント エクスプローラーのコンテキスト メニューの [要求の生成] メニュー項目が強調表示されています。

    次の例のような内容の 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 Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
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 エンドポイントを呼び出すか、 エンドポイント エクスプローラーを使用して、アプリをテストします。 エンドポイント エクスプローラーの手順を次 に示します

  • エンドポイント エクスプローラーで、最初の 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 要求をもう一度試します。

Return values

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" に設定します。

  • エンドポイント エクスプローラーでPUT エンドポイントを右クリックし、[要求の生成] を選択します。

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

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

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

    Content-Type: application/json
    
    {
      "id": 1,
      "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();
});
  • エンドポイント エクスプローラーで、DELETE エンドポイントを右クリックし、[要求の生成] を選択します。

    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> に変更します。
  • /todoitems メソッド呼び出しから URL プレフィックス Map<HttpVerb> を削除します。

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

TypedResults API を使う

TypedResults ではなく Results を返すと、テストのしやすさや、エンドポイントを記述するための 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[]> 型のオブジェクトがハンドラー メソッドから返されることを確認できます。 For example:

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);
}

Prevent over-posting

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

次に、DTO を使用できます。

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

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();
}

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

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

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

Next steps

Learn more

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

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

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

Overview

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

API Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
POST /todoitems 新しいアイテムを追加します。 To-do item To-do item
PUT /todoitems/{id} 既存のアイテムを更新します。 To-do item None
DELETE /todoitems/{id}     アイテムを削除します。 None None

Prerequisites

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

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

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

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

    Visual Studio で新しいプロジェクトを作成する

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

  • [追加情報] ダイアログで、次の 手順 を実行します。

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

    Additional information

コードを確認する

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

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

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

app.Run();

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

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

アプリを実行する

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 パッケージの管理] を選択します。
  • 「Browse」タブを選択します。
  • 検索ボックスに 「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 ミドルウェアを構成する

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

    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 の概要」を参照してください。

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

    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 を選択します。

  • 要求本文フィールドには、API のパラメーターを反映する生成された形式の例が含まれていることに注意してください。

  • 要求本文に、オプションの id を指定せずに、「JSON for a to-do item」と入力します。

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Select Execute.

    Post を使用する Swagger

Swagger では、[実行] ボタンの下に [応答 ] ウィンドウが 表示 されます。

Post 応答による Swagger

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

  • 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 Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
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>試してみる>実行します。

  • または、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>を選択して試してください
    • id フィールドを1に設定し、[実行] を選択します
  • または、URI https://localhost:<port>/todoitems/1を入力して、ブラウザーから GET /todoitems を呼び出します。 たとえば、https://localhost:5001/todoitems/1 のように指定します。

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

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

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

Return values

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}」>の試してみるを選択します。

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

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

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Select Execute.

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}>試してみてください

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

    DELETE 要求がアプリに送信され、[応答] ウィンドウに 応答 が表示されます。 応答本文が空で、 サーバーの応答 状態コードが 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> に変更します。
  • /todoitems メソッド呼び出しから URL プレフィックス Map<HttpVerb> を削除します。

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

TypedResults API を使う

TypedResults ではなく Results を返すと、テストのしやすさや、エンドポイントを記述するための 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[]> 型のオブジェクトがハンドラー メソッドから返されることを確認できます。 For example:

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);
}

Prevent over-posting

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

次に、DTO を使用できます。

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

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>();
}

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

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

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

Next steps

Learn more

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

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

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

Overview

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

API Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
POST /todoitems 新しいアイテムを追加します。 To-do item To-do item
PUT /todoitems/{id} 既存のアイテムを更新します。 To-do item None
DELETE /todoitems/{id}     アイテムを削除します。 None None

Prerequisites

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

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

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

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

    Visual Studio で新しいプロジェクトを作成する

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

  • [追加情報] ダイアログで、次の 手順 を実行します。

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

コードを確認する

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

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

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

app.Run();

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

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

アプリを実行する

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 パッケージの管理] を選択します。
  • 「Browse」タブを選択します。
  • 検索ボックスに 「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;
    
  • appvar app = builder.Build(); が定義される前に、次の強調表示されたコードを追加します

    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 の概要」を参照してください。

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

    
    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 を選択します。

  • 要求本文フィールドには、API のパラメーターを反映する生成された形式の例が含まれていることに注意してください。

  • 要求本文に、オプションの id を指定せずに、「JSON for a to-do item」と入力します。

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Select Execute.

    Post データを使用する Swagger

Swagger では、[実行] ボタンの下に [応答 ] ウィンドウが 表示 されます。

Post 応答 ペインを使用する Swagger

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

  • 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 Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
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>試してみる>実行します。

  • または、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>を選択して試してください
    • id フィールドを1に設定し、[実行] を選択します
  • または、URI https://localhost:<port>/todoitems/1を入力して、ブラウザーから GET /todoitems を呼び出します。 例: https://localhost:5001/todoitems/1

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

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

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

Return values

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}」>の試してみるを選択します。

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

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

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Select Execute.

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}>試してみてください

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

    DELETE 要求がアプリに送信され、[応答] ウィンドウに 応答 が表示されます。 応答本文が空で、 サーバーの応答 状態コードが 204 です。

Prevent over-posting

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

次に、DTO を使用できます。

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

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 をテストする

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

Azure に発行する

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

Additional resources

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

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

Overview

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

API Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete 完了した To Do 項目を取得します。 None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
POST /todoitems 新しいアイテムを追加します。 To-do item To-do item
PUT /todoitems/{id} 既存のアイテムを更新します。 To-do item None
DELETE /todoitems/{id}     アイテムを削除します。 None None

Prerequisites

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

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

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

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

    Visual Studio で新しいプロジェクトを作成する

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

  • [追加情報] ダイアログで、次の 手順 を実行します。

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

    Additional information

コードを確認する

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

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

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

app.Run();

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

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

アプリを実行する

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 パッケージの管理] を選択します。
  • 「Browse」タブを選択します。
  • 検索ボックスに 「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 コンテナーは、データベース コンテキストやその他のサービスへのアクセスを提供します。

このチュートリアルでは 、エンドポイント エクスプローラーと .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 エンドポイントを右クリックし、[要求の生成] を選択します。

    エンドポイント エクスプローラーのコンテキスト メニューの [要求の生成] メニュー項目が強調表示されています。

    次の例のような内容の 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 Description Request body Response body
GET /todoitems すべての To Do アイテムを取得します。 None To Do アイテムの配列
GET /todoitems/complete すべての完了した To Do 項目を取得します None To Do アイテムの配列
GET /todoitems/{id} ID でアイテムを取得します。 None To-do item
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 エンドポイントを呼び出すか、 エンドポイント エクスプローラーを使用して、アプリをテストします。 エンドポイント エクスプローラーの手順を次 に示します

  • エンドポイント エクスプローラーで、最初の 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 要求をもう一度試します。

Return values

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" に設定します。

  • エンドポイント エクスプローラーでPUT エンドポイントを右クリックし、[要求の生成] を選択します。

    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();
});
  • エンドポイント エクスプローラーで、DELETE エンドポイントを右クリックし、[要求の生成] を選択します。

    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> に変更します。
  • /todoitems メソッド呼び出しから URL プレフィックス Map<HttpVerb> を削除します。

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

TypedResults API を使う

TypedResults ではなく Results を返すと、テストのしやすさや、エンドポイントを記述するための 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[]> 型のオブジェクトがハンドラー メソッドから返されることを確認できます。 For example:

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);
}

Prevent over-posting

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

次に、DTO を使用できます。

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

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();
}

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

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

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

Next steps

Learn more

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