Home [C# 筆記] 使用 ASP.NET Core 建立 Web API
Post
Cancel

[C# 筆記] 使用 ASP.NET Core 建立 Web API

使用 ASP.NET Core 建立 Web API

MSDN - 教學課程:使用 ASP.NET Core 建立 Web API

概觀

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

1
2
3
4
5
6
API	描述	要求本文	回應本文
GET /api/todoitems	取得所有待辦事項	無	待辦事項的陣列
GET /api/todoitems/{id}	依識別碼取得項目	無	待辦事項
POST /api/todoitems	新增記錄	待辦事項	待辦事項
PUT /api/todoitems/{id}	更新現有的項目	待辦事項	無
DELETE /api/todoitems/{id}    	刪除項目	無	無

下圖顯示應用程式的設計。

architecture

建立 Web 專案

Visual Studio

  • 從 [檔案] 功能表選取 [新增] >[專案]
  • 在搜尋方塊中輸入 Web API。
  • 選取 ASP.NET Core Web API 範本,然後選取 [下一步]。
  • 在 [設定新專案] 對話方塊中,將專案命名為 TodoApi,然後選取 [下一步]。
  • 在 [其他資訊] 對話方塊中:
    • 確認架構為 .NET 8.0(長期支援)。
    • 確認已核取 [使用控制器 (取消核取以使用最小 API)] 核取方塊。
    • 確認已核取 [啟用 OpenAPI 支援] 核取方塊。
    • 選取建立。

新增 NuGet 套件

必須新增 NuGet 套件,才能支援本教學課程中使用的資料庫。

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

測試專案

專案範本會建立支援 Swagger 的 WeatherForecast API。

Visual Studio

Ctrl+F5 即可執行而不使用偵錯工具。
當專案尚未設定為使用 SSL 時,Visual Studio 會顯示下列對話方塊:
ssl

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

若您同意信任開發憑證,請選取 [是]。
Visual Studio 會啟動預設瀏覽器並瀏覽至 https://localhost:/swagger/index.html,其中 是專案建立時設定的隨機選擇連接埠號碼。

Swagger 頁面

Swagger 頁面 /swagger/index.html 隨即顯示。 選取 [GET]>[試用]>[執行]。 頁面會顯示:

  • 用來測試 WeatherForecast API 的 Curl 命令。
  • 用來測試 WeatherForecast API 的 URL。
  • 回應碼、本文和標頭。
  • 具有媒體類型與範例值和架構的下拉式清單方塊。

Swagger 可用來為 Web API 產生有用的文件和說明頁面。 本教學課程會使用 Swagger 來測試應用程式。

複製並貼上瀏覽器中的要求 URL:https://localhost:/weatherforecast 系統會傳回類似下列範例的 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[
    {
        "date": "2019-07-16T19:04:05.7257911-06:00",
        "temperatureC": 52,
        "temperatureF": 125,
        "summary": "Mild"
    },
    {
        "date": "2019-07-17T19:04:05.7258461-06:00",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2019-07-18T19:04:05.7258467-06:00",
        "temperatureC": 39,
        "temperatureF": 102,
        "summary": "Cool"
    },
    {
        "date": "2019-07-19T19:04:05.7258471-06:00",
        "temperatureC": 10,
        "temperatureF": 49,
        "summary": "Bracing"
    },
    {
        "date": "2019-07-20T19:04:05.7258474-06:00",
        "temperatureC": -1,
        "temperatureF": 31,
        "summary": "Chilly"
    }
]

新增模型類別

「模型」是代表應用程式所管理資料的一組類別。 此應用程式的模型是 TodoItem 類別。

Visual Studio

  • 在 [方案總管] 中,以滑鼠右鍵按一下專案。 選取 [新增]>[新增資料夾]。 將資料夾命名為 Models註冊免費試用帳戶。
  • 以滑鼠右鍵按一下 Models 資料夾並選取 [新增]>[類別]。 將類別命名為 TodoItem,然後選取 [新增]。
  • 以下列項目取代範本程式碼:
1
2
3
4
5
6
7
8
9
namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

Id 屬性的功能相當於關聯式資料庫中的唯一索引鍵。
模型類別可位於專案中的任何位置,但依照慣例會使用 Models 資料夾。

新增資料庫內容

「資料庫內容」是為資料模型協調 Entity Framework 功能的主要類別。 此類別是透過衍生自 Microsoft.EntityFrameworkCore.DbContext 類別來建立。

Visual Studio

以滑鼠右鍵按一下 Models 資料夾並選取 [新增]>[類別]。 將類別命名為 TodoContext,然後按一下 [新增]。
輸入下列程式碼:

1
2
3
4
5
6
7
8
9
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models
{
    public class TodoContext: DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options) : base(options) { }
        public DbSet<TodoItem> TodoItems { get; set; } = null!;
    }
}

登錄資料庫內容

在 ASP.NET Core 中,資料庫內容等服務必須向相依性插入 (DI) 容器註冊。 此容器會將服務提供給控制器。
使用下列醒目提示的程式碼更新 Program.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//1.加入 using 指示詞。
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

//2.將資料庫內容新增至 DI 容器。指定資料庫內容將會使用記憶體內部資料庫。
builder.Services.AddDbContext<TodoContext>(opt => 
    opt.UseInMemoryDatabase("TodoList"));  //Assembly:Microsoft.EntityFrameworkCore.InMemory.dll

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

上述 程式碼:

  • 加入 using 指示詞。
  • 將資料庫內容新增至 DI 容器。
  • 指定資料庫內容將會使用記憶體內部資料庫。

Scaffold 控制器

Visual Studio

以滑鼠右鍵按一下 Controllers 資料夾。

  • 選取 [新增]> [新增 Scaffold 項目]。
  • 選取 [使用 Entity Framework 執行動作的 API 控制器],然後選取 [新增]。
  • 在 [使用 Entity Framework 執行動作的 API 控制器] 對話方塊中:
    • 在 [模型類別] 中選取 [TodoItem (TodoApi.Models)]。
    • 在 [資料內容類別] 中選取 [TodoContext (TodoApi.Models)]。
    • 選取新增。 如果 Scaffolding 作業失敗,請選取 [新增] 以嘗試第二次 Scaffolding。

產生的程式碼:

  • 使用 [ApiController] 屬性標記類別。 這個屬性表示控制器會回應 Web API 要求。 如需屬性啟用的特定行為相關資訊,請參閱 使用 ASP.NET Core 建立 Web API。
  • 使用 DI 將資料庫內容 (TodoContext) 插入到控制器中。 控制器中的每一個 CRUD 方法都會使用資料庫內容。

ASP.NET Core 範本適用於:

  • 路由範本中檢視包含 [action] 的控制器。
  • API 控制器在路由範本中不包含 [action]。 當 [action] 權杖不在路由範本中時,動作名稱 (方法名稱) 不會包含在端點中。 也就是說,動作的相關聯方法名稱不會用於比對路由。

更新 PostTodoItem 建立方法

更新 PostTodoItem 中的 return 陳述式,以使用 nameof 運算子:

1
2
3
4
5
6
7
8
9
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();

    //    return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
    return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}

上述程式碼是 HTTP POST 方法,以 [HttpPost] 屬性表示。 該方法會從 HTTP 要求本文取得 TodoItem 的值。

CreatedAtAction 方法:

  • 成功時會傳回 HTTP 201 狀態碼。 對於可在伺服器上建立新資源的 HTTP POST 方法,HTTP 201 是標準回應。
  • 將 Location標頭新增到回應。 Location 標頭會指定新建待辦事項的 URI。
  • 參考 GetTodoItem 動作以建立 Location 標頭的 URI。 C# nameof 關鍵字是用來避免在 CreatedAtAction 呼叫中以硬式編碼方式寫入動作名稱。

測試 PostTodoItem

  • 按 Ctrl+F5 執行應用程式。
  • 在 Swagger 瀏覽器視窗中,選取 [POST /api/TodoItems],然後選取 [試用]。
  • 在 [要求本文] 輸入視窗中,更新 JSON。 例如:
1
2
3
4
{
  "name": "walk dog",
  "isComplete": true
}

選取 [執行]

post

測試位置標頭 URI

在上述 POST 中,Swagger UI 會顯示 [回應] 標頭底下的位置標頭。 例如: location: https://localhost:7260/api/TodoItems/1 。 位置標頭會顯示所建立資源的 URI。

若要測試位置標頭:

  • 在 Swagger 瀏覽器視窗中,選取 [GET /api/TodoItems/{id}],然後選取 [試用]。
  • 在 id 輸入方塊中輸入 1,然後選取 [執行]。

get

檢查 GET 方法

兩個 GET 端點即會實作:

  • GET /api/todoitems
  • GET /api/todoitems/{id} 上一節顯示 /api/todoitems/{id} 路由的範例。

依照 POST 指示新增另一個待辦事項,然後使用 Swagger 測試 /api/todoitems 路由。

這個應用程式會使用記憶體內部資料庫。 如果應用程式在停止後再啟動,上述 GET 要求將不會傳回任何資料。 如果沒有傳回任何資料,請將資料 POST 到應用程式。

傳送和 URL 路徑

[HttpGet] 屬性代表回應 HTTP GET 要求的方法。 每個方法的 URL 路徑的建構方式如下:

  • 一開始在控制器的 Route 屬性中使用範本字串:
1
2
3
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
  • 以控制器的名稱取代 [controller],也就是將控制器類別名稱減去 “Controller” 字尾。 在此範例中,控制器類別名稱是 TodoItemsController,因此控制器名稱是 “TodoItems”。 ASP.NET Core 路由不區分大小寫。
  • 如果 [HttpGet] 屬性具有路由範本 (例如 [HttpGet(“products”)]),請將其附加到路徑。 此範例不使用範本。 如需詳細資訊,請參閱使用 Http[Verb] 屬性的屬性路由

在下列 GetTodoItem 方法中,”{id}” 是待辦事項唯一識別碼的預留位置變數。 在叫用 GetTodoItem 時,會將 URL 中的 “{id}” 值提供給方法的 id 參數。

1
2
3
4
5
6
7
8
9
10
11
12
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}

傳回值

GetTodoItems 和 GetTodoItem 方法的傳回型別為 ActionResult類型 ([ActionResult Type](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/action-return-types?view=aspnetcore-8.0#actionresultt-type))。 ASP.NET Core 會自動將物件序列化為 JSON,並將 JSON 寫入至回應訊息的本文。 此傳回型別的回應碼為 200 OK,假設沒有任何未處理的例外狀況。 未處理的例外狀況會轉譯成 5xx 錯誤。

ActionResult 傳回型別可代表各種 HTTP 狀態碼。 例如,GetTodoItem 可傳回兩個不同的狀態值: 如果沒有項目符合所要求的識別碼,方法會傳回 404 狀態NotFound錯誤碼。 否則,方法會傳回 200 與 JSON 回應本文。 傳回 HTTP 200 回應中的 item 結果。

PutTodoItem 方法

檢查 PutTodoItem 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id)
    {
        return BadRequest();
    }

    _context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoItemExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}

PutTodoItem 類似於 PostTodoItem,但是會使用 HTTP PUT。 回應是 204 (No Content) (204 (沒有內容))。 根據 HTTP 規格,PUT 要求需要用戶端傳送整個更新的實體,而不只是變更。 若要支援部分更新,請使用 HTTP PATCH。

測試 PutTodoItem 方法

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

使用 Swagger UI,使用 PUT 按鈕來更新識別碼為 1 的 TodoItem,並將其名稱設定為 “feed fish”。 請注意,回應為 HTTP 204 No Content。

DeleteTodoItem 方法

檢查 DeleteTodoItem 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todoItem);
    await _context.SaveChangesAsync();

    return NoContent();
}

測試 DeleteTodoItem 方法

使用 Swagger UI 刪除識別碼為 1 的 TodoItem。 請注意,回應為 HTTP 204 No Content。

程式碼

TodoItem.cs

1
2
3
4
5
6
7
8
9
namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set;}
    }
}

TodoContext.cs

1
2
3
4
5
6
7
8
9
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models
{
    public class TodoContext: DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options) : base(options) { }
        public DbSet<TodoItem> TodoItems { get; set; } = null!;
    }
}

Program.cs

  1. 加入 using 指示詞。
    1
    2
    
    using Microsoft.EntityFrameworkCore;
    using TodoApi.Models;
    
  2. 將資料庫內容新增至 DI 容器。指定資料庫內容將會使用記憶體內部資料庫。
1
2
builder.Services.AddDbContext<TodoContext>(opt => 
    opt.UseInMemoryDatabase("TodoList"));  //Assembly:Microsoft.EntityFrameworkCore.InMemory.dll

完整 程式碼Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//1.加入 using 指示詞。
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

//2.將資料庫內容新增至 DI 容器。指定資料庫內容將會使用記憶體內部資料庫。
builder.Services.AddDbContext<TodoContext>(opt => 
    opt.UseInMemoryDatabase("TodoList"));  //Assembly:Microsoft.EntityFrameworkCore.InMemory.dll

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

TodoItemsController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoItemsController(TodoContext context)
        {
            _context = context;
        }

        // GET: api/TodoItems
        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
        {
            return await _context.TodoItems.ToListAsync();
        }

        // GET: api/TodoItems/5
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return todoItem;
        }

        // PUT: api/TodoItems/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        {
            if (id != todoItem.Id)
            {
                return BadRequest();
            }

            _context.Entry(todoItem).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!TodoItemExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/TodoItems
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
        {
            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            //return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
            return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id}, todoItem);
        }

        // DELETE: api/TodoItems/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return NotFound();
            }

            _context.TodoItems.Remove(todoItem);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool TodoItemExists(long id)
        {
            return _context.TodoItems.Any(e => e.Id == id);
        }
    }
}
This post is licensed under CC BY 4.0 by the author.