Json.Net 反序列化对象失败,OData.Delta - 仅限整数

本文关键字:Delta 整数 OData Net 反序列化 对象 失败 Json | 更新日期: 2023-09-27 18:31:15

这个问题影响了我的 ASP.Net WebApi补丁方法,它看起来很像这样:

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

但问题不在于WebApi - 故障出在 Json.Net 和OData.Delta之间。

问题是JsonConvert.DeserializeObject看不到OData.Delta对象的整数,我想知道是否有可以应用的解决方法或修复程序。

更新:在 Json.Net 库中编写了代码(见下文)来解决此问题。 只需要将其包含在下一次更新中(如果詹姆斯·牛顿-金允许的话)

更新2:经过进一步测试,我决定最好的做法是停止使用OData.Delta并编写我自己的(见答案)

用于证明问题存在的单元测试(为清楚起见,使用下面移动的语句)

测试 1:失败,并显示 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);
}

测试 2:成功使用长 (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);
}

为了确保反序列化一开始就有效,这两个测试都通过了。

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

我发现了这个OData错误报告,这听起来像是一个类似的问题,但它很旧并且已关闭,所以可能不是。

任何帮助都会很棒。

Using 语句(从测试文件的顶部):

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

如果詹姆斯·牛顿-金接受解决方案-更改为6.0.6版本。替换 JsonSerializerInternalReader.cs第 1581 行:

contract.TrySetMember(newObject, memberName, value);

跟:

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

Json.Net 反序列化对象失败,OData.Delta - 仅限整数

OData.Delta 不适用于 Int64 以外的任何数字类型的 Json.Net。 最简单的方法是编写 OData.Delta 的替代品(我在公司时间做了,所以我不能完整地发布它很抱歉),其中包含如下方法:

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

该类可以是动态泛型,如下所示:

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

使用这样的工作变量:

T _obj = new T();

在重写的 TrySetMember 方法中,我们需要使用反射检查属性的基础类型,并调用相应的 TrySet...方法如下:

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

如果成功设置了该值,我们可以将属性名称添加到列表中,然后可以使用该列表来修补原始记录,如下所示:

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 次单元测试,一切似乎都运行良好。 下面是一个示例:

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

这是我基于 Rob 解决方案的此问题的实现:

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

我使用字典而不是时态对象删除了泛型类型参数中空构造函数的必要条件。

谢谢罗布·;)