Json.Net DeserializeObject failing with OData.Delta - integers only

前端 未结 2 1030
佛祖请我去吃肉
佛祖请我去吃肉 2020-12-31 19:25

This problem is affecting my ASP.Net WebApi Patch method which looks a lot like this:

public MyModel Patch(int id, [FromBody]Delta newRecord){         


        
相关标签:
2条回答
  • 2020-12-31 20:04

    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);
    }
    
    0 讨论(0)
  • 2020-12-31 20:16

    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 ;)

    0 讨论(0)
提交回复
热议问题