Json.Net DeserializeObject not working with OData.Delta - integers

This issue affects my ASP.Net WebApi patch method, which looks something like this:

public MyModel Patch(int id, [FromBody]Delta<MyModel> newRecord){/*stuff here*/} 

But it is not WebApi that the problem is the failure between Json.Net and OData.Delta.

Problem JsonConvert.DeserializeObject does not see integers of OData.Delta strong> objects, and I am wondering if there is a workaround or fix that I can apply.

UPDATE: wrote code (see below) in the Json.Net library that will fix this. It just needs to be included in the next update (if James Newton King allows)

UPDATE 2: after further testing, I decided that the best way to do this is to stop using OData.Delta and write my own (see answer)

Units tests to prove the problem exist (using the instructions below for clarity)

Test 1: crashing with int (Int32):

 class TestObjWithInt { public int Int { get; set; } } [TestMethod] public void IsApplied_When_IntIsDeserializedToDelta() { string testData = "{\"Int\":1}"; var deserializedDelta = JsonConvert.DeserializeObject<Delta<TestObjWithInt>>(testData); var result = deserializedDelta.GetChangedPropertyNames().Contains("Int"); Assert.IsTrue(result); } 

Test 2: succeeds with long (Int64)

 class TestObjWithLong { public long Long { get; set; } } [TestMethod] public void IsApplied_When_LongIsDeserializedToDelta() { string testData = "{\"Long\":1}"; var deserializedDelta = JsonConvert.DeserializeObject<Delta<TestObjWithLong>>(testData); var result = deserializedDelta.GetChangedPropertyNames().Contains("Long"); Assert.IsTrue(result); } 

And to be sure that deserialization works for starters, these two tests pass.

 [TestMethod] public void IsApplied_When_LongIsDeserializedToTestObject() { string testData = "{\"Long\":1}"; var deserializedObject = JsonConvert.DeserializeObject<TestObjWithLong>(testData); var result = deserializedObject.Long == 1; Assert.IsTrue(result); } [TestMethod] public void IsApplied_When_IntIsDeserializedToTestObject() { string testData = "{\"Int\":1}"; var deserializedObject = JsonConvert.DeserializeObject<TestObjWithInt>(testData); var result = deserializedObject.Int == 1; Assert.IsTrue(result); } 

I found this OData Error Report, which looks like a similar problem, but its old and closed, so probably not.

Any help would be great.

Using statements (from the top of the test file):

 using System; using System.Linq; using System.Web.Http.OData; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; 

The decision made by James Newton King - will change to release 6.0.6. Replace the line J81SerializerInternalReader.cs 1581:

 contract.TrySetMember(newObject, memberName, value); 

from:

 bool done = false; while (!(done = done || contract.TrySetMember(newObject, memberName, value))) { switch (reader.TokenType) { case JsonToken.Integer: if (value is long && ((long)value) <= Int32.MaxValue && ((long)value) >= Int32.MinValue) value = Convert.ToInt32(value); //Add else if (...) to cast to other data types here (none additional required to date). else done = true; break; default: done = true; break; } } 
+7
c # odata asp.net-web-api
source share
2 answers

OData.Delta <T> does not work with Json.Net for any number except Int64. The easiest approach is to write a replacement for OData.Delta <T> (which I did in corporate time, so I cannot publish it completely sorry) containing the following methods:

 private bool TrySetInt32(object value, PropertyInfo propertyInfo, bool isNullable) { var done = false; if (value is Int32) { propertyInfo.SetValue(_obj, value); done = true; } else if (value == null) { if (isNullable) { propertyInfo.SetValue(_obj, value); done = true; } } else if (value is Int64) //Json.Net - fallback for numbers is an Int64 { var val = (Int64)value; if (val <= Int32.MaxValue && val >= Int32.MinValue) { done = true; propertyInfo.SetValue(_obj, Convert.ToInt32(val)); } } else { Int32 val; done = Int32.TryParse(value.ToString(), out val); if (done) propertyInfo.SetValue(_obj, val); } return done; } 

A class can be dynamically shared as follows:

 public sealed class Patchable<T> : DynamicObject where T : class, new() 

With a working variable:

 T _obj = new T(); 

In the overridden TrySetMember method, we need to check the base type of the property using reflection and call the corresponding TrySet ... method as follows:

 if (underlyingType == typeof(Int16)) done = TrySetInt16(value, propertyInfo, isNullable); else if (underlyingType == typeof(Int32)) done = TrySetInt32(value, propertyInfo, isNullable); 

If the value is set successfully, we can add the property name to the list, which we can use to fix the original record as follows:

 if (done) _changedPropertyNames.Add(propertyInfo.Name); public void Patch(T objectToPatch) { foreach (var propertyName in _changedPropertyNames) { var propertyInfo = _obj.GetType().GetProperty(propertyName); propertyInfo.SetValue(objectToPatch, propertyInfo.GetValue(_obj)); } } 

68 units of tests later, all this works very well. Here is an example:

 class TestObjWithInt32 { public Int32 Int32 { get; set; } public Int32? SetNullable { get; set; } public Int32? UnsetNullable { get; set; } } [TestMethod] public void IsApplied_When_Int32IsDeserializedToPatchable() { string testData = "{\"Int32\":1,\"SetNullable\":1}"; var deserializedPatchable = JsonConvert.DeserializeObject<Patchable<TestObjWithInt32>>(testData); var result = deserializedPatchable.ChangedPropertyNames.Contains("Int32"); Assert.IsTrue(result); var patchedObject = new TestObjWithInt32(); Assert.AreEqual<Int32>(0, patchedObject.Int32); deserializedPatchable.Patch(patchedObject); Assert.AreEqual<Int32>(1, patchedObject.Int32); Assert.IsNull(patchedObject.UnsetNullable); Assert.IsNotNull(patchedObject.SetNullable); } 
+3
source share

This is my implementation for this problem based on Rob solution:

 public sealed class Patchable<T> : DynamicObject where T : class { private readonly IDictionary<PropertyInfo, object> changedProperties = new Dictionary<PropertyInfo, object>(); public override bool TrySetMember(SetMemberBinder binder, object value) { var pro = typeof (T).GetProperty(binder.Name); if (pro != null) changedProperties.Add(pro, value); return base.TrySetMember(binder, value); } public void Patch(T delta) { foreach (var t in changedProperties) t.Key.SetValue( delta, t.Key.PropertyType.IsEnum ? Enum.Parse(t.Key.PropertyType, t.Value.ToString()) : Convert.ChangeType(t.Value, t.Key.PropertyType)); } } 

I deleted the required element with an empty constructor in a generic parameter, using a dictionary instead of a temporary object.

Thanks Rob;)

+1
source share

All Articles