共用方式為


教學課程:使用 ASP.NET Core 建立基本 API

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

作者:Rick AndersonRyan Nowak

基本 API 的架構是要建立具有最低相依性的 HTTP API。 它們很適合只要包含 ASP.NET Core 中基本檔案、功能和相依性的微服務及應用程式。

本教學課程將教導您使用 ASP.NET Core 建立基本 API 的基本概念。 在 ASP.NET Core 中建立 API 的另一種方法是使用控制器。 如需在基本 API 與控制器型 API 之間選擇的說明,請參閱 API 概觀。 如需根據包含更多功能的控制器建立 API 專案的教學課程,請參閱建立 Web API

概觀

本教學課程會建立以下 API:

API 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
POST /todoitems 新增記錄 待辦事項 待辦事項
PUT /todoitems/{id} 更新現有的項目 待辦事項
DELETE /todoitems/{id}     刪除項目

必要條件

建立 API 專案

  • 啟動 Visual Studio 2022 並選取 [建立新專案]

  • 在 [建立新專案] 對話方塊中:

    • 在 [搜尋範本] 搜尋方塊中輸入 Empty
    • 選取 [ASP.NET Core 空白] 範本,然後選取 [下一步]

    Visual Studio 建立新專案

  • 將專案命名為 TodoApi,然後選取 [下一步]

  • 在 [其他資訊] 對話方塊中:

    • 選取 [.NET 9.0 (預覽)]
    • 取消勾選 [不要使用最上層陳述式]
    • 選取 [建立]

    其他資訊

檢查程式碼

Program.cs 檔案包含下列程式碼:

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

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

app.Run();

上述 程式碼:

執行應用程式

按 Ctrl+F5 即可執行而不使用偵錯工具。

Visual Studio 會顯示下列對話方塊:

此專案設定為使用 SSL。為避免瀏覽器中的 SSL 警告,您可以選擇信任 IIS Express 產生的自我簽署憑證。您要信任 IIS Express SSL 憑證嗎?

如果您信任 IIS Express SSL 憑證,請選取 [是]

此時會顯示下列對話方塊:

安全性警告對話方塊

若您同意信任開發憑證,請選取 [是]

如需關於信任 Firefox 瀏覽器的資訊,請參閱 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 憑證錯誤

Visual Studio 會啟動 Kestrel 網頁伺服器,並開啟瀏覽器視窗。

Hello World! 隨即在瀏覽器中顯示。 Program.cs 檔案包含最基本但完整的應用程式。

關閉瀏覽器視窗。

新增 NuGet 套件

必須新增 NuGet 封裝,以支援本教學課程中使用的資料庫和診斷。

  • 在 [工具] 功能表上,選取 [NuGet 套件管理員] > [管理解決方案的 NuGet 套件]
  • 選取 [瀏覽] 索引標籤。
  • 選取 [包含發行前版本]
  • 在搜尋方塊中輸入 Microsoft.EntityFrameworkCore.InMemory,然後選取 Microsoft.EntityFrameworkCore.InMemory
  • 選取右窗格中的 [專案] 核取方塊,然後選取 [安裝]
  • 遵循上述指示來新增 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore 封裝。

模型和資料庫內容類別

  • 在專案資料夾中,使用下列程式碼建立名為 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。

測試張貼資料

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 端點將資料新增至應用程式。

  • 選取 [檢視] > [其他視窗] > [端點總管]

  • 以滑鼠右鍵按一下 [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 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得所有已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
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 要求。

傳回值

ASP.NET Core 會自動將物件序列化為 JSON,並將 JSON 寫入至回應訊息的本文。 此傳回型別的回應碼為 200 OK,假設沒有任何未處理的例外狀況。 未處理的例外狀況會轉譯成 5xx 錯誤。

傳回型別可代表各種 HTTP 狀態碼。 例如,GET /todoitems/{id} 可傳回兩個不同的狀態值:

  • 如果沒有項目符合所要求的識別碼,方法會傳回 404 狀態 NotFound 錯誤碼。
  • 否則,方法會傳回 200 與 JSON 回應本文。 傳回 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();
});

此方法與 MapPost 方法類似,除了它使用 HTTP PUT。 成功的回應會傳回 204 (無內容)。 根據 HTTP 規格,PUT 要求需要用戶端傳送整個更新的實體,而不只是變更。 若要支援部分更新,請使用 HTTP PATCH

測試 PUT 端點

此範例使用在每次應用程式啟動都必須初始化的記憶體內部資料庫。 資料庫中必須有項目,您才能進行 PUT 呼叫。 在發出 PUT 呼叫之前,呼叫 GET 以確保資料庫中有項目。

更新 Id = 1 的待辦事項,並將其名稱設定為 "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) 的單一呼叫來自訂整個端點群組。

以下列程式碼取代 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

傳回 TypedResults 而不是 Results 有幾個優點,包括可測試性,並自動傳回 OpenAPI 的回應型別中繼資料來描述端點。 如需詳細資訊,請參閱 TypedResults 與 Results

Map<HttpVerb> 方法可以呼叫路由處理常式方法,而不使用 Lambda。 若要查看範例,請使用下列程式碼更新 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> 程式碼現在會呼叫方法,而不是 Lambda:

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

以下列程式碼取代 Program.cs 檔案的內容,以使用此 DTO 模型:

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

驗證您可以張貼並取得祕密欄位以外的所有欄位。

使用已完成的範例進行疑難排解

若您遇到無法解決的問題,請將您的程式碼與已完成的專案進行比較。 檢視或下載已完成的專案 (如何下載)。

下一步

深入了解

請參閱基本 API 快速參考

基本 API 的架構是要建立具有最低相依性的 HTTP API。 它們非常適合想要只包含 ASP.NET Core 中基本檔案、功能和相依性的微服務和應用程式。

本教學課程將教導您使用 ASP.NET Core 建立基本 API 的基本概念。 在 ASP.NET Core 中建立 API 的另一種方法是使用控制器。 如需在基本 API 與控制器型 API 之間選擇的說明,請參閱 API 概觀。 如需根據包含更多功能的控制器建立 API 專案的教學課程,請參閱建立 Web API

概觀

本教學課程會建立以下 API:

API 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
POST /todoitems 新增記錄 待辦事項 待辦事項
PUT /todoitems/{id} 更新現有的項目 待辦事項
DELETE /todoitems/{id}     刪除項目

必要條件

建立 API 專案

  • 啟動 Visual Studio 2022 並選取 [建立新專案]

  • 在 [建立新專案] 對話方塊中:

    • 在 [搜尋範本] 搜尋方塊中輸入 Empty
    • 選取 [ASP.NET Core 空白] 範本,然後選取 [下一步]

    Visual Studio 建立新專案

  • 將專案命名為 TodoApi,然後選取 [下一步]

  • 在 [其他資訊] 對話方塊中:

    • 選取 [.NET 7.0]
    • 取消勾選 [不要使用最上層陳述式]
    • 選取 [建立]

    其他資訊

檢查程式碼

Program.cs 檔案包含下列程式碼:

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

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

app.Run();

上述 程式碼:

執行應用程式

按 Ctrl+F5 即可執行而不使用偵錯工具。

Visual Studio 會顯示下列對話方塊:

此專案設定為使用 SSL。為避免瀏覽器中的 SSL 警告,您可以選擇信任 IIS Express 產生的自我簽署憑證。您要信任 IIS Express SSL 憑證嗎?

如果您信任 IIS Express SSL 憑證,請選取 [是]

此時會顯示下列對話方塊:

安全性警告對話方塊

若您同意信任開發憑證,請選取 [是]

如需關於信任 Firefox 瀏覽器的資訊,請參閱 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 憑證錯誤

Visual Studio 會啟動 Kestrel 網頁伺服器,並開啟瀏覽器視窗。

Hello World! 隨即在瀏覽器中顯示。 Program.cs 檔案包含最基本但完整的應用程式。

新增 NuGet 套件

必須新增 NuGet 封裝,以支援本教學課程中使用的資料庫和診斷。

  • 在 [工具] 功能表上,選取 [NuGet 套件管理員] > [管理解決方案的 NuGet 套件]
  • 選取 [瀏覽] 索引標籤。
  • 在搜尋方塊中輸入 Microsoft.EntityFrameworkCore.InMemory,然後選取 Microsoft.EntityFrameworkCore.InMemory
  • 選取右窗格中的 [專案] 核取方塊。
  • 在 [版本] 下拉式清單中,選取可用的最新版本 7 (例如 7.0.17),然後選取 [安裝]
  • 遵循上述指示,以可用的最新版本 7 新增 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore 套件。

模型和資料庫內容類別

在專案資料夾中,使用下列程式碼建立名為 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,其整合了 Swagger 工具以產生符合 OpenAPI 規格的測試 UI:

  • NSwag:.NET 連結庫,可將 Swagger 直接整合到 ASP.NET Core 應用程式,並提供中介軟體和設定。
  • Swagger:一組開放原始碼工具,例如 OpenAPIGenerator 和 SwaggerUI,可產生遵循 OpenAPI 規格的 API 測試頁面。
  • OpenAPI 規格:根據控制器和模型內的 XML 和屬性註釋來描述 API 功能的文件。

如需搭配 ASP.NET 使用 OpenAPI 和 NSwag 的詳細資訊,請參閱使用 Swagger/OpenAPI 的 ASP.NET Core Web API 文件

安裝 Swagger 工具

  • 執行以下命令:

    dotnet add package NSwag.AspNetCore
    

上面的命令會新增 NSwag.AspNetCore 套件,其中包含用來產生 Swagger 文件和 UI 的工具。

設定 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 總管,這是提供 HTTP API 相關中繼資料的服務。 Swagger 會使用 API 總管來產生 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";
        });
    }
    

    前面的程式碼可讓 Swagger 中介軟體提供所產生的 JSON 文件和 Swagger UI。 Swagger 只會在開發環境中啟用。 在生產環境中啟用 Swagger 可能會公開關於 API 結構和實作的潛在敏感性詳細資料。

測試張貼資料

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]>[試用]

  • 請注意,[要求本文] 欄位包含所產生的範例格式,且會反映 API 的參數。

  • 在要求本文中,針對待辦事項輸入 JSON,而不指定選擇性的 id

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 選取 [執行]。

    Swagger 與 Post

Swagger 會在 [執行] 按鈕下方提供 [回應] 窗格。

具有 Post 回應的 Swagger

請記下一些實用的詳細資料:

  • cURL:Swagger 提供 Unix/Linux 語法中的範例 cURL 命令,其可在命令列中搭配任何使用 Unix/Linux 語法的 Bash 殼層 (包括來自 Git for Windows 的 Git Bash) 來執行。
  • 要求 URL:Swagger UI 的 JavaScript 程式碼針對 API 呼叫所發出 HTTP 要求的簡化表示法。 實際要求可能包含標頭和查詢參數與要求本文等詳細資料。
  • 伺服器回應:包含回應本文和標頭。 回應本文顯示 id 已設定為 1
  • 回應碼:傳回 201 HTTP 狀態碼,指出已成功處理要求,並導致建立新的資源。

檢查 GET 端點

範例應用程式會藉由呼叫 MapGet 來實作數個 GET 端點:

API 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得所有已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
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} 以從特定識別碼傳回資料:

    • 選取 [GET /todoitems]>[試用]
    • 將 [識別碼] 欄位設定為 1,然後選取 [執行]
  • 或者,從瀏覽器輸入 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} 可傳回兩個不同的狀態值:

  • 如果沒有項目符合所要求的識別碼,方法會傳回 404 狀態 NotFound 錯誤碼。
  • 否則,方法會傳回 200 與 JSON 回應本文。 傳回 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();
});

此方法與 MapPost 方法類似,除了它使用 HTTP PUT。 成功的回應會傳回 204 (無內容)。 根據 HTTP 規格,PUT 要求需要用戶端傳送整個更新的實體,而不只是變更。 若要支援部分更新,請使用 HTTP PATCH

測試 PUT 端點

此範例使用在每次應用程式啟動都必須初始化的記憶體內部資料庫。 資料庫中必須有項目,您才能進行 PUT 呼叫。 在發出 PUT 呼叫之前,呼叫 GET 以確保資料庫中有項目。

更新 Id = 1 的待辦事項,並將其名稱設定為 "feed fish"

使用 Swagger 來傳送 PUT 要求:

  • 選取 [Put /todoitems/{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}]>[試用]

  • 將 [識別碼] 欄位設定為 1,然後選取 [執行]

    DELETE 要求會傳送至應用程式,而回應會顯示在 [回應] 窗格中。 回應本文是空的,而伺服器回應狀態碼是 204。

使用 MapGroup API

範例應用程式程式碼會在每次設定端點時重複 todoitems URL 前置詞。 API 通常會有通用 URL 前置詞的端點群組,而且 MapGroup 方法可用來協助組織這類群組。 其可減少重複的程式碼,並允許使用對方法 (例如 RequireAuthorizationWithMetadata) 的單一呼叫來自訂整個端點群組。

以下列程式碼取代 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

傳回 TypedResults 而不是 Results 有幾個優點,包括可測試性,並自動傳回 OpenAPI 的回應型別中繼資料來描述端點。 如需詳細資訊,請參閱 TypedResults 與 Results

Map<HttpVerb> 方法可以呼叫路由處理常式方法,而不使用 Lambda。 若要查看範例,請使用下列程式碼更新 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> 程式碼現在會呼叫方法,而不是 Lambda:

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

以下列程式碼取代 Program.cs 檔案的內容,以使用此 DTO 模型:

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

驗證您可以張貼並取得祕密欄位以外的所有欄位。

使用已完成的範例進行疑難排解

若您遇到無法解決的問題,請將您的程式碼與已完成的專案進行比較。 檢視或下載已完成的專案 (如何下載)。

下一步

深入了解

請參閱基本 API 快速參考

基本 API 的架構是要建立具有最低相依性的 HTTP API。 它們非常適合想要只包含 ASP.NET Core 中基本檔案、功能和相依性的微服務和應用程式。

本教學課程將教導您使用 ASP.NET Core 建立基本 API 的基本概念。 在 ASP.NET Core 中建立 API 的另一種方法是使用控制器。 如需在基本 API 與控制器型 API 之間選擇的說明,請參閱 API 概觀。 如需根據包含更多功能的控制器建立 API 專案的教學課程,請參閱建立 Web API

概觀

本教學課程會建立以下 API:

API 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
POST /todoitems 新增記錄 待辦事項 待辦事項
PUT /todoitems/{id} 更新現有的項目 待辦事項
DELETE /todoitems/{id}     刪除項目

必要條件

建立 API 專案

  • 啟動 Visual Studio 2022 並選取 [建立新專案]

  • 在 [建立新專案] 對話方塊中:

    • 在 [搜尋範本] 搜尋方塊中輸入 Empty
    • 選取 [ASP.NET Core 空白] 範本,然後選取 [下一步]

    Visual Studio 建立新專案

  • 將專案命名為 TodoApi,然後選取 [下一步]

  • 在 [其他資訊] 對話方塊中:

    • 選取 [.NET 6.0]
    • 取消勾選 [不要使用最上層陳述式]
    • 選取 [建立]

檢查程式碼

Program.cs 檔案包含下列程式碼:

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

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

app.Run();

上述 程式碼:

執行應用程式

按 Ctrl+F5 即可執行而不使用偵錯工具。

Visual Studio 會顯示下列對話方塊:

此專案設定為使用 SSL。為避免瀏覽器中的 SSL 警告,您可以選擇信任 IIS Express 產生的自我簽署憑證。您要信任 IIS Express SSL 憑證嗎?

如果您信任 IIS Express SSL 憑證,請選取 [是]

此時會顯示下列對話方塊:

安全性警告對話方塊

若您同意信任開發憑證,請選取 [是]

如需關於信任 Firefox 瀏覽器的資訊,請參閱 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 憑證錯誤

Visual Studio 會啟動 Kestrel 網頁伺服器,並開啟瀏覽器視窗。

Hello World! 隨即在瀏覽器中顯示。 Program.cs 檔案包含最基本但完整的應用程式。

新增 NuGet 套件

必須新增 NuGet 封裝,以支援本教學課程中使用的資料庫和診斷。

  • 在 [工具] 功能表上,選取 [NuGet 套件管理員] > [管理解決方案的 NuGet 套件]
  • 選取 [瀏覽] 索引標籤。
  • 在搜尋方塊中輸入 Microsoft.EntityFrameworkCore.InMemory,然後選取 Microsoft.EntityFrameworkCore.InMemory
  • 選取右窗格中的 [專案] 核取方塊。
  • 在 [版本] 下拉式清單中,選取可用的最新版本 7 (例如 6.0.28),然後選取 [安裝]
  • 遵循上述指示,以可用的最新版本 7 新增 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore 套件。

模型和資料庫內容類別

在專案資料夾中,使用下列程式碼建立名為 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,其整合了 Swagger 工具以產生符合 OpenAPI 規格的測試 UI:

  • NSwag:.NET 連結庫,可將 Swagger 直接整合到 ASP.NET Core 應用程式,並提供中介軟體和設定。
  • Swagger:一組開放原始碼工具,例如 OpenAPIGenerator 和 SwaggerUI,可產生遵循 OpenAPI 規格的 API 測試頁面。
  • OpenAPI 規格:根據控制器和模型內的 XML 和屬性註釋來描述 API 功能的文件。

如需搭配 ASP.NET 使用 OpenAPI 和 NSwag 的詳細資訊,請參閱使用 Swagger/OpenAPI 的 ASP.NET Core Web API 文件

安裝 Swagger 工具

  • 執行以下命令:

    dotnet add package NSwag.AspNetCore
    

上面的命令會新增 NSwag.AspNetCore 套件,其中包含用來產生 Swagger 文件和 UI 的工具。

設定 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 總管,這是提供 HTTP API 相關中繼資料的服務。 Swagger 會使用 API 總管來產生 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";
        });
    }
    
    

    前面的程式碼可讓 Swagger 中介軟體提供所產生的 JSON 文件和 Swagger UI。 Swagger 只會在開發環境中啟用。 在生產環境中啟用 Swagger 可能會公開關於 API 結構和實作的潛在敏感性詳細資料。

測試張貼資料

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]>[試用]

  • 請注意,[要求本文] 欄位包含所產生的範例格式,且會反映 API 的參數。

  • 在要求本文中,針對待辦事項輸入 JSON,而不指定選擇性的 id

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 選取 [執行]。

    具有 Post 資料的 Swagger

Swagger 會在 [執行] 按鈕下方提供 [回應] 窗格。

具有 Post 回應的 Swagger 窗格

請記下一些實用的詳細資料:

  • cURL:Swagger 提供 Unix/Linux 語法中的範例 cURL 命令,其可在命令列中搭配任何使用 Unix/Linux 語法的 Bash 殼層 (包括來自 Git for Windows 的 Git Bash) 來執行。
  • 要求 URL:Swagger UI 的 JavaScript 程式碼針對 API 呼叫所發出 HTTP 要求的簡化表示法。 實際要求可能包含標頭和查詢參數與要求本文等詳細資料。
  • 伺服器回應:包含回應本文和標頭。 回應本文顯示 id 已設定為 1
  • 回應碼:傳回 201 HTTP 狀態碼,指出已成功處理要求,並導致建立新的資源。

檢查 GET 端點

範例應用程式會藉由呼叫 MapGet 來實作數個 GET 端點:

API 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得所有已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
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} 以從特定識別碼傳回資料:

    • 選取 [GET /todoitems]>[試用]
    • 將 [識別碼] 欄位設定為 1,然後選取 [執行]
  • 或者,從瀏覽器輸入 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} 可傳回兩個不同的狀態值:

  • 如果沒有項目符合所要求的識別碼,方法會傳回 404 狀態 NotFound 錯誤碼。
  • 否則,方法會傳回 200 與 JSON 回應本文。 傳回 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();
});

此方法與 MapPost 方法類似,除了它使用 HTTP PUT。 成功的回應會傳回 204 (無內容)。 根據 HTTP 規格,PUT 要求需要用戶端傳送整個更新的實體,而不只是變更。 若要支援部分更新,請使用 HTTP PATCH

測試 PUT 端點

此範例使用在每次應用程式啟動都必須初始化的記憶體內部資料庫。 資料庫中必須有項目,您才能進行 PUT 呼叫。 在發出 PUT 呼叫之前,呼叫 GET 以確保資料庫中有項目。

更新 Id = 1 的待辦事項,並將其名稱設定為 "feed fish"

使用 Swagger 來傳送 PUT 要求:

  • 選取 [Put /todoitems/{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}]>[試用]

  • 將 [識別碼] 欄位設定為 1,然後選取 [執行]

    DELETE 要求會傳送至應用程式,而回應會顯示在 [回應] 窗格中。 回應本文是空的,而伺服器回應狀態碼是 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);
}

以下列程式碼取代 Program.cs 檔案的內容,以使用此 DTO 模型:

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

驗證您可以張貼並取得祕密欄位以外的所有欄位。

測試基本 API

如需測試基本 API 應用程式的範例,請參閱此 GitHub 範例

發佈至 Azure

如需部署至 Azure 的資訊,請參閱 快速入門:部署 ASP.NET Web 應用程式

其他資源

基本 API 的架構是要建立具有最低相依性的 HTTP API。 它們很適合只要包含 ASP.NET Core 中基本檔案、功能和相依性的微服務及應用程式。

本教學課程將教導您使用 ASP.NET Core 建立基本 API 的基本概念。 在 ASP.NET Core 中建立 API 的另一種方法是使用控制器。 如需在基本 API 與控制器型 API 之間選擇的說明,請參閱 API 概觀。 如需根據包含更多功能的控制器建立 API 專案的教學課程,請參閱建立 Web API

概觀

本教學課程會建立以下 API:

API 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
POST /todoitems 新增記錄 待辦事項 待辦事項
PUT /todoitems/{id} 更新現有的項目 待辦事項
DELETE /todoitems/{id}     刪除項目

必要條件

建立 API 專案

  • 啟動 Visual Studio 2022 並選取 [建立新專案]

  • 在 [建立新專案] 對話方塊中:

    • 在 [搜尋範本] 搜尋方塊中輸入 Empty
    • 選取 [ASP.NET Core 空白] 範本,然後選取 [下一步]

    Visual Studio 建立新專案

  • 將專案命名為 TodoApi,然後選取 [下一步]

  • 在 [其他資訊] 對話方塊中:

    • 選取 [.NET 8.0 (長期支援)]
    • 取消勾選 [不要使用最上層陳述式]
    • 選取 [建立]

    其他資訊

檢查程式碼

Program.cs 檔案包含下列程式碼:

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

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

app.Run();

上述 程式碼:

執行應用程式

按 Ctrl+F5 即可執行而不使用偵錯工具。

Visual Studio 會顯示下列對話方塊:

此專案設定為使用 SSL。為避免瀏覽器中的 SSL 警告,您可以選擇信任 IIS Express 產生的自我簽署憑證。您要信任 IIS Express SSL 憑證嗎?

如果您信任 IIS Express SSL 憑證,請選取 [是]

此時會顯示下列對話方塊:

安全性警告對話方塊

若您同意信任開發憑證,請選取 [是]

如需關於信任 Firefox 瀏覽器的資訊,請參閱 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 憑證錯誤

Visual Studio 會啟動 Kestrel 網頁伺服器,並開啟瀏覽器視窗。

Hello World! 隨即在瀏覽器中顯示。 Program.cs 檔案包含最基本但完整的應用程式。

關閉瀏覽器視窗。

新增 NuGet 套件

必須新增 NuGet 封裝,以支援本教學課程中使用的資料庫和診斷。

  • 在 [工具] 功能表上,選取 [NuGet 套件管理員] > [管理解決方案的 NuGet 套件]
  • 選取 [瀏覽] 索引標籤。
  • 在搜尋方塊中輸入 Microsoft.EntityFrameworkCore.InMemory,然後選取 Microsoft.EntityFrameworkCore.InMemory
  • 選取右窗格中的 [專案] 核取方塊,然後選取 [安裝]
  • 遵循上述指示來新增 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore 封裝。

模型和資料庫內容類別

  • 在專案資料夾中,使用下列程式碼建立名為 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。

測試張貼資料

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 端點將資料新增至應用程式。

  • 選取 [檢視] > [其他視窗] > [端點總管]

  • 以滑鼠右鍵按一下 [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 描述 要求本文 回應本文
GET /todoitems 取得所有待辦事項 待辦事項的陣列
GET /todoitems/complete 取得所有已完成的待辦事項 待辦事項的陣列
GET /todoitems/{id} 依識別碼取得項目 待辦事項
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 要求。

傳回值

ASP.NET Core 會自動將物件序列化為 JSON,並將 JSON 寫入至回應訊息的本文。 此傳回型別的回應碼為 200 OK,假設沒有任何未處理的例外狀況。 未處理的例外狀況會轉譯成 5xx 錯誤。

傳回型別可代表各種 HTTP 狀態碼。 例如,GET /todoitems/{id} 可傳回兩個不同的狀態值:

  • 如果沒有項目符合所要求的識別碼,方法會傳回 404 狀態 NotFound 錯誤碼。
  • 否則,方法會傳回 200 與 JSON 回應本文。 傳回 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();
});

此方法與 MapPost 方法類似,除了它使用 HTTP PUT。 成功的回應會傳回 204 (無內容)。 根據 HTTP 規格,PUT 要求需要用戶端傳送整個更新的實體,而不只是變更。 若要支援部分更新,請使用 HTTP PATCH

測試 PUT 端點

此範例使用在每次應用程式啟動都必須初始化的記憶體內部資料庫。 資料庫中必須有項目,您才能進行 PUT 呼叫。 在發出 PUT 呼叫之前,呼叫 GET 以確保資料庫中有項目。

更新 Id = 1 的待辦事項,並將其名稱設定為 "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) 的單一呼叫來自訂整個端點群組。

以下列程式碼取代 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

傳回 TypedResults 而不是 Results 有幾個優點,包括可測試性,並自動傳回 OpenAPI 的回應型別中繼資料來描述端點。 如需詳細資訊,請參閱 TypedResults 與 Results

Map<HttpVerb> 方法可以呼叫路由處理常式方法,而不使用 Lambda。 若要查看範例,請使用下列程式碼更新 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> 程式碼現在會呼叫方法,而不是 Lambda:

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

以下列程式碼取代 Program.cs 檔案的內容,以使用此 DTO 模型:

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

驗證您可以張貼並取得祕密欄位以外的所有欄位。

使用已完成的範例進行疑難排解

若您遇到無法解決的問題,請將您的程式碼與已完成的專案進行比較。 檢視或下載已完成的專案 (如何下載)。

下一步

深入了解

請參閱基本 API 快速參考