简体   繁体   English

ASP.NET WebAPI 与 MVC 在参数的 json-(反)序列化方面存在细微的行为偏差

[英]ASP.NET WebAPI vs MVC subtle behavioural deviations when it comes to json-(de)serialization of parameters

Let's assume we have the following simple ajax-call:假设我们有以下简单的 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);
    });

The ajax call targets an action of an asp.net controller. ajax 调用的目标是一个 asp.net 控制器的动作。 The asp.net website has default ("factory") settings when it comes to the handling json-serialization with the only tweak being that Newtonsoft.Json.dll is installed via nuget and thus the web.config contains the following section: asp.net 网站在处理 json 序列化时具有默认(“工厂”)设置,唯一的调整是 Newtonsoft.Json.dll 是通过 nuget 安装的,因此 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>

The configuration sections for both webapi and mvc inside global.asax.cs have remained as they where. global.asax.cs 中 webapi 和 mvc 的配置部分保持原样。 Having said all this, I noticed that if the controller 'somecontroller' is a webapi controller:说了这么多,我注意到如果控制器“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]});
    }
}

then the data received in the c# world inside the 'someaction' method are like so:然后在'someaction'方法内的c#世界中接收到的数据是这样的:

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

However, if the controller is an mvc controller (mvc4 to be precise):但是,如果控制器是 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] });
    }
}

then the data received in the csharp world inside the method look like so:然后在方法内部的 csharp 世界中接收到的数据如下所示:

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

It's apparent that there is a deviation between webapi and mvc controllers in terms of how deserialization of parameters works both when it comes to empty arrays and empty strings.很明显,当涉及到空数组和空字符串时,webapi 和 mvc 控制器在参数反序列化的工作方式方面存在偏差。 I have managed to work around the quirks of the MVC controller so as to enforce the "webapi" behaviour both for empty strings and empty arrays (I will post my solution at the end for completeness).我设法解决了 MVC 控制器的怪癖,以便对空字符串和空数组强制执行“webapi”行为(为了完整起见,我将在最后发布我的解决方案)。

My question is this:我的问题是这样的:

Why does this deviation in regards to deserialization exist in the first place?为什么首先存在这种关于反序列化的偏差?

I can't come to terms that it was done merely for the sake of "convenience" given how much room the default mvc-settings leave for bugs that are just nerve-racking to discern and fix clearly and consistently at the action/dto-level.考虑到默认的 mvc 设置为那些在 action/dto 上清晰且一致地识别和修复的错误留下了多少空间,我无法接受它仅仅是为了“方便”而完成的——等级。

Addendum: For anyone interested here's how I forced the mvc controller to behave the "webapi" way when it comes to deserializing parameters before feeding them into the action-methods:附录:对于任何有兴趣的人来说,在将参数提供给操作方法之前,我如何强制 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());

Utility classes:实用程序类:

  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
  }

And

    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
    }

Why does this deviation in regards to deserialization exist in the first place?为什么首先存在这种关于反序列化的偏差?

History.历史。

When ASP.NET MVC was first created in 2009, it used the native .NET JavaScriptSerializer class to handle JSON serialization. 2009 年首次创建ASP.NET MVC时,它使用本机 .NET JavaScriptSerializer类来处理 JSON 序列化。 When Web API came along three years later, the authors decided to switch to using the increasingly popular Json.Net serializer because it was much more robust and full-featured than the older JavaScriptSerializer.三年后Web API出现时,作者决定改用越来越流行的 Json.Net 序列化器,因为它比旧的 JavaScriptSerializer 更健壮和功能齐全。 However, they apparently felt that they could not change MVC to match for backward compatibility reasons-- existing projects which relied on specific JavaScriptSerializer behaviors would break unexpectedly when upgraded.然而,他们显然认为由于向后兼容的原因,他们无法更改 MVC 以匹配 - 依赖特定 JavaScriptSerializer 行为的现有项目在升级时会意外中断。 So, that decision created the discrepancy between MVC and Web API.因此,该决定造成了 MVC 和 Web API 之间的差异。

In ASP.NET MVC Core , the internals of MVC and Web API have been unified and use Json.Net.ASP.NET MVC Core 中,MVC 和 Web API 的内部已经统一并使用 Json.Net。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM