MODEL Binding
這一切其實都圍繞著一個核心:溝通與理解
HTTP 的世界裡,Request 扮演著使用者「開口說話」的角色,而 Model Binding 則像是一位貼心的翻譯員,幫我們把這些話語準確地轉換成程式可以理解的變數或物件

商品搜尋 + 分頁
1 | GET /products?keyword=phone&page=2&pageSize=20 |
設定
1 | [] |
結果
1 | { "keyword": "phone", "page": 2, "pageSize": 20 } |
因為這是「篩選條件」,而不是資源唯一識別。用 QueryString 比較直觀、可被書籤儲存

取得某個商品詳細資訊
1 | GET /products/12345 |
設定
1 | [] |
結果
1 | { "Id": 12345, "Name": "iPhone 15 Pro" } |
因為 id=12345 就是這個資源的唯一,應該放在路由裡,而不是 QueryString

注意事項
找不到就回 404,不要回 200 + 空物件
路由參數要有型別約束與格式限制,例如只接受整數可以用 {id:int};如果是 GUID:用 {id:guid}。這樣亂打 /products/abc 會更乾淨地被 routing 擋掉
ID 是「定位資源」,Query 是「調整視角/篩選」,GET /products/12345?fields=name,price、?lang=zh-TW、?include=reviews 這種是合理的:資源還是同一個,只是回傳內容或視角不同;但用 Query 來表達「是哪一個商品」會讓語意變混亂。
不要把可猜的 id 當成授權機制,如果商品是公開沒問題;但如果是「訂單 / 發票 / 個資」,使用者改個 id 就能看到別人的資料就是嚴重漏洞(IDOR)。需要做權限檢查或用不可猜的識別(例如 UUID/隨機 token)。
快取與條件式請求,可以考慮 ETag/Last-Modified,或 CDN cache;路由式的資源 URL 天然比較適合快取(相對於一堆複雜 query 組合)。

註冊會員
1 | <form action="/account/register" method="post"> |
設定
1 | [] |
結果
1 | { "username": "kevin88", "email": "kevin@example.com" } |
因為 HTML form 提交(application/x-www-form-urlencoded)就是最典型的來源
流程
- 瀏覽器用
- ASP.NET Core 看到這種內容格式 → 從 Body 解析成 key/value
- Model Binding 把 username/email/password 塞進 action 參數([FromForm])
- 你的程式做驗證、建立帳號、存資料庫
- 回傳結果(成功/失敗)
注意事項
用 DTO 收參數,集中驗證規則, 因為當把欄位散落在參數上會讓驗證規則越寫越亂,用一個 RegisterRequest 才好維護也好加欄位。DTO 可以搭配 DataAnnotations 或 FluentValidation,一次處理完整輸入
錯誤訊息要避免「帳號枚舉」,對外回應用較模糊、統一的訊息(例如「註冊失敗」),細節只記在 server log 或用內部 error code,至少避免一眼就能判斷某 email 是否已被註冊。
速率限制與機器人防護要先想,加上 IP/user-agent 速率限制、CAPTCHA、email 驗證、黑名單/風控,至少確保 DB 不會被灌爆,信箱也不會被濫發驗證信
CSRF:只要你用 Cookie 認證就要注意,表單天生容易被「別的網站代替你送出」,Cookie 又會自動跟著送,CSRF 就是這樣來的。純註冊通常不依賴既有登入狀態,但很多系統會把註冊和「自動登入 / 綁定推薦人 / 綁定購物車」混在一起,這時 CSRF 就變得重要;若是 MVC/Razor 常會加 Anti-Forgery token。

上傳大頭照
1 | <form action="/account/upload-avatar" method="post" enctype="multipart/form-data"> |
設定
1 | [] |
因為二進位檔案內容不能用 JSON/Query 表示,必須用 multipart/form-data。ASP.NET Core 會自動把它轉成 IFormFile
流程
- 用 multipart 收到檔案 → 綁成 IFormFile avatar
- 從 avatar 取得檔名/大小/內容流
- 決定存到哪(路徑、命名、權限)
- 寫入磁碟(或轉存到 S3/Blob)
- 回傳結果(通常回檔案 id / URL,而不是原始檔名)

注意事項
不要用 avatar.FileName 當真正檔名,使用者給的檔名只能拿來顯示,拿來存檔就等於把你的路徑控制權交出去。
因為 FileName 可能含有奇怪字元、路徑片段、甚至刻意碰撞同名覆蓋,建議自己產生安全檔名(GUID/雪花 ID),原檔名只當 metadata路徑要固定在你允許的資料夾內,我們要做的是「把檔案放進盒子」,不是「讓對方決定你電腦哪裡可以寫」。要避免 directory traversal(../)類型的路徑穿越,使用固定根目錄 + 你產生的檔名,再用 Path.GetFullPath 檢查仍在根目錄下更保險
驗證檔案類型要看內容,不要只看副檔名,因為副檔名跟 Content-Type 都可以偽造,只有檔案內容才比較有參考價值。至少做「白名單」,只允許常見圖片(jpg/png/webp),並檢查 magic bytes / 嘗試用圖片 library decode,解不開就拒絕
一定要限制大小,避免被塞爆或拖垮,如果不設上限,就是在邀請別人用超大檔案把你 IO、記憶體或磁碟打到停機,要限制單檔大小、總配額、頻率(rate limit),回應要清楚(413 Payload Too Large),ASP.NET Core 也有 request body/ multipart 限制可設定。
wwwroot 要小心,放進去就可能變成公開檔案,當把檔案存進 wwwroot,等於預設「任何人知道路徑就能直接下載/執行」,這通常不是你想要的。頭像通常可以公開,但你還是要確保只會是「安全的圖片格式」,並避免讓上傳內容被當成可執行腳本;更安全的作法是存到非公開資料夾,再用受控 API 讀出。
用 async 並正確釋放資源,因為上傳是 IO 密集,不用 async 你會白白卡住 thread,流量一上來就變慢。用 CopyToAsync、FileStream 設 useAsync: true,並加上 CancellationToken,讓客戶端中斷時能停止寫入
授權與綁定:誰可以上傳、上傳給誰,如果不做授權與歸屬檢查,上傳端點很容易變成「任何人都能替任何人改頭像」。要驗證使用者登入、把檔案關聯到 userId,並確保只能改自己的資源(或有管理權限)

MVC ? Web API?
回傳 View 才算「MVC 用途」,而回傳 JSON/狀態碼就是「Web API 用途」,但接檔案這件事兩邊用的是同一套底層能力。不要以為 IFormFile 是 MVC 才有,其實它在 Microsoft.AspNetCore.Http,Web API 端點照收,差別只是你回傳什麼(View vs JSON)
建立新商品
1 | { |
設定
1 | public class Product { |
1 | { "name": "Gaming Laptop", "price": 35000 } |
Body 只能被讀取一次,所以一個 Action 方法最多只能有一個 [FromBody]

注意項目
用 DTO,不要直接用 Product 實體,真實專案的 Product 可能有 Id/CreatedAt/IsDeleted/OwnerId/CostPrice 等欄位,你不想讓 client 亂塞;用 CreateProductRequest 可以只開放該給的欄位,避免 over-posting。
避免 over-posting(多塞欄位就被你吃進去),client 多送的欄位你如果默默收下來,等於把系統控制權讓出去一部分。,例如 client 送 {“price”:1,”isAdmin”:true} 這類莫名其妙的欄位,如果你的模型剛好有對應欄位就會被綁定,後果很難收拾;DTO + 白名單欄位是基本功。
金額欄位用 decimal 是對的,但要注意精度與規則,金額最怕浮點誤差與規則不一致,decimal 只是第一步,你還要定義小數位與四捨五入規則。,例如允許幾位小數、是否允許 0、要不要做貨幣欄位;否則資料會慢慢變髒
重複送出與 idempotency(前端按兩次就新增兩筆),使用者網路卡一下連按兩次,你的系統如果就生兩筆商品,後面只會一直擦屁股。,可用前端禁用按鈕、後端用 request id / idempotency key(或至少用去重策略)避免重複建立
[FromBody] 一個 action 只能有一個,怎麼解決「想要多東西」?你不是要多個 [FromBody],你要的是「把需要的資料包成一個 request DTO」,例如你想同時帶 product + tags + categoryId,就做 CreateProductRequest { Product…, Tags…, CategoryId… },一次反序列化即可

案例:API 金鑰驗證
1 | GET /secure/products |
設定
1 | [] |
因為金鑰、Token 這種敏感資訊不適合放在 URL 或 Body,每個請求都應該帶在 Header
注意項目
Key 需要「權限範圍」與「最小權限」,一把 key 能做越少事,外流時你要賠的就越少。區分 read-only、write、admin;甚至分到單一資源範圍(某店家只能查自己商品),避免一把 key 看全世界
若跨第三方/高風險場景,API Key 可能不夠,API Key 本質上是「誰拿到誰能用」,遇到可重放、可轉傳的風險時要加簽名或改用 OAuth/JWT,更嚴謹的做法會用時間戳 + nonce + HMAC 簽名(避免重放),或 OAuth 2.0 的 token 機制(可短效、可撤銷、可 scope)

取得系統時間
設定
1 | public interface IClock { DateTime Now { get; } } |
因為 IClock 不是從 HTTP Request 來的資料,而是來自應用程式內部的服務

1 | PUT /products/123?notify=true |
1 | { |
1 | [] |
注意事項
Authorization 不要用 [FromHeader] string token 自己比字串,正規做法是用 ASP.NET Core Authentication/Authorization(JWT Bearer、Policy、Roles/Scopes),讓框架處理驗證、過期、簽章、challenge;action 裡拿到的應該是 User 身分,不是 raw token







