使用JSON.NET对IOrderedEnumerable<T>进行反序列化

7

我和我的团队在使用C#中的JSON.NET反序列化过程中遇到了一个奇怪的行为。

我们有一个简单的ViewModel,其中包含一个IOrderedEnumerable<long>

public class TestClass
{
    public IOrderedEnumerable<long> orderedDatas { get; set; }
    public string Name { get; set; }

    public TestClass(string name)
    {
        this.Name = name;
        this.orderedDatas = new List<long>().OrderBy(p => p);
    }
}

然后,我们只需将此视图模型POST/PUT到API控制器中。

[HttpPost]
public IHttpActionResult Post([FromBody]TestClass test)
{
    return Ok(test);
}

调用此API时,需要使用类似以下JSON格式的数据:

{
    Name: "tiit",
    "orderedDatas": [
        2,
        3,
        4
    ],
}

通过这个调用,我们发现构造函数没有被调用(这可以解释为它不是默认构造函数)。但奇怪的是,如果我们将集合类型更改为 IEnumerable 或 IList,则构造函数将被正确调用。
如果我们将 TestClass 的构造函数更改为默认构造函数:
public class TestClass
{
    public IOrderedEnumerable<long> orderedDatas { get; set; }
    public string Name { get; set; }

    public TestClass()
    {
        this.Name = "default";
        this.orderedDatas = new List<long>().OrderBy(i => i);
    }
}

控制器检索到的对象不会为空。如果我们将集合的类型更改为 IEnumerable 并保留带有参数的构造函数(public TestClass(string name)),那么它也可以工作。
另一个奇怪的事情是,控制器中的对象为 "null"。不仅 orderedDatas 为空,整个对象都为空。
如果在类上添加属性 [JsonObject],并在属性 orderedData 上添加 [JsonIgnore],则它将起作用。
目前,我们将对象更改为简单的 List,并且它工作得很好,但我们想知道为什么根据集合类型,JSON 反序列化表现出不同的行为。
如果我们直接使用 JsonConvert.Deserialize:
var json = "{ Name: 'tiit', 'orderedDatas': [2,3,4,332232] }";
var result = JsonConvert.DeserializeObject<TestClass>(json);

我们可以看到实际的异常信息:

无法创建和填充列表类型 System.Linq.IOrderedEnumerable`1[System.Int64]。路径 'orderedDatas', 第1行,第33个位置。

如果有任何想法/帮助,将不胜感激。
谢谢!
**编辑:谢谢你们的答复。有一件事我一直发现很奇怪(我用粗体标出来了),如果你有任何想法来解释这种行为,请告诉我**

IOrderedEnumerable只是一个接口。列表和数组是有序的,因此它们不实现该接口。如果您想要使用特定的集合类型而不是数组,则应该使用自定义转换器来创建所需的类型(例如SortedList)。 - Panagiotis Kanavos
2个回答

7
正如kisu在this answer中所述,Json.NET无法反序列化您的TestClass,因为它没有内置的逻辑来将IOrderedEnumerable接口映射到所需的具体类。这并不令人意外,因为:
  1. IOrderedEnumerable<TElement>没有公开可用的属性来指示它是如何排序的 - 升序;降序;使用一些复杂的keySelector委托,该委托引用了一个或多个捕获的变量。因此,在序列化过程中将丢失这些信息 - 即使这些信息是公开的,也不会实现序列化委托,例如keySelector委托。

  2. 实现此接口的具体.NET类OrderedEnumerable<TElement, TKey>internal的。通常情况下,它由Enumerable.OrderBy()Enumerable.ThenBy()返回,而不是直接在应用程序代码中创建。请参阅此处以获取示例实现。

对于你的TestClass,使其可以被Json.NET序列化的一个最小改动是在它的构造函数中添加params long [] orderedDatas
public class TestClass
{
    public IOrderedEnumerable<long> orderedDatas { get; set; }
    public string Name { get; set; }

    public TestClass(string name, params long [] orderedDatas)
    {
        this.Name = name;
        this.orderedDatas = orderedDatas.OrderBy(i => i);
    }
}

这利用了一个事实,即当一个类型只有一个公共构造函数时,如果该构造函数是参数化的,Json.NET将调用它来构造类型的实例,通过名称(忽略大小写)将JSON属性匹配和反序列化为构造函数参数。
话虽如此,我不推荐这种设计。从OrderedEnumerable<TElement, TKey>.GetEnumerator()的参考源代码中,我们可以看到每次调用GetEnumerator()时,基础可枚举对象都会重新排序。因此,您的实现可能非常低效。当然,在往返转换后,排序逻辑将丢失。为了理解我的意思,请考虑以下内容:
var test = new TestClass("tiit");
int factor = 1;
test.orderedDatas = new[] { 1L, 6L }.OrderBy(i => factor * i);
Console.WriteLine(JsonConvert.SerializeObject(test, Formatting.Indented));
factor = -1;
Console.WriteLine(JsonConvert.SerializeObject(test, Formatting.Indented));

第一次调用Console.WriteLine()打印
{
  "orderedDatas": [
    1,
    6
  ],
  "Name": "tiit"
}

和第二个打印

{
  "orderedDatas": [
    6,
    1
  ],
  "Name": "tiit"
}

如您所见,每次枚举orderedDatas时,它都会根据捕获变量factor的当前值重新排序。Json.NET在序列化时只能对当前序列进行“快照”,无法序列化序列不断重新排序的“动态逻辑”。
示例fiddle
当将属性更改为IList<long>时,不会抛出异常并且对象将被反序列化。Json.NET内置了将接口IList<T>IEnumerable<T>反序列化为List<T>的逻辑。但是,对于上述原因,它没有内置的具体类型可用于IOrderedEnumerable<T>更新 您问道,换句话说,为什么在尝试并失败地反序列化嵌套的IOrderedEnumerable<T>属性时,参数化构造函数没有被调用,而无参构造函数被调用呢?
Json.NET在反序列化带有和不带有参数化构造函数的对象时使用了不同的操作顺序。这个差异在回答问题Usage of non-default constructor breaks order of deserialization in Json.net中有解释。在考虑到这个答案的情况下,Json.NET何时会抛出尝试并失败地反序列化类型为IOrderedEnumerable<T>的实例的异常呢?
  1. 当类型具有参数化构造函数时,Json.NET将遍历JSON文件中的属性,并在构造对象之前对每个属性进行反序列化,然后将适当的值传递给构造函数。因此,当异常被抛出时,对象尚未构造。

  2. 当类型具有无参数构造函数时,Json.NET将构造一个实例,并开始遍历JSON属性以对其进行反序列化,但在过程中抛出异常。因此,当异常被抛出时,对象已经构造但尚未完全反序列化。

显然,在你的框架中,Json.NET的异常被捕获并吞掉了。因此,在情况#1中,你得到一个null对象,在情况#2中,你得到一个部分反序列化的对象。

3
从我在JsonArrayContract构造函数中所看到的,接口IOrderedEnumerable<>没有被正确处理,很可能是因为List<>T[]没有实现该接口,因此需要特殊处理,并且基本上可以被其他接口替代。
另外,在反序列化类中放置无大小限制的接口没有意义,因为根据定义,它们不能是无限的,并且可以被具有有限、已知大小的集合替换。
请记住,将接口用于反序列化对象涉及一些“猜测”工作,以创建正确的实例。
如果您想要一些指定顺序的不可变集合,我建议使用IReadOnlyList<>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接