iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 26 篇

Day 26|Controller API 與 Routing

  • 分享至 

  • xImage
  •  

Day 25 我們已經知道,可以用:

HTTP Method 、 URL

描述一支 API。

例如:

GET /api/products/10

代表:

取得 Product 10。

今天要繼續理解:

這個 HTTP Request 進入 ASP.NET Core 後,怎麼知道要執行哪一個 C# Method?

這就是:

Routing

今天先記住整體流程:

HTTP Request
↓
Routing
↓
Controller Action
↓
執行程式邏輯
↓
Action Result
↓
HTTP Response

例如:

GET /api/products/10
↓
Routing
↓
ProductsController.GetById(...)
↓
查詢 Product
↓
Ok(product)
↓
200 OK

1. 今天要完成的 Product API

假設我們要建立 Product API:

功能 Method URL
取得所有商品 GET /api/products
取得單一商品 GET /api/products/10
建立商品 POST /api/products
更新商品 PUT /api/products/10
刪除商品 DELETE /api/products/10

今天的工作,就是把這些 HTTP API 對應到 ASP.NET Core 的 Controller Action。


2. 建立 Controller-based Web API

建立專案:

dotnet new webapi -n MyApi --use-controllers

進入專案:

cd MyApi

使用 VS Code:

code .

今天先專注三個角色:

Controller
Routing
Action

3. Program.cs:讓 Application 支援 Controller

先看:

var builder =
    WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app =
    builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

今天先注意兩行。

builder.Services.AddControllers();

可以先理解成:

註冊 Controller 需要的相關功能。

而:

app.MapControllers();

可以先理解成:

讓 Controller 定義的 Route 可以成為 Application 能匹配的 API Endpoint。

至於:

Dependency Injection
Middleware
Endpoint Routing

今天先不深入,後面會再分別介紹。


4. 建立 ProductsController

建立:

Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;

namespace MyApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController
    : ControllerBase
{
}

先理解幾個角色:

ProductsController
→ 管理 Product 相關 API

ControllerBase
→ Web API Controller 常用的 Base Class

[ApiController]
→ 啟用 API Controller 相關行為

[Route(...)]
→ 定義 Controller Route

Controller 與 Action

Controller 可以理解成:

管理一組相關 API 的入口。

例如:

Product
→ ProductsController

Order
→ OrdersController

而 Controller 裡真正處理 Request 的 C# Method,稱為:

Action

例如:

GetAll()
GetById()
Create()
Update()
Delete()

所以:

Controller
→ 管理一組 API

Action
→ 真正處理 Request

5. Controller Route 是怎麼來的?

看:

[Route("api/[controller]")]

其中:

[controller]

會依 Controller 名稱替換。

例如:

ProductsController
↓
Products

因此:

[Route("api/[controller]")]

會形成:

/api/products

這就是 ProductsController 的 Base Route。


6. 第一支 API:GET /api/products

加入第一個 Action:

[HttpGet]
public IActionResult GetAll()
{
    return Ok();
}

現在:

Controller Route
→ /api/products

HTTP Method
→ GET

因此:

GET /api/products
↓
GetAll()

其中:

[HttpGet]

表示:

這個 Action 處理 GET Request。

如果再加入:

[HttpPost]
public IActionResult Create()
{
    return Ok();
}

則:

GET /api/products
→ GetAll()

POST /api/products
→ Create()

可以看到,同一個 URL 可以因為 HTTP Method 不同,而進入不同 Action。

因此 Routing 的核心可以先記成:

HTTP Method
+
Route
↓
Action

7. GET /api/products/10 怎麼找到 Action?

接著加入:

[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
    return Ok();
}

假設 Client 發出:

GET /api/products/10

ASP.NET Core 需要找出:

哪個 Action 可以處理這個 Request?

整個過程可以分成三步。


Step 1:讀取 Request

Request:

GET /api/products/10

包含:

HTTP Method
→ GET

Path
→ /api/products/10

Step 2:組合 Controller Route 與 Action Route

Controller:

[Route("api/[controller]")]

得到:

/api/products

Action:

[HttpGet("{id:int}")]

表示:

HTTP Method
→ GET

Action Route
→ /{id:int}

組合後:

GET /api/products/{id:int}

這代表:

這個 Action 可以接受什麼樣的 Request。


Step 3:比對 Request

Request:

GET /api/products/10

Action 可以接受:

GET /api/products/{id:int}

比對:

GET
→ Method 符合

/api/products
→ Route 符合

10
→ 符合 {id:int}

因此找到:

GetById(int id)

所以 Routing 可以理解成:

根據 HTTP Method 與 Route,找到可以處理這個 Request 的 Action。


8. {id:int} 是什麼?

看:

[HttpGet("{id:int}")]

可以拆成:

{id}
→ Route Parameter

:int
→ Route Constraint

Route Parameter

{id}

表示這一段 Route 是動態值。

例如:

/api/products/1
/api/products/10
/api/products/100

都可以符合:

/api/products/{id}

以:

/api/products/10

來說:

id
→ 10

這個 10 會被辨識成 Route Value。


Route Constraint

如果寫成:

{id:int}

代表 Route Value 必須符合整數格式。

所以:

GET /api/products/10
→ 符合

但:

GET /api/products/abc
→ 不符合

要注意:

Route Constraint 是協助 Routing 判斷 Route 是否匹配,不是完整的資料驗證。


9. Routing 還有兩個重要觀念

Action 名稱不決定 Routing

假設把:

GetById(int id)

改成:

[HttpGet("{id:int}")]
public IActionResult FindProduct(int id)
{
    return Ok();
}

它仍然可以處理:

GET /api/products/10

因為 Routing 真正看的不是:

GetById
FindProduct

而是:

HTTP Method
+
Route

所以:

在 Attribute Routing 中,Action 名稱本身不是 Routing 規則。


Routing 和 Model Binding 不一樣

現在我們知道:

GET /api/products/10

會找到:

GetById(int id)

但:

Route Value 10
↓
怎麼進入
int id?

這是:

Model Binding

負責的工作。

所以可以簡單區分:

Routing
→ 找「誰來處理」

Model Binding
→ 把 Request Data
   放進 Action Parameter

Day 27 會正式介紹 Model Binding。


10. 加入 Product 資料

建立:

Models/Product.cs
namespace MyApi.Models;

public record Product(
    int Id,
    string Name,
    decimal Price
);

再建立:

Models/ProductRequests.cs
namespace MyApi.Models;

public record CreateProductRequest(
    string Name,
    decimal Price
);

public record UpdateProductRequest(
    string Name,
    decimal Price
);

目前先理解:

Product
→ Product 資料

CreateProductRequest
→ 建立 Product 需要的資料

UpdateProductRequest
→ 更新 Product 需要的資料

DTO 與 Model Binding 留到 Day 27 再深入。


暫時使用 List<Product>

在 Controller 裡加入:

private static readonly List<Product>
    Products =
    new List<Product>
    {
        new Product(
            1,
            "Keyboard",
            2000m
        ),
        new Product(
            2,
            "Mouse",
            1000m
        )
    };

private static int nextId = 3;

今天使用 List<Product> 只是模擬資料來源。

暫時不加入:

EF Core
DbContext
Database

避免分散今天的主題。


11. 讓 GET API 真正處理資料

取得所有 Product

[HttpGet]
public ActionResult<List<Product>>
    GetAll()
{
    return Ok(Products);
}

這裡第一次看到:

ActionResult<List<Product>>

可以先理解成:

這支 Action 主要會回傳 List<Product>,但也可以回傳其他 HTTP Result。

流程:

GET /api/products
↓
Routing
↓
GetAll()
↓
Ok(Products)
↓
200 OK

Response Body:

[
  {
    "id": 1,
    "name": "Keyboard",
    "price": 2000
  },
  {
    "id": 2,
    "name": "Mouse",
    "price": 1000
  }
]

取得指定 Product

[HttpGet("{id:int}")]
public ActionResult<Product>
    GetById(int id)
{
    Product? product =
        Products.FirstOrDefault(
            product =>
                product.Id == id
        );

    if (product is null)
    {
        return NotFound();
    }

    return Ok(product);
}

這裡重新用到之前學過的:

LINQ
→ FirstOrDefault()

Nullable
→ Product?

Pattern Matching
→ is null

如果找到:

Ok(product)
→ 200 OK

如果找不到:

NotFound()
→ 404 Not Found

12. Action 執行完,要怎麼回應 Client?

現在看:

GetById(int id)

Action 執行後,可能有兩種結果:

找到 Product

或:

找不到 Product

但 API 還需要把程式執行結果轉成:

HTTP Response

例如:

找到 Product
→ 200 OK
→ 回傳 Product

找不到 Product
→ 404 Not Found

所以 Action 除了執行程式邏輯,最後還要:

決定這次 Request 應該怎麼回應 Client。


ControllerBase 提供 Response Helper

因為:

ProductsController : ControllerBase

所以可以直接使用:

Ok()
NotFound()
BadRequest()
NoContent()
CreatedAtAction()

可以把它們理解成:

協助建立 HTTP 回應結果的 Helper Method。

例如:

return Ok(product);

表示:

處理成功
↓
200 OK
↓
Response Body 放入 product

而:

return NotFound();

表示:

找不到 Resource
↓
404 Not Found

13. 什麼是 Action Result?

像:

Ok(product)

或:

NotFound()

會建立一個:

Action Result

可以先把 Action Result 理解成:

Action 告訴 ASP.NET Core:「這次 Request 應該怎麼回應 Client。」

例如:

return Ok(product);

流程:

Action
↓
Ok(product)
↓
Action Result
↓
ASP.NET Core
↓
HTTP Response

假設 product:

Id = 1
Name = Keyboard
Price = 2000

Client 最後可能收到:

200 OK

Body:

{
  "id": 1,
  "name": "Keyboard",
  "price": 2000
}

ASP.NET Core 會協助把 C# Object 轉成 Response Body,例如 JSON。


常見的 Action Result

Controller Method HTTP Response 常見情境
Ok(data) 200 OK + Body 成功取得資料
CreatedAtAction(...) 201 Created 成功建立 Resource
NoContent() 204 No Content 成功,但不需要 Body
BadRequest() 400 Bad Request Request 有問題
NotFound() 404 Not Found 找不到 Resource

所以完整流程就是:

HTTP Request
↓
Routing
↓
Controller Action
↓
執行程式邏輯
↓
Action Result
↓
HTTP Response

14. 其他 CRUD 也是同一套規則

理解 GET 後,其他 Method 不需要重新學 Routing。

整理如下:

API Attribute Action
GET /api/products [HttpGet] GetAll()
GET /api/products/10 [HttpGet("{id:int}")] GetById(10)
POST /api/products [HttpPost] Create(...)
PUT /api/products/10 [HttpPut("{id:int}")] Update(10, ...)
DELETE /api/products/10 [HttpDelete("{id:int}")] Delete(10)

它們的 Routing 都是:

HTTP Method
+
Route
↓
Action

接下來只需要完成各自的 Action 邏輯。


15. POST、PUT、DELETE

POST

[HttpPost]
public ActionResult<Product>
    Create(
        CreateProductRequest request
    )
{
    Product product =
        new Product(
            nextId,
            request.Name,
            request.Price
        );

    nextId++;

    Products.Add(product);

    return CreatedAtAction(
        nameof(GetById),
        new
        {
            id = product.Id
        },
        product
    );
}

對應:

POST /api/products
↓
Create(request)
↓
201 Created

CreatedAtAction() 可以先理解成:

201 Created
+
新建立的 Product
+
新 Resource 的 Location

例如新 Product 的 Id 是 3:

GET /api/products/3

之後就可以取得它。


PUT

[HttpPut("{id:int}")]
public IActionResult Update(
    int id,
    UpdateProductRequest request
)
{
    int index =
        Products.FindIndex(
            product =>
                product.Id == id
        );

    if (index == -1)
    {
        return NotFound();
    }

    Product current =
        Products[index];

    Products[index] =
        current with
        {
            Name = request.Name,
            Price = request.Price
        };

    return NoContent();
}

成功:

PUT /api/products/10
↓
Update(...)
↓
204 No Content

DELETE

[HttpDelete("{id:int}")]
public IActionResult Delete(int id)
{
    int index =
        Products.FindIndex(
            product =>
                product.Id == id
        );

    if (index == -1)
    {
        return NotFound();
    }

    Products.RemoveAt(index);

    return NoContent();
}

成功:

DELETE /api/products/10
↓
Delete(...)
↓
204 No Content

16. 完整 ProductsController

using Microsoft.AspNetCore.Mvc;
using MyApi.Models;

namespace MyApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController
    : ControllerBase
{
    private static readonly List<Product>
        Products =
        new List<Product>
        {
            new Product(
                1,
                "Keyboard",
                2000m
            ),
            new Product(
                2,
                "Mouse",
                1000m
            )
        };

    private static int nextId = 3;

    [HttpGet]
    public ActionResult<List<Product>>
        GetAll()
    {
        return Ok(Products);
    }

    [HttpGet("{id:int}")]
    public ActionResult<Product>
        GetById(int id)
    {
        Product? product =
            Products.FirstOrDefault(
                product =>
                    product.Id == id
            );

        if (product is null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    [HttpPost]
    public ActionResult<Product>
        Create(
            CreateProductRequest request
        )
    {
        Product product =
            new Product(
                nextId,
                request.Name,
                request.Price
            );

        nextId++;

        Products.Add(product);

        return CreatedAtAction(
            nameof(GetById),
            new
            {
                id = product.Id
            },
            product
        );
    }

    [HttpPut("{id:int}")]
    public IActionResult Update(
        int id,
        UpdateProductRequest request
    )
    {
        int index =
            Products.FindIndex(
                product =>
                    product.Id == id
            );

        if (index == -1)
        {
            return NotFound();
        }

        Product current =
            Products[index];

        Products[index] =
            current with
            {
                Name = request.Name,
                Price = request.Price
            };

        return NoContent();
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        int index =
            Products.FindIndex(
                product =>
                    product.Id == id
        );

        if (index == -1)
        {
            return NotFound();
        }

        Products.RemoveAt(index);

        return NoContent();
    }
}

17. ActionResult<T> 與 IActionResult

最後簡單整理今天看到的兩種 Return Type。

ActionResult<T>

例如:

public ActionResult<Product>
    GetById(int id)

表示這支 Action 主要會回傳:

Product

但也可能回:

404 Not Found

所以可以先理解成:

有主要回傳資料型別,但也可能回其他 HTTP Result。


IActionResult

例如:

public IActionResult Delete(int id)

可能回:

204 No Content

或:

404 Not Found

沒有固定的 Response Body 型別。

今天掌握這個差異即可。


18. 如何快速看懂任何 Controller?

以後看到陌生 Controller,可以固定按照這個順序:

① 看 Controller Route
↓
② 看 HTTP Method
↓
③ 看 Action Route
↓
④ 組成完整 API
↓
⑤ 判斷 Request 能不能匹配
↓
⑥ 找到 Action
↓
⑦ 看 Action 做什麼
↓
⑧ 看最後回什麼 Result

例如:

[ApiController]
[Route("api/[controller]")]
public class OrdersController
    : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult Find(int id)
    {
        return Ok();
    }
}

拆解:

Controller Route
→ /api/orders

HTTP Method
→ GET

Action Route
→ /{id:int}

組合:

GET /api/orders/{id:int}

所以:

GET /api/orders/10

可以匹配:

Find(int id)

最後:

return Ok();

代表:

200 OK

這就是閱讀 Controller 最實用的方法。

Day 26 小結

今天真正要理解的是一個完整流程:

HTTP Request
↓
Routing
↓
Controller Action
↓
執行程式邏輯
↓
Action Result
↓
HTTP Response

其中:

Routing
→ 決定「誰來處理」

例如:

GET /api/products/10
↓
GET /api/products/{id:int}
↓
GetById(...)

Action:

執行真正的程式邏輯

例如:

查詢 Product

Action Result:

決定「處理完之後怎麼回應」

例如:

Ok(product)
→ 200 OK

NotFound()
→ 404 Not Found

最後還要記住:

Routing
→ 找到 Action

Model Binding
→ 把 Request Data
   放進 Action Parameter

整篇可以濃縮成三個問題:

Routing:這個 Request 該交給誰處理?

Action:收到 Request 後要做什麼?

Action Result:處理完之後要怎麼回應 Client?


上一篇
Day 25|ASP.NET Core 入門:從瀏覽器到 Server,理解 HTTP、Request、Response 與 REST
下一篇
Day 27|Model Binding、DTO 與 Validation
系列文
現在就學C# 與 ASP.NET Core 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言