Json.Net DeserializeObject failing with OData.Delta - integers only
OData.Delta<T> does not work with Json.Net for any number Types other than Int64. The easiest approach is to write a replacement for OData.Delta<T> (which I've done on company time so I can't post it in its entirety sorry) containing methods like this:
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;}
The class can be a dynamic generic like this:
public sealed class Patchable<T> : DynamicObject where T : class, new()
With a working variable like this:
T _obj = new T();
In the overridden TrySetMember method, we need to check the underlying type of the property using reflection and call the appropriate TrySet... method like this:
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 a list that we can then use for patching the original record like this:
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 unit tests later, it all seems to work pretty well. Here's 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);}
This is my implementation for this issue 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 removed the requisite of an empty constructor in generic type parameter using the dictionary instead of a temporal object.
Thanks Rob ;)