This problem is affecting my ASP.Net WebApi Patch method which looks a lot like this:
public MyModel Patch(int id, [FromBody]Delta newRecord){
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 ;)