在使用 Model Binder 繫結資料模型時 (Entity Type) ,大部分情況都是非常方便的,可有效減少 Action 參數的用量,也可大幅降低程式複雜度。但是在我們之前的某個專案就採到一個 Model Binder 的地雷,這個地雷不是 ASP.NET MVC 的 Bug,而是一個開發時應注意的地方,採用標準的寫法準沒錯。
我的同事寫了一個「修改會員資料」的 Action,該 Action 的定義如下:
public ActionResult EditProfile(string id, [Bind(Exclude = "ID")]Member _NewMember)
然後該 Action 會執行以下兩個動作,以將該 Model 物件直接儲存更新至資料庫中 (以下為錯誤示範):
1. 先依據傳入的 id 查出資料庫中該筆資料
var _OrigMember = (from p in db.Member where p.id == id select p).FirstOrDefault();
2. 再利用 .NET 的 反映(Reflection) 機制將傳入的 Model 物件 ( _NewMember ) 逐一更新至 _OrigMember
var PropertyDesc = (from p in TypeDescriptor.GetProperties(_NewMember)
.Cast<PropertyDescriptor>()
where p.Name != "id"
select p);
foreach (var item in PropertyDesc)
{
if (item.GetValue(_NewMember) != null)
item.SetValue(_OrigMember, item.GetValue(_NewMember));
}
雖然看似合理,而且這種寫法也蠻方便的,可以一體適用所有的 LINQ to SQL 模型物件,但卻隱藏著一個致命的殺機!
假設我們有個表格叫做 Member,裡面有個欄位是「是否啟用」,欄位型態是 bit,當網站前端有個表單用來更新會員基本資料的,讓使用者輸入的欄位不包括「是否啟用」欄位,且在 Controller 中也明確指定該參數有排除「是否啟用」欄位的 Model Binding,所以會跳過該欄位的驗證邏輯。
不過,當 DefaultModelBinder 在繫結 Member 參數時,繫結的流程是:
- 建立一個空的 Entity 資料物件 (Member)
- 將表單傳入的資料(FormCollection)逐項設定至該 Entity 資料物件,若該 Member 參數有宣告 Bind 屬性且設定 Exclude 欄位的話,這些欄位會自動排除
但是,由於 Member 資料表的「是否啟用」欄位型態是 Bit,也就是就算沒有繫結該欄位,該欄位的值也一定會是 True 或 False,這時若資料庫中的資料是 True,而透過上述 Reflection 機制會寫入該 Model 的值 ( False ),在更新資料時該欄位的值就會無故被更新,導致資料錯誤!我們之前就花了好多時間才發現這個潛在的邏輯錯誤(真的很難 DEBUG,所以要快去闖關培養抓 BUG 的敏感度)
還是回歸比較正派的寫法,使用 Controller 內建的 UpdateModel 方法!所以程式會修改成如下:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(string id)
{
var _OrigMember = (from p in db.Member where p.id==id select p)
.FirstOrDefault();
if (TryUpdateModel(_OrigMember, new string[] {"Name","Email","Tel"})
&& ModelState.IsValid)
{
db.SubmitChanges();
return RedirectToAction("Index");
}
else
{
return View(_OrigMember);
}
}
**注意** UpdateModel 或 TryUpdateModel 共有 10 個多載,由於許多一些線上的範例為了簡化說明,所以通常都會用第一個多載方法,但使用這個多載會有「安全性問題」,因為它預設會從所有從 Browser 傳來的資料 ( e.g. FormCollection ) 判斷是否有可以繫結的資料,只要發現可繫結的欄位,就會將資料直接寫入 Model 物件的公開屬性中,當你有些欄位不要更新時,請務必加上「白名單」或「黑名單」!
相關連結