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

Deserialization of IOrderedEnumerable<T> with JSON.NET

我和我的团队在 C# 中遇到 JSON.NET 反序列化的奇怪行为。

我们有一个带有 IOrderedEnumerable<long> 的简单 ViewModel :

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

用看起来像这样的 json 调用此 API :

{
    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)),它也会工作。

另外一个奇怪的事情是controller中的对象test是"null"。不仅 orderedDatas 为空,而且整个对象都为空。

如果我们在 class 上添加一个属性 [JsonObject],并在 属性 orderedData 上添加一个 [JsonIgnore],它就可以工作。

现在,我们将对象更改为一个简单的 List,它工作正常,但我们想知道为什么 JSON 反序列化会根据集合的类型而有所不同。

如果我们直接使用 JsonConvert.Deserialize :

var json = "{ Name: 'tiit', 'orderedDatas': [2,3,4,332232] }";
var result = JsonConvert.DeserializeObject<TestClass>(json);

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

Cannot create and populate list type System.Linq.IOrderedEnumerable`1[System.Int64]. Path 'orderedDatas', line 1, position 33.

如有任何想法/帮助,我们将不胜感激。

谢谢!

** 编辑:感谢您的回答。有一件事我一直觉得很奇怪(我把它加粗了),如果你有任何想法来解释这种行为,请告诉我**

据我在 JsonArrayContract 的构造函数中所见,接口 IOrderedEnumerable<> 未正确处理,很可能是因为它未由 List<>T[] 实现,并且因此需要更特殊的处理,基本上可以用其他接口代替。

此外,将无大小的接口放入反序列化的 class 中没有意义,因为根据定义,它们不能是无限的,可以由具有有限的已知大小的集合替换。

请记住,在反序列化对象中使用接口涉及反序列化器中的一些 "guess work" 以创建正确的实例。

如果您想要一些具有指定顺序的不可变集合,我建议您使用 IReadOnlyList<>

正如 kisu 在 中所述,Json.NET 无法反序列化您的 TestClass,因为它没有将 IOrderedEnumerable<T> 接口映射到具体 class 因为需要反序列化它。这并不奇怪,因为:

  1. IOrderedEnumerable<TElement> has no publicly available property indicating how it is sorted -- ascending; descending; using some complex keySelector delegate that refers to one or more captured variables。因此,此信息将在序列化期间丢失 - 并且即使信息是 public.

  2. ,无论如何也不会实现诸如 keySelector 委托的序列化
  3. 实现此接口的具体 .Net class,OrderedEnumerable<TElement, TKey>, is internal. Normally it is returned by Enumerable.OrderBy() or Enumerable.ThenBy() not created directly in application code. See here 示例实现。

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

这利用了这样一个事实,即当一个类型只有一个 public 构造函数时,如果该构造函数被参数化,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 在使用和不使用参数化构造函数反序列化对象时使用不同的操作顺序。在问题 的答案中解释了这种差异。牢记这个答案,Json.NET 到底什么时候抛出异常来尝试反序列化 IOrderedEnumerable<T> 类型的实例?

  1. 当类型具有参数化构造函数时,Json.NET 将遍历 JSON 文件中的属性并反序列化每个属性的值 before构造对象,然后将适当的值传递给构造函数。因此,当抛出异常时,不会构造对象。

  2. 当类型具有无参数构造函数时,Json.NET 将构造一个实例并开始咀嚼 JSON 属性以反序列化它们,中途抛出异常。因此,当异常被抛出时,对象被构造但没有完全反序列化。

显然,在您的框架中的某处,来自 Json.NET 的异常被捕获并吞没了。因此,在案例 #1 中,你得到一个 null 对象,在案例 #2 中,你得到一个部分反序列化的对象。