﻿# ASP.NET MVC 開發心得分享 (12)：Model Binder 的陷阱

在使用 [Model Binder](http://msdn.microsoft.com/en-us/library/dd410405\(VS.100\).aspx) 繫結資料模型時 (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)](http://msdn.microsoft.com/zh-tw/library/cxz4wk15.aspx) 機制將傳入的 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 參數時，繫結的流程是：

1.  建立一個空的 Entity 資料物件 (Member)
2.  將表單傳入的資料(FormCollection)逐項設定至該 Entity 資料物件，若該 Member 參數有宣告 Bind 屬性且設定 Exclude 欄位的話，這些欄位會自動排除

但是，由於 Member 資料表的「是否啟用」欄位型態是 Bit，也就是就算沒有繫結該欄位，該欄位的值也一定會是 True 或 False，這時若資料庫中的資料是 True，而透過上述 Reflection 機制會寫入該 Model 的值 ( False )，在更新資料時該欄位的值就會無故被更新，導致資料錯誤！我們之前就花了好多時間才發現這個潛在的邏輯錯誤（真的很難 DEBUG，所以要[快去闖關](http://wwwexam.blog.miniasp.com/)培養抓 BUG 的敏感度）

還是回歸比較**正派的寫法**，使用 Controller 內建的 [UpdateModel 方法](http://msdn.microsoft.com/en-us/library/dd470185.aspx)！所以程式會修改成如下：

```csharp
[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](http://msdn.microsoft.com/en-us/library/dd470185.aspx) 或 [TryUpdateModel](http://msdn.microsoft.com/en-us/library/dd470756.aspx) 共有 10 個多載，由於許多一些線上的範例為了簡化說明，所以通常都會用[第一個多載](http://msdn.microsoft.com/zh-tw/library/dd470933\(en-us\).aspx)方法，但使用這個多載會有「安全性問題」，因為它預設會從所有從 Browser 傳來的資料 ( e.g. [FormCollection](http://msdn.microsoft.com/en-us/library/system.web.mvc.formcollection.aspx) ) 判斷是否有可以繫結的資料，只要發現可繫結的欄位，就會將資料直接寫入 Model 物件的公開屬性中，當你有些欄位不要更新時，請務必加上「白名單」或「黑名單」！

**相關連結**

-   [ASP.NET MVC UpdateModel and TryUpdateModel for DataBinding](https://sswug.org/sswugresearch/community/asp-net-mvc-updatemodel-and-tryupdatemodel-for-databinding/)
-   [Controller.TryUpdateModel<(Of <(TModel>)>) Method](http://msdn.microsoft.com/en-us/library/dd470756.aspx)
-   [Controller.UpdateModel<(Of <(TModel>)>) Method](http://msdn.microsoft.com/en-us/library/dd470185.aspx)
-   [Models and Model Binders in MVC Applications](http://msdn.microsoft.com/en-us/library/dd410405\(VS.100\).aspx)
-   [Using ASP.NET MVC Model Binder](http://www.microsoft.com/belux/msdn/nl/chopsticks/default.aspx?id=798) ( MSDN Chopsticks )
-   [ASP.NET MVC框架开发系列课程(17)：Model Binder](http://www.docin.com/p-11759499.html)
