昨天研究 Reflection 時,我們已經知道:
Framework 可以在 Runtime 查看 Handler 的結構。
例如:
UpdateUser(int id, User user)
Framework 可以透過 Reflection 發現:
第一個參數叫 id,Type 是 int。
第二個參數叫 user,Type 是 User。
但這時又出現下一個問題。
知道 Handler 需要什麼參數,還不代表知道這些資料到底要去哪裡找。
例如:
int id
到底應該從網址 Path 裡取得?
還是 Query String?
User user
又應該從 Request Body 取得嗎?
如果還有:
string token
它可能又要從 Header 裡面找。
所以今天想理解的問題就是:
Framework 到底怎麼知道 Handler 的每一個參數,應該從 Request 的哪個地方取得?
這就是 Parameter Binding 開始真正發揮作用的地方。
💭 我原本以為……
以前看到:
GetUser(int id)
會覺得 Framework 自然就知道 id 是什麼。
例如網址:
/users/10
Framework 就會把:
10
放進:
id
看起來很理所當然。
但仔細想就會發現:
Request 裡可能同時存在很多地方都有資料。
例如:
Path:
/users/10
Query:
?includeOrders=true
Headers:
Authorization: ...
Body:
{"name":"Andy"}
那 Handler 如果長這樣:
UpdateUser(int id, bool includeOrders, User user)
Framework 必須知道:
id → Route
includeOrders → Query
user → Body
它不可能只是隨便找一個值塞進去。
所以這中間一定需要某種規則。
什麼是 Parameter Binding?
Parameter Binding 可以先理解成:
把 Request 中的資料,轉換並放進 Handler 所需要的參數。
例如 Request:
PUT /users/10?notify=true
Body:
{"name":"Andy"}
Handler:
UpdateUser(int id, bool notify, User user)
Framework 最後需要完成:
10
↓
int id
true
↓
bool notify
JSON
↓
User user
最後才能真正呼叫:
UpdateUser(10, true, user)
所以整個 Binding 流程其實包含幾件事:
知道 Handler 有哪些參數
找到每個參數的資料來源
取得原始值
把原始值轉成正確 Type
建立最後的 Argument List
呼叫 Handler
Parameter 的資料來源有哪些?
目前我們前幾天已經研究過幾種常見資料來源。
Route
例如:
/users/10
如果 Route Pattern 是:
/users/{id}
那:
id = 10
Query
例如:
/search?keyword=book&page=2
可以取得:
keyword = book
page = 2
Header
例如:
Authorization: Bearer ...
可能會成為某個 Handler 所需要的資訊。
Body
例如:
{"name":"Andy","age":20}
可能被反序列化成:
User
所以可以整理成:
HttpRequest
├── Route Values
├── Query
├── Headers
└── Body
而 Parameter Binding 就是在這些地方找資料。
Route Binding 怎麼想?
假設 Route 是:
/users/{id}
實際 Request:
/users/10
經過 Routing 後,可以得到:
id = "10"
注意這時很可能還是一個字串。
但是 Handler 需要:
int id
所以 Framework 還要做:
"10"
↓
Type Conversion
↓
10
最後才能把 int 10 傳進 Handler。
這裡第一次看到:
Binding 不只是「找到資料」。
還包含:
把資料轉成正確 Type。
Query Binding 也是一樣
Request:
/products?page=2
解析 Query 後:
page = "2"
但 Handler:
GetProducts(int page)
需要的是 int。
所以還要:
"2"
↓
Convert
↓
2
如果是:
?active=true
Handler:
bool active
就需要:
"true"
↓
Convert
↓
true
所以 Query Parser 負責:
把 Query 拆成 Key / Value。
Parameter Binder 則進一步負責:
把 Value 轉成 Handler 真正需要的 Type。
Body Binding 又不太一樣
Body 可能不是簡單的一個:
"10"
而是一整份 JSON。
例如:
{"name":"Andy","age":20}
Handler:
CreateUser(User user)
這時 Binding 就會需要:
Request Body
↓
Content-Type = application/json
↓
JSON Deserialization
↓
User
↓
傳給 user
所以前一天學到的 JSON Deserialization,其實可以看成 Parameter Binding 裡的一種資料轉換方式。
簡單 Type:
"10"
↓
int
複雜 Type:
JSON
↓
User Object
Framework 到底怎麼判斷來源?
這裡就開始有很多不同的 Framework 設計。
最簡單的版本,可以使用「慣例」。
例如:
簡單型別先找 Route 或 Query。
複雜 Model 嘗試從 Body。
但這種方式很快就會遇到模糊情況。
例如:
Handler:
Search(string id)
Request 同時存在:
Route id = 10
Query id = 20
那 Framework 到底應該使用哪一個?
這時就需要更明確的規則。
Attribute 為什麼會出現?
ASP.NET Core 裡常常可以看到:
[FromRoute]
[FromQuery]
[FromBody]
[FromHeader]
以前看到這些 Attribute 時,可能只覺得:
這是在告訴 Framework 資料從哪裡來。
現在終於比較能理解它為什麼存在。
例如概念上:
[FromRoute] int id
就是明確告訴 Framework:
這個 id 不要亂猜。
請從 Route Value 取得。
而:
[FromQuery] string keyword
代表:
請從 Query 取得。
[FromBody] User user
則代表:
請從 Body 建立 User。
所以 Attribute 可以看成:
開發者提供給 Framework 的額外 Metadata。
Reflection 再次接回來了
昨天知道 Reflection 可以看到:
Parameter Name
Parameter Type
今天又可以再往前一步:
Reflection 還可以看到 Parameter 上面的 Attribute。
例如:
Parameter:
id
Type:
int
Attribute:
FromRoute
Framework 讀到這些 Metadata 後,就能決定:
資料來源 = Route
所以流程變成:
Handler Parameter
↓
Reflection
↓
Name
Type
Attributes
↓
決定 Binding Source
↓
從 HttpRequest 找資料
↓
Convert
↓
Argument
這就是 Reflection 和 Parameter Binding 開始真正接起來的地方。
如果沒有 Attribute 呢?
Framework 也不可能要求每一個參數全部標 Attribute。
否則寫一個簡單 Handler 會變得很麻煩。
所以真正 Framework 通常會有預設規則。
例如某些情況下可以根據:
Parameter Type
Parameter Name
Route Pattern
是否為 Complex Type
已註冊服務
其他 Metadata
推斷參數來源。
所以 Binding 可以有兩種感覺:
Explicit Binding
開發者明確告訴 Framework:
從哪裡拿。
Implicit Binding
Framework 根據既定規則推斷。
對 Mini Web Framework 來說,目前不用一開始就做到這麼複雜。
我們可以先從最簡單的規則開始。
例如:
如果 Route Values 有同名 Key
→ 從 Route
否則如果 Query 有同名 Key
→ 從 Query
如果 Parameter 是 Complex Type
→ 從 JSON Body
這樣就已經可以理解基本原理。
Type Conversion 為什麼也是 Framework 的工作?
Request 裡的 Route 和 Query 很多時候原始形式都是文字。
例如:
id = "10"
page = "2"
active = "true"
但是 Handler 可能希望:
int id
int page
bool active
Framework 如果直接把 string 傳進 int Parameter,當然不行。
所以 Binding 還需要一層:
Raw Value
↓
Target Type
↓
Conversion
例如:
"10" + typeof(int)
↓
10
"true" + typeof(bool)
↓
true
這也代表 Framework 必須知道:
Parameter 的 Type 是什麼。
又再次需要 Reflection。
如果轉換失敗怎麼辦?
例如:
Request:
/users/abc
但 Handler 是:
GetUser(int id)
Framework 拿到:
"abc"
然後想轉成 int。
當然會失敗。
這時 Framework 不能假裝:
id = 0
然後繼續執行。
比較合理的做法是:
Binding Failed
↓
產生錯誤結果
↓
不要正常執行 Handler
真正 Framework 可能會產生:
400 Bad Request
或加入 Model State Error 等資訊。
這也讓我發現:
Parameter Binding 不只是在「方便開發者」。
它其實也是 Request Validation 的第一道關卡之一。
資料格式不符合 Handler 的要求時,就應該在進入商業邏輯前被發現。
Parameter Binding 的完整流程
做到這裡,可以把今天的概念整理成:
HTTP Request
↓
HttpRequest
↓
Routing
↓
找到 Handler
↓
Reflection
↓
取得 Parameters
↓
對每個 Parameter:
Parameter Name
Parameter Type
Parameter Attribute
↓
決定 Data Source
↓
Route / Query / Header / Body
↓
取得 Raw Value
↓
Type Conversion / Deserialization
↓
Argument
全部完成後:
Arguments[]
↓
Invoke Handler
官方怎麼說?
ASP.NET Core 的 Model Binding 會從 HTTP Request 的不同來源取得資料,並把它轉換成 Action Parameter 或 Model 所需要的 .NET Type。
常見資料來源包含:
Form Fields
Request Body
Route Data
Query String
Uploaded Files
另外也可以透過 Attribute 明確指定 Binding Source。
例如:
FromRoute
FromQuery
FromBody
FromHeader
所以今天推導出的概念,其實和真正 Framework 的設計方向一致。
Framework 會先理解 Handler 的需求,再把 Request Data 轉換成適合的參數。
以前的我:
Handler 寫:
GetUser(int id)
Framework 就會自動給 id。
現在的我:
Handler
↓
Reflection
↓
Parameter:
Name = id
Type = int
↓
Parameter Binding
↓
決定資料來源
↓
Route / Query / Header / Body
↓
找到 Raw Value
↓
Convert to int
↓
Argument
↓
Invoke Handler
所以:
「Framework 自動給我參數」
其實背後是一套完整的 Binding Pipeline。
今天最大的發現是:
Framework 不只是幫 Request Parsing。
它還必須理解「HTTP 世界的資料」和「C# 世界的參數」之間怎麼對應。
HTTP 世界可能只有:
"10"
"true"
JSON Text
Header String
但 C# Handler 希望的是:
int
bool
User
其他 Object
Framework 就站在中間:
HTTP Data
↓
Binding
↓
C# Values
這又再次看到 Framework 最核心的角色:
做兩個世界之間的轉換。
Framework 會先透過 Reflection 查看 Handler 需要哪些 Parameter。
接著依照 Attribute 或既定 Convention,決定每個 Parameter 的資料來源。
再從 Request 的 Route、Query、Headers 或 Body 中取得資料。
最後經過 Type Conversion 或 Deserialization,建立真正的 Arguments,再呼叫 Handler。
🛠 與 Mini Web Framework 的關聯
目前 Mini Web Framework 的完整方向已經越來越清楚:
Browser
↓
TCP
↓
HttpRequest
↓
Routing
↓
找到 Handler
↓
Reflection
↓
Parameter Binding
├── Route
├── Query
├── Headers
└── Body
↓
Arguments
↓
Handler
↓
HttpResponse
↓
Browser
以前 Route Table 只是:
Path
↓
Func
但如果加入 Parameter Binding,Handler 就有機會從固定的:
沒有參數
進化成:
Handler(int id)
Handler(string keyword)
Handler(User user)
甚至:
Handler(int id, User user)
這會讓 Mini Web Framework 的使用方式開始真正接近一般 Web Framework。
今天暫時不實作
今天依然先把概念理解清楚。
目前前面累積的 Request、Headers、Query、Body、JSON 等實作還需要一起補齊。
之後正式做 Parameter Binding 時,可以先從最簡單的 Query 開始。
例如 Handler:
Search(int page)
Request:
/search?page=2
Framework 先用 Reflection 看出:
Parameter Name = page
Parameter Type = int
再取得:
Query["page"] = "2"
最後把:
"2"
轉成:
2
並呼叫 Handler。
先讓最簡單的 Binding 跑通,再慢慢加入 Route、Body 和 Attribute。
結尾
前一天研究 Reflection 時,我開始知道 Framework 可以「看懂 Handler 長什麼樣」。
但今天才發現:
看懂 Handler 只是第一步。
Framework 真正要做到的是:
知道 Handler 需要什麼。
找到 Request 裡對應的資料。
把資料轉成正確 Type。
最後放進正確的 Parameter。
所以一個看似很簡單的:
GetUser(int id)
背後可能經過:
Reflection
↓
Parameter Metadata
↓
Route Matching
↓
Raw Value = "10"
↓
Type Conversion
↓
int 10
↓
Invoke Handler
這也是為什麼平常 Framework 使用起來會這麼方便。
複雜度沒有消失。
只是被移到了 Parameter Binding Pipeline 裡。
現在 Framework 已經有可能:
找到 Handler
準備 Parameter
執行 Handler
但是目前我們一直假設 Handler 最後會產生:
HttpResponse
真正 Framework 裡,Handler 可能回傳的東西其實很多。
例如:
string
User
List
JSON Result
Status Code
甚至什麼都不回傳。
那 Framework 又是怎麼把這些完全不同的回傳值,最後統一變成 HTTP Response?