Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
178 views
in Technique[技术] by (71.8m points)

c# - How do I make JSON.NET ignore object relationships?

I'm working on an Entity Framework project. I want to serialize a bunch of entity class instances. I've bound these together into a container class:

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

Etcetera... it is an instance of this class that I'm attempting to serialize. I want JSON.NET to serialize the members of each entity class instance that are actually columns in the underlying database. I do not want it to even attempt to serialize object references.

In particular, my entity classes have virtual members that allow me to write C# code that navigates all my inter-entity relationships without worrying about actual key values, joins, etc., and I want JSON.NET to ignore the associated parts of my entity classes.

On the surface, there seems to be a JSON.NET configuration option that does exactly what I'm talking about:

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

Unfortunately, JSON.NET seems to be ignoring the second statement above.

I actually found a web page (http://json.codeplex.com/workitem/24608) where someone else brought the same issue to the attention of James Newton-King himself, and his response (in its entirety) was "Write a custom contract resolver."

As inadequate as I find that response to be, I have been attempting to follow its guidance. I would very much like to be able to write a "contract resolver" that ignored everything except primitive types, strings, DateTime objects, and my own Pseudocontext class along with the Lists it contains directly. If someone has an example of something that at least resembles that, it might be all I need. This is what I came up with on my own:

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

When I attempt to use the above (by setting serializer.ContractResolver to an instance of WhatDecadeIsItAgain prior to serialization), I get OutOfMemory errors during serialization that indicate that JSON.NET is encountering reference loops that never terminate (in spite of my efforts to make JSON.NET just ignore object references).

I feel like my "custom contract resolver" may be wrong. As shown above, it's built around the premise that I should return the default "contract" for the types I do want to serialize, and a "contract" that simply returns "null" for all other types.

I have no idea how correct these assumptions are, though, and it's not easy to tell. The JSON.NET design is very much based on implementation inheritance, method overriding, etc.; I'm not much of an OOP guy, and I find that sort of design to be pretty obscure. Were there a "custom contract resolver" interface that I could implement, Visual Studio 2012 would be able to stub out the required methods very quickly, and I imagine I'd have little trouble filling the stubs in with real logic.

I'd have no problem writing, for example, a method that returns "true" if I want to serialize an object of a supplied type and "false" otherwise. Perhaps I'm missing something, but I've found no such method to override, nor have I been able to find the hypothetical interface (ICustomContractResolver ?) that would tell me what I'm actually supposed to be doing in the last code snippet inserted above.

Also, I realize that there are JSON.NET attributes ([JsonIgnore]?) that are designed to deal with situations like this. I can't really use that approach, since I'm using "model first". Unless I decide to tear up my entire project architecture, my entity classes will be automatically generated, and they will not contain JsonIgnore attributes, nor do I feel comfortable editing the automated classes to contain these attributes.

Incidentally, for a while I did have things set up to serialize object references, and I was just ignoring all the superfluous "$ref" and "$id" data that JSON.NET was returning in its serialization output. I've abandoned that approach for the moment at least, because (rather suddenly) serialization started taking an inordinate amount of time (~45 minutes to get ~5 MB of JSON).

I haven't been able to tie that sudden change in performance back to anything specific that I did. If anything, the volume of data in my database is lower now than it was when serialization was actually completing in reasonable time. But I'd be more than happy with a return to the status quo ante (in which I was just having to ignore "$ref", "$id", etc.) if that could be achieved.

At this point, I'm also open to the prospect of using some other JSON library, or a different strategy altogether. I feel like I could just use StringBuilder, System.Reflection, etc. and come of with my own, homemade solution... but isn't JSON.NET supposed to be able to handle this sort of thing pretty easily??

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

First, to address your issues with reference loops-- The PreserveReferencesHandling setting controls whether Json.Net emits $id and $ref to track inter-object references. If you have this set to None and your object graph contains loops, then you will also need to set ReferenceLoopHandling to Ignore to prevent errors.

Now, to get Json.Net to ignore all object references altogether and only serialize primitive properties (except in your Pseudocontext class of course), you do need a custom Contract Resolver, as you suggested. But don't worry, it is not as hard as you think. The resolver has the capability to inject a ShouldSerialize method for each property to control whether or not that property should be included in the output. So, all you need to do is derive your resolver from the default one, then override the CreateProperty method such that it sets ShouldSerialize appropriately. (You do not need a custom JsonConverter here, although it is possible to solve this problem with that approach. It would require quite a bit more code, however.)

Here is the code for the resolver:

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

Here is a full demo showing the resolver in action.

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

Output:

{
  "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
    }
  ]
}

Hope this is in the ballpark of what you were looking for.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...