昨天我們一路追到 JSON Deserialization。
假設 Client 傳來:
{"name":"Andy","age":20}
Framework 可以先取得 Request Body,再根據 Content-Type 判斷這是一份 JSON,最後把它反序列化成某個 C# Object。
例如:
JSON
↓
User
├── Name = Andy
└── Age = 20
但做到這裡後,我突然發現還有一個問題。
Framework 怎麼知道:
這一次應該把 JSON 轉成 User?
下一個 Handler 可能需要的又不是 User。
例如某個 Handler 可能想要:
User user
另一個 Handler 可能想要:
Product product
登入功能可能需要:
LoginRequest request
甚至還可能有:
int id
string name
HttpRequest request
如果 Framework 不知道 Handler 到底需要什麼,就算它已經會解析 JSON,好像也不知道最後應該把資料變成什麼。
所以今天想理解的問題是:
Framework 到底怎麼知道 Handler 需要哪些參數?
而 Reflection 又為什麼會在這個地方出現?
以前寫 ASP.NET Core Controller 時,可能會直接寫:
CreateUser(User user)
或:
GetUser(int id)
然後 Framework 好像就知道:
這個地方需要 User。
那個地方需要 int。
甚至可以自動把 Request 裡的資料放進正確的位置。
以前我很少去想:
Framework 怎麼知道我的 Method 長什麼樣?
因為 Method 明明是我自己寫的。
Framework 事先不可能知道所有開發者未來會寫:
User
Product
LoginRequest
OrderRequest
還是其他自己建立的 Class。
也就是說,如果 Framework 可以處理這些未知的 Handler,它一定需要一種能力:
在程式執行時,去「觀察」程式本身的結構。
這就開始碰到今天的主角:
Reflection。
Reflection 是什麼?
Reflection 中文通常翻成「反射」。
第一次看到這個名字時,我其實完全看不出它在做什麼。
目前可以先把它理解成:
程式在執行期間,查看 Type、Method、Property、Parameter 等程式結構資訊的能力。
平常我們寫:
User user = new User();
是我們自己本來就知道 User 是什麼。
但 Reflection 比較像是在問:
這個 Type 叫什麼?
它有哪些 Property?
這個 Method 有幾個參數?
第一個參數是什麼 Type?
第二個參數叫什麼名字?
它回傳什麼 Type?
也就是:
程式
↓
觀察程式本身的結構
這件事情聽起來有點抽象,但放到 Framework 裡就非常合理。
Framework 為什麼需要看 Handler?
假設有一個 Handler:
CreateUser(User user)
Framework 收到:
POST /users
Request Body 是:
{"name":"Andy","age":20}
現在 Framework 已經知道:
Route
↓
找到 CreateUser
Body
↓
有 JSON
但接下來它必須回答:
CreateUser 到底需要什麼?
如果透過 Reflection 發現:
Method = CreateUser
Parameters:
user → User
那 Framework 就可以知道:
原來這個 Handler 的第一個參數型別是 User。
接著才有可能:
JSON
↓
Deserialize()
↓
User Object
↓
傳給 CreateUser
整個流程就接起來了。
如果下一個 Handler 需要 Product 呢?
例如:
CreateProduct(Product product)
Reflection 查到:
Parameter Type = Product
那 Framework 就可以改成:
JSON
↓
Deserialize()
↓
Product Object
也就是 Framework 不需要事先把所有 Model 寫死。
它可以在執行時查看 Handler 本身的定義,再決定要怎麼準備資料。
這就是 Reflection 對 Framework 很重要的其中一個原因。
Handler 不一定只有一個參數
事情當然不一定永遠只有:
CreateUser(User user)
也可能是:
UpdateUser(int id, User user)
這時 Framework 看到兩個參數:
id → int
user → User
那它就必須開始思考:
id 要去哪裡找?
User 又要去哪裡找?
假設 Request 是:
PUT /users/10
Body:
{"name":"Andy","age":20}
那可能會形成:
Route
↓
/users/{id}
Route Value
↓
id = 10
Body
↓
JSON
Reflection
↓
Handler 需要:
int id
User user
接著 Framework 才能想辦法:
10
↓
轉成 int
↓
id
JSON
↓
Deserialize()
↓
user
最後:
UpdateUser(id, user)
這時我才發現,前幾天學過的 Route、Query、Header、Body 並不是互相獨立的東西。
到了 Handler Parameter Binding 時,它們全部可能變成「參數的資料來源」。
Parameter Binding 是什麼?
前面提過 Model Binding。
現在可以再更具體一點理解。
假設 Handler 是:
Search(string keyword, int page)
Request 是:
/search?keyword=book&page=2
Framework 可以先解析 Query:
keyword → book
page → 2
再透過 Reflection 查看 Handler:
keyword → string
page → int
然後嘗試配對:
Query["keyword"]
↓
"book"
↓
string keyword
Query["page"]
↓
"2"
↓
轉成 int
↓
int page
最後才真正呼叫:
Search("book", 2)
所以 Framework 做的事情開始變成:
查看 Handler 需要什麼
↓
從 Request 裡找資料
↓
轉換成正確 Type
↓
放到正確 Parameter
↓
執行 Handler
這已經比單純 Routing 複雜很多了。
Reflection 到底能看到什麼?
以目前需要理解的範圍,可以先知道 Reflection 能協助取得很多 Metadata。
例如:
Type 的名稱
Type 有哪些 Property
Method 的名稱
Method 有哪些 Parameters
Parameter 的名稱
Parameter 的 Type
Method 的 Return Type
甚至還可以進一步取得 Attribute 等資訊。
所以對 Framework 而言,可以想成:
Handler
↓
Reflection
↓
Method Metadata
例如:
Method Name = CreateUser
Parameter Count = 1
Parameter Name = user
Parameter Type = User
Return Type = HttpResponse
Framework 就可以根據這些資訊決定下一步該怎麼做。
Metadata 又是什麼?
Metadata 可以先理解成:
描述程式結構的資料。
例如一個 Method:
CreateUser(User user)
真正的程式碼是:
這個 Method 裡面到底要做什麼。
但 Metadata 描述的是:
它叫 CreateUser。
它有一個參數。
參數名字是 user。
參數 Type 是 User。
它回傳什麼 Type。
這些不是 Handler 的商業邏輯本身,而是「描述 Handler」的資訊。
Reflection 就可以讓程式在 Runtime 取得這些 Metadata。
這跟 Day6、Day7 看到的 DLL 有關嗎?
其實有。
前面研究 Compiler 和 DLL 時,我們就曾經知道:
.NET Assembly 裡不只是 IL。
還包含 Metadata。
當時可能會覺得 Metadata 很抽象。
到了今天,突然開始看到它的用途。
Compiler 建置程式時,不只產生執行所需的 IL,也會保留 Type、Method、Member 等相關 Metadata。
Runtime 與 Reflection API 才有辦法在執行期間回答:
這個 Type 有哪些 Method?
這個 Method 有哪些 Parameters?
這個 Parameter 是什麼 Type?
所以 Day6 當初只是看到:
DLL 裡面除了 IL 還有 Metadata。
Day21 終於開始理解:
Framework 可以利用這些 Metadata 做很多自動化。
原來前面的知識真的接回來了。
Framework 可以直接執行 Method 嗎?
Reflection 除了查看 Method,也可以搭配相關 API 去呼叫 Method。
概念上可以想成:
Reflection
↓
找到 Method
↓
準備 Parameters
↓
Invoke
例如:
Framework 找到:
CreateUser
然後準備:
User user
最後就可以呼叫這個 Method。
所以未來一個更完整的 Framework 可能形成:
Request
↓
Routing
↓
找到 Handler Method
↓
Reflection 查看 Parameters
↓
Parameter Binding
↓
準備 Arguments
↓
Invoke Handler
↓
得到 Result
↓
HttpResponse
這個流程開始非常接近平常 Framework 真正在做的事情。
那 Delegate 和 Reflection 有什麼不同?
Day13 時,我們用過:
Dictionary<string, Action>
後來變成:
Dictionary<string, Func>
當時我們直接把可執行的 Delegate 放進 Route Table。
所以找到 Route 後:
handler()
就可以直接執行。
這種方式很簡單,而且執行也很直接。
但它有一個限制:
Framework 對 Handler 的結構知道得比較少。
例如我們直接規定:
Func
就代表:
Handler 不收參數。
而且一定回傳 HttpResponse。
如果未來希望 Handler 可以長成:
CreateUser(User user)
GetUser(int id)
Search(string keyword, int page)
那單一固定的 Func 就不夠彈性。
這時 Reflection 就可以讓 Framework:
先查看 Handler 長什麼樣
↓
再動態準備它需要的參數
所以 Delegate 和 Reflection 不是誰取代誰。
而是在不同設計下各自有用途。
為什麼 Reflection 聽起來很強,卻不能什麼都用它?
這裡也要避免另一個誤區。
Reflection 很有彈性,但也會讓程式:
更複雜
更多錯誤可能拖到 Runtime 才發現
通常比直接呼叫更有額外成本
也更難追蹤與理解
所以真正 Framework 不會因為 Reflection 很方便,就什麼事情全部靠 Reflection。
通常會搭配:
快取
預先建立 Delegate
Expression
Source Generator
編譯期產生程式碼
或其他最佳化策略
來避免每一次 Request 都重複做昂貴的探索工作。
目前我們還不用做到這麼深。
今天只要先理解:
Reflection 提供 Framework 一種在 Runtime 觀察未知使用者程式結構的能力。
這就是它最重要的價值。
📚 官方怎麼說?
在 .NET 中,Reflection 可以用來取得 Assembly、Module、Type、Method、Property、Field 等相關資訊。
也可以取得 Method 的 Parameter 資訊,甚至在適當情況下動態呼叫 Method。
這些能力對 Framework 特別重要。
因為 Framework 在被開發出來時,不可能知道未來每一個使用者會建立什麼 Controller、Method 或 Model。
但在程式真正執行時,Framework 可以透過 Runtime Metadata 與 Reflection 取得這些資訊。
也就是:
Framework 開發時
↓
不知道 User 寫了哪些 Handler
Application 執行時
↓
Reflection 查看 Handler Metadata
這就提供了 Framework 很大的擴充能力。
以前的我:
Framework 好像知道 Controller 裡有哪些 Method。
現在的我:
Compiler
↓
建立 Assembly
↓
保留 Metadata
↓
Runtime
↓
Reflection
↓
Framework 查看 Type / Method / Parameter
↓
知道 Handler 需要什麼
所以 Framework 並不是「神奇地知道」。
而是 Runtime 裡原本就存在描述程式結構的 Metadata。
Framework 再透過 Reflection 去讀取它。
前面 Day6 學到的:
Assembly = IL + Metadata + …
到了今天終於開始有更實際的意義。
今天最大的發現是:
Framework 不只是在處理 HTTP。
它同時還在理解「我們寫的 C# 程式」。
前面的流程大多是在:
Browser
↓
HTTP
↓
Framework
但到了今天,多出另一個方向:
我們寫的 Handler
↓
Metadata
↓
Reflection
↓
Framework
最後兩條線會合:
HTTP Request 資料
↓
Framework
Handler Metadata
↓
Framework
Framework 再把兩邊配起來:
Request Data
↓
Parameter Binding
↓
Handler Parameters
↓
Execute
這就是為什麼 Framework 可以提供那麼高階的開發體驗。
Framework 可以利用 Runtime Metadata 和 Reflection,查看 Handler Method 的結構。
例如發現:
CreateUser(User user)
就可以知道:
這個 Handler 需要一個 User。
接著再從 Request 中找到適合的資料來源,例如 JSON Body,反序列化成 User,最後把它傳給 Handler。
也就是:
Handler Metadata
+
Request Data
↓
Parameter Binding
↓
Handler Execution
🛠 與 Mini Web Framework 的關聯
目前 Mini Web Framework 已經一路從:
TCP
↓
HTTP
↓
Request
↓
Routing
↓
Handler
↓
Response
慢慢走到更高階的問題。
之前 Route Table 是:
Path
↓
固定的 Delegate
未來則可以繼續思考:
Path
↓
Handler Method
↓
Reflection
↓
Parameters
↓
Binding
↓
Invoke
整體可能會變成:
Browser
↓
HttpRequest
↓
Router
↓
Handler Metadata
↓
Reflection
↓
Parameter Binding
↓
Handler
↓
HttpResponse
↓
Browser
這時我們的 Mini Web Framework 就開始不只是 HTTP Server。
而是真的開始處理:
開發者寫的程式要怎麼和 HTTP 世界連接起來。
今天暫時不實作
今天一樣先把 Reflection 為什麼會出現在 Framework 裡想清楚。
因為目前 Day16~Day20 的一些 Request Parsing、Headers、Query、Body、JSON 實作還需要補上。
等前面的資料流真正跑通後,再實作 Reflection 與 Parameter Binding 才不會只剩下一段看不懂的「魔法程式碼」。
之後實作時,可以從非常簡單的實驗開始。
例如先建立一個普通 Method,再讓程式使用 Reflection 印出:
Method Name
Parameter Name
Parameter Type
Return Type
先親眼確認:
程式真的可以在 Runtime 查看另一段程式的結構。
再慢慢把它接進 Mini Web Framework。
結尾
今天第一次研究 Reflection,讓我重新理解「Framework 自動幫我做事情」這句話。
以前看到:
CreateUser(User user)
會覺得 Framework 就是知道要給我一個 User。
但現在一路拆下來後,可以看到背後可能經過:
HTTP Request
↓
Body
↓
JSON
另一方面:
Handler
↓
Reflection
↓
發現 Parameter Type = User
兩邊最後接起來:
JSON
↓
Deserialize()
↓
User Object
↓
傳給 Handler
所以 Framework 所謂的「自動」,其實不是沒有規則。
它只是把:
Metadata
Reflection
Parsing
Type Conversion
Deserialization
Parameter Binding
這些步驟全部包起來了。
而開發者最後只看到:
CreateUser(User user)
這也再次符合這個系列一路以來最大的感受:
Framework 並沒有讓底層機制消失。
它只是把複雜度藏到了更適合的位置。
現在 Framework 已經有可能知道:
Handler 需要什麼參數。
但 Reflection 取得的可能只是:
Parameter Type = User
接下來 Framework 還要判斷:
User 要從 Body 取得?
id 要從 Route 取得?
keyword 要從 Query 取得?
某個值又要不要從 Header 取得?
也就是說:
知道 Handler 需要什麼,還不代表知道「資料要去哪裡找」。
所以新的問題就是:
Framework 到底怎麼決定每個參數要從 Route、Query、Header 還是 Body 取得?