如何让JSON.NET忽略对象关系?

31

我正在开发一个Entity Framework项目。 我想序列化一堆实体类的实例。 我已经将它们绑定到了一个容器类中:

public class Pseudocontext
{
    public List<Widget> widgets;
    public List<Thing> things;

等等……我正在尝试序列化这个类的实例。我希望JSON.NET序列化每个实体类实例中实际上是底层数据库列的成员。我不希望它甚至尝试序列化对象引用。

特别地,我的实体类有虚成员,让我编写C#代码来浏览所有的实体关系而不必担心实际的键值、连接等等,我希望JSON.NET忽略我实体类相关部分。

表面上看,似乎有一个JSON.NET配置选项正好符合我的要求:

JsonSerializer serializer = new JsonSerializer();
serializer.PreserveReferencesHandling = PreserveReferencesHandling.None;

不幸的是,JSON.NET似乎忽略了上面的第二个语句。

实际上,我找到了一个网页(http://json.codeplex.com/workitem/24608),其中有人向James Newton-King本人提出了同样的问题,他的回复(全部内容)是"编写自定义合同解析器"。

尽管我认为这种回答是不充分的,但我一直在尝试遵循它的指导。我非常希望能够编写一个"合同解析器",它只忽略原始类型、字符串、DateTime对象和我的自定义Pseudocontext类以及它直接包含的列表。如果有人有类似的示例,那可能就是我需要的全部内容。以下是我自己想出来的:

public class WhatDecadeIsItAgain : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        JsonContract contract = base.CreateContract(objectType);
        if (objectType.IsPrimitive || objectType == typeof(DateTime) || objectType == typeof(string)
            || objectType == typeof(Pseudocontext) || objectType.Name.Contains("List"))
        {
            contract.Converter = base.CreateContract(objectType).Converter;
        }
        else
        {
            contract.Converter = myDefaultConverter;
        }
        return contract;
    }
    private static GeeThisSureTakesALotOfClassesConverter myDefaultConverter = new GeeThisSureTakesALotOfClassesConverter();
}

public class GeeThisSureTakesALotOfClassesConverter : Newtonsoft.Json.Converters.CustomCreationConverter<object>
{
    public override object Create(Type objectType)
    {
        return null;
    }
}

当我尝试使用上面的方法(通过将 serializer.ContractResolver 设置为 WhatDecadeIsItAgain 的一个实例来进行序列化),在序列化期间会发生 OutOfMemory 错误,这表明 JSON.NET 遇到了永远不会终止的引用循环(尽管我已经努力让 JSON.NET "忽略对象引用")。
我觉得我的“自定义合同解析器”可能是错的。如上所示,它是围绕着这样的前提构建的:对于我想要序列化的类型,我应该返回默认的“合同”,对于所有其他类型,返回一个只返回“null”的“合同”。

我不知道这些假设有多正确,这很难说。JSON.NET的设计非常基于实现继承、方法重写等;我不是一个面向对象编程的人,我觉得这种设计相当晦涩难懂。如果有一个“自定义合同解析器”接口,我就可以实现它,Visual Studio 2012就能很快地生成所需的方法存根,我想我填充这些存根以实现真正的逻辑也不会有太大的问题。

举个例子,我可以轻松地编写一个方法,如果要序列化一个指定类型的对象,则返回“true”,否则返回“false”。也许我漏掉了什么,但我没有找到任何需要重写的方法,也没有找到假设接口(ICustomContractResolver?)告诉我上述代码片段中我应该做什么。

此外,我意识到有些 JSON.NET 属性([JsonIgnore]?)是专门设计用来处理这种情况的。但是我无法使用该方法,因为我正在使用“模型优先”的方法。除非我决定彻底重构整个项目架构,否则实体类将自动生成,它们不会包含 JsonIgnore 属性,也不会在编辑自动化类时感到舒适。
顺便说一句,有一段时间,我设置了对象引用序列化,并且只忽略了所有超额的“$ref”和“$id”数据,而 JSON.NET 在其序列化输出中返回了这些数据。至少目前,我已经放弃了这种方法,因为(相当突然地)序列化开始花费很多时间(大约 45 分钟才能获取约 5 MB 的 JSON)。
我无法将这种性能的突然变化与我所做的任何具体事情联系起来。如果说有什么区别的话,我的数据库中的数据量现在比序列化所需的合理时间要少。但如果能够实现返回到以前的状态(其中我只需要忽略“$ref”、“$id”等内容),那我将非常高兴。

目前,我也愿意考虑使用其他的JSON库,或者完全不同的策略。我感觉我可以使用StringBuilder、System.Reflection等来自己制定一个解决方案...但是JSON.NET不应该能够轻松处理这种情况吗?

4个回答

52

首先,针对您在引用循环方面遇到的问题--PreserveReferencesHandling设置控制着Json.Net是否发出$id$ref来跟踪对象之间的引用。如果您将此设置为None且您的对象图包含循环,则还需要将ReferenceLoopHandling设置为Ignore以防止错误。

现在,要让Json.Net完全忽略所有对象引用并只序列化基元属性(当然除了您的Pseudocontext类),您确实需要一个自定义合同解析器,就像您建议的那样。但是不要担心,这并不像你想象的那么难。解析器具有注入每个属性的ShouldSerialize方法的能力以控制是否应在输出中包含该属性。因此,您需要做的就是从默认解析器派生您的解析器,然后重写CreateProperty方法,使其适当地设置ShouldSerialize。(您不需要在这里使用自定义JsonConverter,尽管可以使用该方法解决此问题。但是它需要更多的代码。)

以下是解析器的代码:

class CustomResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty prop = base.CreateProperty(member, memberSerialization);

        if (prop.DeclaringType != typeof(PseudoContext) && 
            prop.PropertyType.IsClass && 
            prop.PropertyType != typeof(string))
        {
            prop.ShouldSerialize = obj => false;
        }

        return prop;
    }
}

这是一个完整的演示,展示了解析器的工作过程。

class Program
{
    static void Main(string[] args)
    {
        // Set up some dummy data complete with reference loops
        Thing t1 = new Thing { Id = 1, Name = "Flim" };
        Thing t2 = new Thing { Id = 2, Name = "Flam" };

        Widget w1 = new Widget
        {
            Id = 5,
            Name = "Hammer",
            IsActive = true,
            Price = 13.99M,
            Created = new DateTime(2013, 12, 29, 8, 16, 3),
            Color = Color.Red,
        };
        w1.RelatedThings = new List<Thing> { t2 };
        t2.RelatedWidgets = new List<Widget> { w1 };

        Widget w2 = new Widget
        {
            Id = 6,
            Name = "Drill",
            IsActive = true,
            Price = 45.89M,
            Created = new DateTime(2014, 1, 22, 2, 29, 35),
            Color = Color.Blue,
        };
        w2.RelatedThings = new List<Thing> { t1 };
        t1.RelatedWidgets = new List<Widget> { w2 };

        // Here is the container class we wish to serialize
        PseudoContext pc = new PseudoContext
        {
            Things = new List<Thing> { t1, t2 },
            Widgets = new List<Widget> { w1, w2 }
        };

        // Serializer settings
        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ContractResolver = new CustomResolver();
        settings.PreserveReferencesHandling = PreserveReferencesHandling.None;
        settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
        settings.Formatting = Formatting.Indented;

        // Do the serialization and output to the console
        string json = JsonConvert.SerializeObject(pc, settings);
        Console.WriteLine(json);
    }

    class PseudoContext
    {
        public List<Thing> Things { get; set; }
        public List<Widget> Widgets { get; set; }
    }

    class Thing
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public List<Widget> RelatedWidgets { get; set; }
    }

    class Widget
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsActive { get; set; }
        public decimal Price { get; set; }
        public DateTime Created { get; set; }
        public Color Color { get; set; }
        public List<Thing> RelatedThings { get; set; }
    }

    enum Color { Red, White, Blue }
}

输出:

{
  "Things": [
    {
      "Id": 1,
      "Name": "Flim"
    },
    {
      "Id": 2,
      "Name": "Flam"
    }
  ],
  "Widgets": [
    {
      "Id": 5,
      "Name": "Hammer",
      "IsActive": true,
      "Price": 13.99,
      "Created": "2013-12-29T08:16:03",
      "Color": 0
    },
    {
      "Id": 6,
      "Name": "Drill",
      "IsActive": true,
      "Price": 45.89,
      "Created": "2014-01-22T02:29:35",
      "Color": 2
    }
  ]
}

希望这符合你的期望。


是的,这完全解释清楚了。你的CreateProperty()实现特别是我所缺少的。其余部分都很容易被绊倒。总体结果是一个非常有用的例子。 - user1172763
谢谢,很高兴你觉得它有用。 - Brian Rogers
2
这真的帮我省去了在模型中装饰所有属性的麻烦,非常感谢! - rsalfonso
嗨@BrianRogers,这对我有效。但是我在CustomResolver类中找不到prop.DeclaringType != typeof(PseudoContext),所以我只好将其注释掉了。 - Dilip Langhanoja

8

另外,如果你正在寻找一种方法来针对具有不同成员类型名称(例如,使用实体框架创建了一些模型)的所有模型类执行此操作,那么this answer可以提供帮助,并且您可以通过它在JSON序列化中忽略导航属性。


5

一种更简单的方法是修改您的模型T4模板(.tt),将[JsonIgnore]属性附加到导航属性上,这将只保留基元类型可序列化。


非常感谢!我去了我的模型并使用了属性,然后就完成了!这似乎是一个非常简单但有用的解决方案,可以根据所选答案创建我们自己的。 - Guillermo Perez

0

还有另一个简单的解决方案。将以下行添加到您的DbContext构造函数中:

 public MyContextEntities() : base("name=MyContextEntities")
    {
        Configuration.ProxyCreationEnabled = false;
    }

JsonConvert不会序列化System.Data.Entity.DynamicProxies.*命名空间中的所有内容。 实际上,您将获得一个JSON,其中仅填充了您在Include()中添加的依赖项。


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