チュートリアル: ASP.NET Core を使って最小 API を作成する
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 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 | なし |
必須コンポーネント
Visual Studio 2022 Preview と ASP.NET および Web 開発ワークロード。
API プロジェクトを作成する
Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。
[新しいプロジェクトの作成] ダイアログで次を行います。
- [テンプレートの検索] ボックスに、「
Empty
」と入力します。 - [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。
- [テンプレートの検索] ボックスに、「
プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。
[追加情報] ダイアログで、次を行います。
- [.NET 9.0 (プレビュー)] を選択します
- [最上位レベルのステートメントを使用しない] をオフにする
- [作成]
コードを確認する
Program.cs
ファイルには、次のコードが含まれています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
上記のコードでは次の操作が行われます。
- 事前に構成された既定値で WebApplicationBuilder と WebApplication を作成します。
Hello World!
を返す HTTP GET エンドポイント/
を作成します。
アプリを実行する
Ctrl + F5 キーを押して、デバッガーなしで実行します。
Visual Studio に次のダイアログが表示されます。
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 エンドポイントを右クリックし、[要求の生成] を選びます。
次の例のような内容の
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
要求行の上にある [要求の送信] リンクを選択します。POST 要求がアプリに送信され、応答が [応答] ペインに表示されます。
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 Explorer で 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();
});
Endpoints Explorer で DELETE エンドポイントを右クリックし、[要求の生成] を選びます。
DELETE 要求が
TodoApi.http
に追加されます。DELETE 要求行の
{id}
を1
に置き換えます。 DELETE 要求は次の例のようになります。DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
DELETE 要求の [要求の送信] リンクを選択します。
DELETE 要求がアプリに送信され、応答が [応答] ペインに表示されます。 応答本文は空であり、状態コードは 204 です。
MapGroup API を使う
サンプル アプリ コードでは、エンドポイントを設定するたびに todoitems
URL プレフィックスが繰り返されます。 API には共通の URL プレフィックスを持つエンドポイントのグループがあることが多く、MapGroup メソッドはそのようなグループの整理に役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorization や WithMetadata のようなメソッドを 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();
}
シークレット フィールドを除くすべてのフィールドを投稿および取得できることを確認します。
完成したサンプルを使用したトラブルシューティング
解決できない問題が発生した場合は、コードを完成したプロジェクトと比較します。 完成したプロジェクトを表示またはダウンロードします (ダウンロード方法)。
次のステップ
- JSON シリアル化オプションを構成する。
- エラーと例外の処理: 開発者の例外ページ は、最小限の API アプリに対して開発環境で既定で有効になっています。 エラーや例外の処理方法については、ASP.NET Core API でのエラーの処理に関する記事をご覧ください。
- Minimal API アプリのテストの例については、この GitHub サンプルを参照してください。
- 最小 API で OpenAPI サポート。
- クイック スタート: Azure に発行します。
- ASP.NET Core の Minimal API の整理。
詳細情報
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 | なし |
必須コンポーネント
Visual Studio 2022 と ASP.NET と Web 開発ワークロード。
API プロジェクトを作成する
Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。
[新しいプロジェクトの作成] ダイアログで次を行います。
- [テンプレートの検索] ボックスに、「
Empty
」と入力します。 - [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。
- [テンプレートの検索] ボックスに、「
プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。
[追加情報] ダイアログで、次を行います。
- [.NET 7.0] を選ぶ
- [最上位レベルのステートメントを使用しない] をオフにする
- [作成]
コードを確認する
Program.cs
ファイルには、次のコードが含まれています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
上記のコードでは次の操作が行われます。
- 事前に構成された既定値で WebApplicationBuilder と WebApplication を作成します。
Hello World!
を返す HTTP GET エンドポイント/
を作成します。
アプリを実行する
Ctrl + F5 キーを押して、デバッガーなしで実行します。
Visual Studio に次のダイアログが表示されます。
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 テスト ページで、[Post /todoitems]>[Try it out] を選びます。
[Request body] フィールドには、API のパラメーターを反映した生成結果の形式例が表示されることに注意してください。
要求本文に、「オプションの
id
を指定せずに、To Do アイテムの JSON」と入力します。{ "name":"walk dog", "isComplete":true }
[実行] を選択します。
Swagger には、[Execute] ボタンの下に [Responses] ペインがあります。
役に立つ詳細事項をいくつか紹介します。
- cURL: Swagger には、Unix/Linux 構文での cURL コマンド例が用意されています。これは、Git for Windows の Git Bash など、Unix/Linux 構文を使う任意の bash シェルを使ってコマンド ラインで実行できます。
- 要求 URL: API 呼び出しに対して Swagger UI の JavaScript コードによって作成される HTTP 要求の簡略化された表現。 実際の要求には、ヘッダー、クエリ パラメーター、要求本文などの詳細が含まれる場合があります。
- サーバーの応答: 応答の本文とヘッダーが含まれています。 応答本文には、
id
が1
に設定されたことが示されています。 - 応答コード: 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 メソッドはそのようなグループの整理に役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorization や WithMetadata のようなメソッドを 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>();
}
シークレット フィールドを除くすべてのフィールドを投稿および取得できることを確認します。
完成したサンプルを使用したトラブルシューティング
解決できない問題が発生した場合は、コードを完成したプロジェクトと比較します。 完成したプロジェクトを表示またはダウンロードします (ダウンロード方法)。
次のステップ
- JSON シリアル化オプションを構成する。
- エラーと例外の処理: 開発者の例外ページ は、最小限の API アプリに対して開発環境で既定で有効になっています。 エラーや例外の処理方法については、ASP.NET Core API でのエラーの処理に関する記事をご覧ください。
- Minimal API アプリのテストの例については、この GitHub サンプルを参照してください。
- 最小 API で OpenAPI サポート。
- クイック スタート: Azure に発行します。
- ASP.NET Core の Minimal API の整理。
詳細情報
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 | なし |
必須コンポーネント
- Visual Studio 2022 と ASP.NET と Web 開発ワークロード。
- .NET 6.0 SDK
API プロジェクトを作成する
Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。
[新しいプロジェクトの作成] ダイアログで次を行います。
- [テンプレートの検索] ボックスに、「
Empty
」と入力します。 - [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。
- [テンプレートの検索] ボックスに、「
プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。
[追加情報] ダイアログで、次を行います。
- [.NET 6.0] を選びます
- [最上位レベルのステートメントを使用しない] をオフにする
- [作成]
コードを確認する
Program.cs
ファイルには、次のコードが含まれています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
上記のコードでは次の操作が行われます。
- 事前に構成された既定値で WebApplicationBuilder と WebApplication を作成します。
Hello World!
を返す HTTP GET エンドポイント/
を作成します。
アプリを実行する
Ctrl + F5 キーを押して、デバッガーなしで実行します。
Visual Studio に次のダイアログが表示されます。
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 テスト ページで、[Post /todoitems]>[Try it out] を選びます。
[Request body] フィールドには、API のパラメーターを反映した生成結果の形式例が表示されることに注意してください。
要求本文に、「オプションの
id
を指定せずに、To Do アイテムの JSON」と入力します。{ "name":"walk dog", "isComplete":true }
[実行] を選択します。
Swagger には、[Execute] ボタンの下に [Responses] ペインがあります。
役に立つ詳細事項をいくつか紹介します。
- cURL: Swagger には、Unix/Linux 構文での cURL コマンド例が用意されています。これは、Git for Windows の Git Bash など、Unix/Linux 構文を使う任意の bash シェルを使ってコマンド ラインで実行できます。
- 要求 URL: API 呼び出しに対して Swagger UI の JavaScript コードによって作成される HTTP 要求の簡略化された表現。 実際の要求には、ヘッダー、クエリ パラメーター、要求本文などの詳細が含まれる場合があります。
- サーバーの応答: 応答の本文とヘッダーが含まれています。 応答本文には、
id
が1
に設定されたことが示されています。 - 応答コード: 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 | なし |
必須コンポーネント
Visual Studio 2022 と ASP.NET と Web 開発ワークロード。
API プロジェクトを作成する
Visual Studio 2022 を開始し、[新しいプロジェクトの作成] を選択します。
[新しいプロジェクトの作成] ダイアログで次を行います。
- [テンプレートの検索] ボックスに、「
Empty
」と入力します。 - [ASP.NET Core 空] テンプレートを選択し、[次へ] を選びます。
- [テンプレートの検索] ボックスに、「
プロジェクトに「TodoApi」 という名前を付け、 [次へ] を選択します。
[追加情報] ダイアログで、次を行います。
- [.NET 8.0 (長期的なサポート)] を選択します
- [最上位レベルのステートメントを使用しない] をオフにする
- [作成]
コードを確認する
Program.cs
ファイルには、次のコードが含まれています。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
上記のコードでは次の操作が行われます。
- 事前に構成された既定値で WebApplicationBuilder と WebApplication を作成します。
Hello World!
を返す HTTP GET エンドポイント/
を作成します。
アプリを実行する
Ctrl + F5 キーを押して、デバッガーなしで実行します。
Visual Studio に次のダイアログが表示されます。
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 エンドポイントを右クリックし、[要求の生成] を選びます。
次の例のような内容の
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
要求行の上にある [要求の送信] リンクを選択します。POST 要求がアプリに送信され、応答が [応答] ペインに表示されます。
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 Explorer で 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();
});
Endpoints Explorer で DELETE エンドポイントを右クリックし、[要求の生成] を選びます。
DELETE 要求が
TodoApi.http
に追加されます。DELETE 要求行の
{id}
を1
に置き換えます。 DELETE 要求は次の例のようになります。DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
DELETE 要求の [要求の送信] リンクを選択します。
DELETE 要求がアプリに送信され、応答が [応答] ペインに表示されます。 応答本文は空であり、状態コードは 204 です。
MapGroup API を使う
サンプル アプリ コードでは、エンドポイントを設定するたびに todoitems
URL プレフィックスが繰り返されます。 API には共通の URL プレフィックスを持つエンドポイントのグループがあることが多く、MapGroup メソッドはそのようなグループの整理に役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorization や WithMetadata のようなメソッドを 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();
}
シークレット フィールドを除くすべてのフィールドを投稿および取得できることを確認します。
完成したサンプルを使用したトラブルシューティング
解決できない問題が発生した場合は、コードを完成したプロジェクトと比較します。 完成したプロジェクトを表示またはダウンロードします (ダウンロード方法)。
次のステップ
- JSON シリアル化オプションを構成する。
- エラーと例外の処理: 開発者の例外ページ は、最小限の API アプリに対して開発環境で既定で有効になっています。 エラーや例外の処理方法については、ASP.NET Core API でのエラーの処理に関する記事をご覧ください。
- Minimal API アプリのテストの例については、この GitHub サンプルを参照してください。
- 最小 API で OpenAPI サポート。
- クイック スタート: Azure に発行します。
- ASP.NET Core の Minimal API の整理。
詳細情報
Minimal API のクイック リファレンスを参照してください
ASP.NET Core