您如何“真正”使用 Newtonsoft.Json 序列化循环引用对象?

Posted

技术标签:

【中文标题】您如何“真正”使用 Newtonsoft.Json 序列化循环引用对象?【英文标题】:How Do You "Really" Serialize Circular Referencing Objects With Newtonsoft.Json? 【发布时间】:2014-12-13 14:39:32 【问题描述】:

我在使用 Newtonsoft.Json 从我的 ASP.NET Web API 控制器正确序列化一些数据时遇到问题。

这就是我认为正在发生的事情 - 如果我错了,请纠正我。在某些情况下(特别是当数据中没有任何循环引用时)一切都会像您期望的那样工作 - 填充对象的列表被序列化并返回。如果我在模型中引入导致循环引用的数据(如下所述,即使设置了PreserveReferencesHandling.Objects),只有指向具有循环引用的第一个对象的列表元素会以客户端可以“一起工作”。如果在将内容发送到序列化程序之前以不同的方式排序,则“导致的元素”可以是数据中的任何元素,但至少有一个元素将以客户端可以“使用”的方式进行序列化。空对象最终被序列化为 Newtonsoft 引用 ($ref:X)。

例如,如果我有一个包含如下导航属性的 EF 模型:

在我的 global.asax 中:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects;

这是我使用实体框架执行的基本查询(延迟加载已关闭,因此这里没有任何代理类):

[HttpGet]
[Route("starting")]
public IEnumerable<Balance> GetStartingBalances()

   using (MyContext db = new MyContext())
   
       var data = db.Balances
        .Include(x => x.Source)
        .Include(x => x.Place)
        .ToList()
       return data;
    

到目前为止一切顺利,data 已被填充。

如果没有循环引用,生活是伟大的。但是,只要有 2 个具有相同 SourcePlaceBalance 实体,那么序列化就会将我返回到 Newtonsoft 引用而不是它们的最顶层列表的后面的 Balance 对象成熟的对象,因为它们已经在SourcePlace 对象的Balances 属性中序列化:

["$id":"1","BalanceID":4,"SourceID":2,"PlaceID":2 ...Omitted for clarity...,"$ref":"4"]

这个问题是客户端不知道如何处理$ref:4,即使我们人类了解发生了什么。在我的情况下,这意味着我不能在我的整个余额列表中使用 AngularJS 到 ng-repeat 这个 JSON,因为它们并非都是真正的 Balance 对象,具有要绑定的 Balance 属性。我敢肯定还有很多其他用例会遇到同样的问题。

我无法关闭json.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects,因为很多其他事情都会中断(这在此处和其他地方的 100 个其他问题中有详细记录)。

除了遍历 Web API 控制器中的实体并执行此操作之外,是否有更好的解决方法

Balance.Source.Balances = null;

要打破循环引用的所有导航属性?因为那似乎也不对。

【问题讨论】:

【参考方案1】:

是的,使用PreserveReferencesHandling.Objects 确实是使用循环引用序列化对象图的最佳方式,因为它生成最紧凑的 JSON,并且实际上保留了对象图的引用结构。也就是说,当您将 JSON 反序列化回对象时(使用理解 $id$ref 表示法的库),对特定对象的每个引用都将指向该对象的同一个实例,而不是具有多个实例相同的数据。

在您的情况下,问题是您的客户端解析器不理解 Json.Net 生成的 $id$ref 表示法,因此无法解析引用。这可以通过在反序列化 JSON 后使用 javascript 方法来重建对象引用来解决。有关示例,请参见 here 和 here。

根据您的情况,另一种可能可行的方法是在序列化时将ReferenceLoopHandling 设置为Ignore,而不是将PreserveReferencesHandling 设置为Objects。虽然这不是一个完美的解决方案。有关使用 ReferenceLoopHandling.IgnorePreserveReferencesHandling.Objects 之间差异的详细说明,请参阅 this question。

【讨论】:

【参考方案2】:

我写了一个最小的程序来测试这个。这是我的github: https://github.com/assafwo1/TestSerializeJsonObjects。这是代码:

using Newtonsoft.Json;
using System.Diagnostics;

namespace TestSerializeJsonObjects

    class Program
    
        public class Node
        
            public Node Next  get; set; 
        
        static void Main(string[] args)
        
            // create new node
            var head = new Node();
            // point its "next" field at itself
            head.Next = head;
            // this is now the smallest circular reference data structure possible 
            // assert that head next is head
            Debug.Assert(head.Next == head);
            // save to string
            var s = JsonConvert.SerializeObject(head, new JsonSerializerSettings
            
                PreserveReferencesHandling = PreserveReferencesHandling.Objects
            );
            // get from string
            var head2 = JsonConvert.DeserializeObject<Node>(s, new JsonSerializerSettings
            
                PreserveReferencesHandling = PreserveReferencesHandling.Objects
            );
            // assert that head2 next is head2
            Debug.Assert(head2.Next == head2);
            // done
        
    

【讨论】:

嗨,阿萨夫,沙洛姆,欢迎来到 SO。很高兴您花时间回答问题,但最好为您的代码添加解释,而不仅仅是从您的 Github 中粘贴代码……我建议您编辑您的代码并添加一些解释。

以上是关于您如何“真正”使用 Newtonsoft.Json 序列化循环引用对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 C# 进行条件序列化 - NewtonSoft.Json

如何使用 Newtonsoft.JSON 解析 JSON 响应? [复制]

如何使用 Newtonsoft.Json 反序列化 JSON 数组

如何使用Newtonsoft.Json生成这样的json字符串

如何在MS构建任务中使用NewtonSoft.json?

在Asp.Net Core 3.0中如何使用 Newtonsoft.Json 库序列化数据