ASP.NET WebAPI vs MVC subtle behavioural deviations when it comes to json-(de)serialization of parameters -
let's assume have following simple ajax-call:
$.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 action of asp.net controller. asp.net website has default ("factory") settings when comes handling json-serialization tweak being newtonsoft.json.dll installed via nuget , web.config contains following section:
<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 both webapi , mvc inside global.asax.cs have remained where. having said this, noticed if controller 'somecontroller' webapi controller:
public class foocontroller : apicontroller { public class { 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] entity) { return ok(new {ping1 = (string) null, ping2 = "", ping3 = new long[0]}); } }
then data received in c# world inside 'someaction' method so:
entity.somestring1: "", entity.somestring2: null, entity.somearray1: [], entity.somearray2: null
however, if controller mvc controller (mvc4 precise):
public class foocontroller : system.web.mvc.controller { public class { 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] entity) { return json(new { ping1 = (string)null, ping2 = "", ping3 = new long[0] }); } }
then data received in csharp world inside method so:
entity.somestring1: null, entity.somestring2: null, entity.somearray1: null, entity.somearray2: null
it's apparent there deviation between webapi , mvc controllers in terms of how deserialization of parameters works both when comes empty arrays , empty strings. have managed work around quirks of mvc controller enforce "webapi" behaviour both empty strings , empty arrays (i post solution @ end completeness).
my question this:
why deviation in regards deserialization exists in first place?
i can't come terms done merely sake of "convenience" given how room default mvc-settings leave bugs nerve-racking discern , fix , consistently @ action/dto-level.
addendum: interested here's how forced mvc controller behave "webapi" way when comes deserializing parameters before feeding them action-methods:
//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 = }; return base.bindmodel(controllercontext, bindingcontext); } } //0 respect empty ajaxstrings aka "{ foo: '' }" gets converted 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 jsonserializer = new jsonserializer //newtonsoft { converters = {new expandoobjectconverter(), new isodatetimeconverter()}, contractresolver = new camelcasepropertynamescontractresolver(), referenceloophandling = referenceloophandling.ignore, preservereferenceshandling = preservereferenceshandling.none #if debug ,formatting = formatting.indented #endif }; var jsonobject = jsonreader.tokentype == jsontoken.startarray //0 ? (object)jsonserializer.deserialize<list<expandoobject>>(jsonreader) : (object)jsonserializer.deserialize<expandoobject>(jsonreader); return new dictionaryvalueprovider<object>(addtobackingstore(jsonobject), cultureinfo.currentculture); //1 } //0 use jsonnet deserialize object dynamic expando object if start [ treat array //1 return object in dictionary value provider mvc can understand //todo refactor being nonrecursive 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 idictionary<string, object>; if (d != null) { foreach (var entry in d) { addtobackingstore(entry.value, makepropertykey(prefix, entry.key), backingstore); } return backingstore; } var l = value ilist; if (l != null) { if (l.count == 0) //0 here dragons { backingstore[prefix] = new object[0]; //0 here dragons } else { (var = 0; < 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 dragons vital deserialize empty jsarrays "{ foo: [] }" empty csharp array aka new object[0] //0 here dragons without tweak null wrong }
why deviation in regards deserialization exist in first place?
history.
when asp.net mvc first created in 2009, used native .net javascriptserializer
class handle json serialization. when web api came along 3 years later, authors decided switch using increasingly popular json.net serializer because more robust , full-featured older javascriptserializer. however, apparently felt not change mvc match backward compatibility reasons-- existing projects relied on specific javascriptserializer behaviors break unexpectedly when upgraded. so, decision created discrepancy between mvc , web api.
in asp.net mvc core, internals of mvc , web api have been unified , use json.net.
Comments
Post a Comment