ASP.NET WebAPI与MVC微妙的行为偏差,涉及参数的json-(de)序列化

时间:2017-04-06 11:13:59

标签: asp.net json asp.net-mvc asp.net-web-api json.net

我们假设我们有以下简单的ajax调用:

 $.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]});
    }
}

然后在'someaction'方法中的c#世界中收到的数据是这样的:

    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 个答案:

答案 0 :(得分:3)

  

为什么首先存在关于反序列化的偏差?

历史。

在2009年首次创建ASP.NET MVC时,它使用本机.NET JavaScriptSerializer类来处理JSON序列化。三年后,当Web API出现时,作者决定转而使用日益流行的Json.Net序列化程序,因为它比旧的JavaScriptSerializer更强大,功能更全面。但是,他们显然认为他们无法改变MVC以符合向后兼容性原因 - 依赖于特定JavaScriptSerializer行为的现有项目在升级时会意外中断。因此,该决定造成了MVC和Web API之间的差异。

ASP.NET MVC Core中,MVC和Web API的内部已经统一并使用Json.Net。