2017-04-06 12 views
4
(反)序列

假設我們有以下簡單的Ajax調用:ASP.NET的WebAPI VS MVC微妙的行爲偏差,當涉及到json-的參數

$.ajax({ 
    url: "/somecontroller/someaction", 
    data: JSON.stringify({ 
     someString1: "", 
     someString2: null, 
     someArray1: [], 
     someArray2: null 
    }), 
    method: "POST", 
    dataType: "json", 
    contentType: "application/json; charset=utf-8" 
}) 
    .done(function (response) { 
     console.log(response); 
    }); 

Ajax調用目標的行動asp.net控制器。 asp.net網站在處理json序列化時有默認(「工廠」)設置,唯一的調整是通過nuget安裝Newtonsoft.Json.dll,因此web.config包含以下部分:

<dependentAssembly> 
     <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> 
     <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" /> 
    </dependentAssembly> 

global.asax.cs中webapi和mvc的配置部分保持不變。說了這麼多,我注意到,如果控制器「somecontroller」是的WebAPI控制器:

public class FooController : ApiController 
{ 
    public class Some 
    { 
     public string SomeString1 { get; set; } 
     public string SomeString2 { get; set; } 
     public long[] SomeArray1 { get; set; } 
     public long[] SomeArray2 { get; set; } 
    } 

    [HttpPost] 
    public IHttpActionResult Bar([FromBody] Some entity) 
    { 
     return Ok(new {ping1 = (string) null, ping2 = "", ping3 = new long[0]}); 
    } 
} 

然後在C#世界獲得「someaction」方法裏面的數據是,像這樣:

entity.someString1: "", 
    entity.someString2: null, 
    entity.someArray1: [], 
    entity.someArray2: null 

然而,如果控制器是一個MVC控制器(mvc4要準確):

public class FooController : System.Web.Mvc.Controller 
{ 
    public class Some 
    { 
     public string SomeString1 { get; set; } 
     public string SomeString2 { get; set; } 
     public long[] SomeArray1 { get; set; } 
     public long[] SomeArray2 { get; set; } 
    } 

    [HttpPost] 
    public System.Web.Mvc.JsonResult Bar([FromBody] Some entity) 
    { 
     return Json(new { ping1 = (string)null, ping2 = "", ping3 = new long[0] }); 
    } 
} 

然後在CSHARP世界接收的方法中的數據看起來像這樣:

entity.someString1: null, 
    entity.someString2: null, 
    entity.someArray1: null, 
    entity.someArray2: null 

很明顯,webapi和mvc控制器之間在參數的反序列化過程中如何處理空數組和空字符串時存在偏差。我設法解決了MVC控制器的怪癖,以便對空字符串和空數組執行「webapi」行爲(爲了完整性,我將在末尾發佈我的解決方案)。

我的問題是這樣的:

爲什麼會出現這種偏差在關於反序列化擺在首位存在?

我不能說這只是爲了「方便」而完成的,因爲默認mvc設置留下了多少空間,這些缺陷只是讓人費解並能夠清楚並一致地修復行動/ dto級別。

附錄:任何有興趣在這裏就是我強迫MVC控制器的行爲「的WebAPI」的方式,當談到他們送入動作的方法之前,反序列化參數:

//inside Application_Start 
    ModelBinders.Binders.DefaultBinder = new CustomModelBinder_Mvc(); 
    ValueProviderFactories.Factories.Remove(
     ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault() 
); 
    ValueProviderFactories.Factories.Add(new JsonNetValueProviderFactory_Mvc()); 

實用工具類:

using System.Web.Mvc; 

    namespace Project.Utilities 
    { 
     public sealed class CustomModelBinder_Mvc : DefaultModelBinder //0 
     { 
      public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 
      { 
       bindingContext.ModelMetadata.ConvertEmptyStringToNull = false; 
       Binders = new ModelBinderDictionary { DefaultBinder = this }; 
       return base.BindModel(controllerContext, bindingContext); 
      } 
     } 
     //0 respect empty ajaxstrings aka "{ foo: '' }" gets converted to foo="" instead of null http://stackoverflow.com/a/12734370/863651 
    } 

而且

using Newtonsoft.Json; 
    using Newtonsoft.Json.Converters; 
    using Newtonsoft.Json.Serialization; 
    using System; 
    using System.Collections; 
    using System.Collections.Generic; 
    using System.Dynamic; 
    using System.Globalization; 
    using System.IO; 
    using System.Web.Mvc; 
    using IValueProvider = System.Web.Mvc.IValueProvider; 
    // ReSharper disable RedundantCast 

    namespace Project.Utilities 
    { 
     public sealed class JsonNetValueProviderFactory_Mvc : ValueProviderFactory //parameter deserializer 
     { 
      public override IValueProvider GetValueProvider(ControllerContext controllerContext) 
      { 
       if (controllerContext == null) 
        throw new ArgumentNullException(nameof(controllerContext)); 

       if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)) 
        return null; 

       var jsonReader = new JsonTextReader(new StreamReader(controllerContext.HttpContext.Request.InputStream)); 
       if (!jsonReader.Read()) 
        return null; 

       var jsonObject = jsonReader.TokenType == JsonToken.StartArray //0 
        ? (object)JsonSerializer.Deserialize<List<ExpandoObject>>(jsonReader) 
        : (object)JsonSerializer.Deserialize<ExpandoObject>(jsonReader); 

       return new DictionaryValueProvider<object>(AddToBackingStore(jsonObject), InvariantCulture); //1 
      } 
      private static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture; 
      private static readonly JsonSerializer JsonSerializer = new JsonSerializer //newtonsoft 
      { 
       Converters = 
       { 
        new ExpandoObjectConverter(), 
        new IsoDateTimeConverter {Culture = InvariantCulture} 
       } 
      }; 
      //0 use jsonnet to deserialize object to a dynamic expando object if we start with a [ treat this as an array 
      //1 return the object in a dictionary value provider which mvc can understand 

      private static IDictionary<string, object> AddToBackingStore(object value, string prefix = "", IDictionary<string, object> backingStore = null) 
      { 
       backingStore = backingStore ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); 

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

       var l = value as IList; 
       if (l != null) 
       { 
        if (l.Count == 0) //0 here be dragons 
        { 
         backingStore[prefix] = new object[0]; //0 here be dragons 
        } 
        else 
        { 
         for (var i = 0; i < l.Count; i++) 
         { 
          AddToBackingStore(l[i], MakeArrayKey(prefix, i), backingStore); 
         } 
        } 
        return backingStore; 
       } 

       backingStore[prefix] = value; 
       return backingStore; 
      } 

      private static string MakeArrayKey(string prefix, int index) => $"{prefix}[{index.ToString(CultureInfo.InvariantCulture)}]"; 
      private static string MakePropertyKey(string prefix, string propertyName) => string.IsNullOrEmpty(prefix) ? propertyName : $"{prefix}.{propertyName}"; 
     } 
     //0 here be dragons  its vital to deserialize empty jsarrays "{ foo: [] }" to empty csharp array aka new object[0] 
     //0 here be dragons  without this tweak we would get null which is completely wrong 
    } 
+1

相關問題:http://stackoverflow.com/questions/3641723/why-do-i-get-null-instead-of-empty-string-when-receiving-post-request-in-from-ra。沒有找到任何詳細的基本原理,關於視圖模型的新實例中引用類型的默認值在模型綁定期間爲null的情況只有幾點。對我來說,選擇它的原因並不真實。 –

+0

確實。我正在研究這樣的帖子,人們會通過解決核心問題(也就是數據腐敗)而繞過叢林而變通解決方案。我覺得這裏的房間裏有一頭大象。 – xDisruptor

回答

3

爲什麼這種反序列化方面的偏差首先存在?

歷史。

ASP.NET MVC於2009年首次創建時,它使用本機。NET JavaScriptSerializer類來處理JSON序列化。當三年後Web API出現時,作者決定轉而使用日益流行的Json.Net序列化程序,因爲它比舊版JavaScriptSerializer更強大,功能更強大。然而,他們顯然覺得他們不能改變MVC以適應後向兼容的原因 - 依賴於特定JavaScriptSerializer行爲的現有項目在升級時會意外中斷。所以,這個決定造成了MVC和Web API之間的差異。

ASP.NET MVC Core中,MVC和Web API的內部已經統一併使用Json.Net。

+0

我認爲這種特殊差異一直延續到Core。令我感到困惑的是,對於這個問題沒有強調和突出強調,另一個問題是沒有一個有效的官方方式來強制MVC控制器上的WebAPI行爲(認爲是一個開關)。這將保持向後兼容性,並會使開發人員的生活變得更加簡單。我對原始執行有這樣一個問題的事實感到失望。微軟應該知道比在空數組和字符串中引入數據損壞更好(2009年或者沒有)。 – xDisruptor

+1

是的,MVC和Web API已經爲所有非Core版本使用了不同的序列化器。但是,正如我所說,這種差異在MVC Core中被消除了。 (我的理解是'JavaScriptSerializer'類從未被移植過,所以MVC不會使用它。) –

相關問題