文章出處

最近將一個項目從ASP.NET MVC 3升級至剛剛發布的ASP.NET MVC 5.1,升級后發現一個ajax請求出現了500錯誤,日志中記錄的詳細異常信息如下:

System.ArgumentException: 已添加了具有相同鍵的項。(An item with the same key has already been added)
   在 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   在 System.Web.Mvc.JsonValueProviderFactory.AddToBackingStore(EntryLimitedDictionary backingStore, String prefix, Object value)
   在 System.Web.Mvc.JsonValueProviderFactory.AddToBackingStore(EntryLimitedDictionary backingStore, String prefix, Object value)
   在 System.Web.Mvc.JsonValueProviderFactory.GetValueProvider(ControllerContext controllerContext)
   在 System.Web.Mvc.ValueProviderFactoryCollection.GetValueProvider(ControllerContext controllerContext)
   在 System.Web.Mvc.ControllerBase.get_ValueProvider()
   在 System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
   在 System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
   在 System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass21.<BeginInvokeAction>b__19(AsyncCallback asyncCallback, Object asyncState)
   在 System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResultBase`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
   在 System.Web.Mvc.Async.AsyncResultWrapper.Begin[TResult](AsyncCallback callback, Object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate`1 endDelegate, Object tag, Int32 timeout)
   在 System.Web.Mvc.Async.AsyncControllerActionInvoker.BeginInvokeAction(ControllerContext controllerContext, String actionName, AsyncCallback callback, Object state)

雖然問題是由于升級至MVC 5.1引起的,但本著“遇到問題,先懷疑自己”的原則,檢查了一下代碼,竟然在js代碼中發現了一個存在已久的低級錯誤:

var pagingBuider = { "PageIndex": 1 };
function buildPaging(pageIndex) {
    pagingBuider.pageIndex = pageIndex;
    $.ajax({
        data: JSON.stringify(pagingBuider),
        contentType: 'application/json; charset=utf-8'
    });
}

PageIndex在賦值時寫成了pageIndex(第1個字母大寫P寫成了小寫p),在js中開頭字母小寫也是規范寫法,當時可能是直覺性地寫出來的,所以這個低級錯誤情有可原。

/*這時你可能不禁要問:為什么自己給自己找事,開頭字母用大寫呢?哎,我也有我的苦衷,這段js代碼是在服務端根據C#對象的屬性生成的,C#的規范是開頭字母大寫*/

由于這樣一個低級錯誤,在ajax請求時發送給服務端的json字符串變成了這樣:

{"PageIndex":1,"pageIndex":2}

這時找茬的勁頭一涌而出,一個大大的問號浮現在眼前。。。

 

為什么ASP.NET MVC 3能包容這個錯誤,并且得到正確的值(PageIndex=2),而ASP.NET MVC 5.1卻不能呢?是MVC 5.1更嚴謹了還是心胸更狹窄了?

好奇心的驅使下,嘗試在ASP.NET MVC的開源代碼中一探究竟。

  • 用git簽出ASP.NET MVC的源代碼——https://git01.codeplex.com/aspnetwebstack
  • 用VS2013打開解決方案,在解決方案管理器中搜索到JsonValueProviderFactory

在AddToBackingStore方法中找到了異常的引發點(最后1行代碼 backingStore.Add(prefix, value)):

private static void AddToBackingStore(EntryLimitedDictionary backingStore, string prefix, object value)
{
    IDictionary<string, object> d = value as IDictionary<string, object>;
    if (d != null)
    {
        foreach (KeyValuePair<string, object> entry in d)
        {
            AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
        }
        return;
    }

    IList l = value as IList;
    if (l != null)
    {
        for (int i = 0; i < l.Count; i++)
        {
            AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
        }
        return;
    }

    // primitive
    backingStore.Add(prefix, value);
}

進一步追蹤下去,找到了引發異常的具體代碼行:

_innerDictionary.Add(key, value);

_innerDictionary在運行時的對應實現是:

new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

在Dictionary的構造函數中特地使用了StringComparer.OrdinalIgnoreCase(也就是key不區分大小寫),可見微軟程序員考慮到了“大小寫寫錯”的情況,但是沒有考慮到“正確與錯誤的大小寫都出現”的情況。

當MVC 5.1接收到 {"PageIndex":1,"pageIndex":2} 的json字符串,在執行如下操作時:

_innerDictionary.Add("PageIndex", 1);
_innerDictionary.Add("pageIndex", 2);

引爆了異常:

System.ArgumentException: 已添加了具有相同鍵的項。(An item with the same key has already been added)。

修復這個問題很簡單:

if (_innerDictionary.ContainsKey(key))
{
    _innerDictionary[key] = value;
}
else
{
    _innerDictionary.Add(key, value);
}

是微軟程序員沒考慮到還是有什么特別考慮?

但是,仔細看了一下JsonValueProviderFactory的實現代碼讓人覺得答案更可能是前者,比如下面的代碼:

private static object GetDeserializedObject(ControllerContext controllerContext)
{
    if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
    {
        // not JSON request
        return null;
    }

    StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
    string bodyText = reader.ReadToEnd();
    if (String.IsNullOrEmpty(bodyText))
    {
        // no JSON data
        return null;
    }

    JavaScriptSerializer serializer = new JavaScriptSerializer();
    object jsonData = serializer.DeserializeObject(bodyText);
    return jsonData;
}

StreadReader竟然不進行Dispose(比如放在using中),這不像是出自一個優秀程序員之手。

當時聽到ASP.NET MVC開源的消息時,心想這下終于可以一睹世界頂級公司頂尖程序員寫的賞心悅目的漂亮代碼了!現在卻讓人有一點點失望。。。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()