Link to automatically created objects

I am trying to serialize and deserialize a complex graph of an object:

The class Acontains a read-only property containing an immutable array of type objects B. Type objects B, as well as an immutable array, are created in the type constructor A.

Other types contain references to type objects Bthat are obtained by accessing an array of the type object A.

During deserialization, I need references to B, in order to ultimately point to the corresponding object created by the constructor Aby index, instead of creating new objects Bfrom JSON. I am trying to use PreserveReferencesHandlingwith JSON.NET. This clearly does not work, because it is trying to use deserialized versions Brather than non- Adesigned versions.

Is there any other strategy that I can use here without changing my types?

Edit: In order to clarify and make it very clear, the solution should not change the type itself. You can touch the contract recognizer, the binder, the reference recognizer, etc., but not the type. In addition, types Bcannot be deserialized. They must be created by the constructor A.

+4
source share
1 answer

Update

Your question does not provide an example of what you are trying to accomplish, so I guess some of your design requirements. To confirm your situation:

  • You have a complex graph of objects for serialization using Json.NET
  • There are many instances of the class throughout the graph A.
  • Acontains an immutable array of class instances Bthat can only ever be built inside the constructor A.
  • Each instance Amay or may not have properties for serialization (not specified)
  • Each instance Bmay or may not have properties for serialization (not specified).
  • B, B A.
  • B, B A, , .
  • A .
  • # , .

:

public abstract class B
{
    public int Index { get; set; } // Some property that could be modified.
}

public class A
{
    public class BActual : B
    {
    }

    static int nextId = -1;

    readonly B[] items; // A private read-only array that is never changed.

    public A()
    {
        items = Enumerable.Range(101 + 10 * Interlocked.Increment(ref nextId), 2).Select(i => new BActual { Index = i }).ToArray();
    }

    public string SomeProperty { get; set; }

    public IEnumerable<B> Items
    {
        get
        {
            foreach (var b in items)
                yield return b;
        }
    }

    public string SomeOtherProperty { get; set; }
}

public class MidClass
{
    public MidClass()
    {
        AnotherA = new A();
    }

    public A AnotherA { get; set; }
}

public class MainClass
{
    public MainClass()
    {
        A1 = new A();
        MidClass = new MidClass();
        A2 = new A();
    }

    public List<B> ListOfB { get; set; }

    public A A2 { get; set; }

    public MidClass MidClass { get; set; }

    public A A1 { get; set; }
}

, , Json.NET A . , PreserveReferencesHandling = PreserveReferencesHandling.Objects -, A , .

PreserveReferencesHandling.Objects - JsonConverter A, ( ) A B "$ref" B B, A.

:

// Used to enable Json.NET to traverse an object hierarchy without actually writing any data.
public class NullJsonWriter : JsonWriter
{
    public NullJsonWriter()
        : base()
    {
    }

    public override void Flush()
    {
        // Do nothing.
    }
}

public class TypeInstanceCollector<T> : JsonConverter where T : class
{
    readonly List<T> instanceList = new List<T>();
    readonly HashSet<T> instances = new HashSet<T>();

    public List<T> InstanceList { get { return instanceList; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override bool CanRead { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        T instance = (T)value;
        if (!instances.Contains(instance))
        {
            instanceList.Add(instance);
            instances.Add(instance);
        }
        // It necessary to write SOMETHING here.  Null suffices.
        writer.WriteNull();
    }
}

public class ADeserializer : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(A).IsAssignableFrom(objectType);
    }

    public override bool CanWrite { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = JObject.Load(reader);
        if (obj == null)
            return existingValue;
        A a;

        var refId = (string)obj["$ref"];
        if (refId != null)
        {
            a = (A)serializer.ReferenceResolver.ResolveReference(serializer, refId);
            if (a != null)
                return a;
        }

        a = ((A)existingValue) ?? new A();

        var items = obj["Items"];
        obj.Remove("Items");

        // Populate properties other than the items, if any
        // This also updates the ReferenceResolver table.
        using (var objReader = obj.CreateReader())
            serializer.Populate(objReader, a);

        // Populate properties of the B items, if any
        if (items != null)
        {
            if (items.Type != JTokenType.Array)
                throw new JsonSerializationException("Items were not an array");
            var itemsArray = (JArray)items;
            if (a.Items.Count() < itemsArray.Count)
                throw new JsonSerializationException("too few items constructucted"); // Item counts must match
            foreach (var pair in a.Items.Zip(itemsArray, (b, o) => new { ItemB = b, JObj = o }))
            {
#if false
                // If your B class has NO properties to deserialize, do this
                var id = (string)pair.JObj["$id"];
                if (id != null)
                    serializer.ReferenceResolver.AddReference(serializer, id, pair.ItemB);
#else
                // If your B class HAS SOME properties to deserialize, do this
                using (var objReader = pair.JObj.CreateReader())
                {
                    // Again, Populate also updates the ReferenceResolver table
                    serializer.Populate(objReader, pair.ItemB);
                }
#endif
            }
        }

        return a;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public class RootProxy<TRoot, TTableItem>
{
    [JsonProperty("table", Order = 1)]
    public List<TTableItem> Table { get; set; }

    [JsonProperty("data", Order = 2)]
    public TRoot Data { get; set; }
}

public class TestClass
{
    public static string Serialize(MainClass main)
    {
        // First, collect all instances of A 
        var collector = new TypeInstanceCollector<A>();

        var collectionSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, Converters = new JsonConverter[] { collector } };
        using (var jsonWriter = new NullJsonWriter())
        {
            JsonSerializer.CreateDefault(collectionSettings).Serialize(jsonWriter, main);
        }

        // Now serialize a proxt class with the collected instances of A at the beginning, to establish reference ids for all instances of B.
        var proxy = new RootProxy<MainClass, A> { Data = main, Table = collector.InstanceList };
        var serializationSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects };

        return JsonConvert.SerializeObject(proxy, Formatting.Indented, serializationSettings);
    }

    public static MainClass Deserialize(string json)
    {
        var serializationSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, Converters = new JsonConverter[] { new ADeserializer() } };
        var proxy = JsonConvert.DeserializeObject<RootProxy<MainClass, A>>(json, serializationSettings);

        return proxy.Data;
    }

    static IEnumerable<A> GetAllA(MainClass main)
    {
        // For testing.  In your case apparently you can't do this manually.
        if (main.A1 != null)
            yield return main.A1;
        if (main.A2 != null)
            yield return main.A2;
        if (main.MidClass != null && main.MidClass.AnotherA != null)
            yield return main.MidClass.AnotherA;
    }

    static IEnumerable<B> GetAllB(MainClass main)
    {
        return GetAllA(main).SelectMany(a => a.Items);
    }

    public static void Test()
    {
        var main = new MainClass();
        main.A1.SomeProperty = "main.A1.SomeProperty";
        main.A1.SomeOtherProperty = "main.A1.SomeOtherProperty";

        main.A2.SomeProperty = "main.A2.SomeProperty";
        main.A2.SomeOtherProperty = "main.A2.SomeOtherProperty";

        main.MidClass.AnotherA.SomeProperty = "main.MidClass.AnotherA.SomeProperty";
        main.MidClass.AnotherA.SomeOtherProperty = "main.MidClass.AnotherA.SomeOtherProperty";

        main.ListOfB = GetAllB(main).Reverse().ToList();

        var json = Serialize(main);

        var main2 = Deserialize(json);

        var json2 = Serialize(main2);

        foreach (var b in main2.ListOfB)
            Debug.Assert(GetAllB(main2).Contains(b)); // No assert
        Debug.Assert(json == json2); // No assert
        Debug.Assert(main.ListOfB.Select(b => b.Index).SequenceEqual(main2.ListOfB.Select(b => b.Index))); // No assert
        Debug.Assert(GetAllA(main).Select(a => a.SomeProperty + a.SomeOtherProperty).SequenceEqual(GetAllA(main2).Select(a => a.SomeProperty + a.SomeOtherProperty))); // No assert
    }
}

-, [JsonConstructor], , Json.NET A. . , B . , .

-, PreserveReferencesHandling = PreserveReferencesHandling.Objects, , B, , , . I.e., .

:

public class B
{
    public int Index { get; set; }
}

public class A
{
    static int nextId = -1;

    readonly B [] items; // A private read-only array that is never changed.

    [JsonConstructor]
    private A(IEnumerable<B> Items, string SomeProperty)
    {
        this.items = (Items ?? Enumerable.Empty<B>()).ToArray();
        this.SomeProperty = SomeProperty;
    }

    // // Create instances of "B" with different properties each time the default constructor is called.
    public A() : this(Enumerable.Range(101 + 10*Interlocked.Increment(ref nextId), 2).Select(i => new B { Index = i }), "foobar") 
    {
    }

    public IEnumerable<B> Items
    {
        get
        {
            foreach (var b in items)
                yield return b;
        }
    }

    [JsonIgnore]
    public int Count { get { return items.Length; } }

    public B GetItem(int index)
    {
        return items[index];
    }

    public string SomeProperty { get; set; }

    public string SomeOtherProperty { get; set; }
}

public class TestClass
{
    public A A { get; set; }

    public List<B> ListOfB { get; set; }

    public static void Test()
    {
        var a = new A() { SomeOtherProperty = "something else" };
        var test = new TestClass { A = a, ListOfB = a.Items.Reverse().ToList() };

        var settings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects };

        var json = JsonConvert.SerializeObject(test, Formatting.Indented, settings);
        Debug.WriteLine(json);
        var test2 = JsonConvert.DeserializeObject<TestClass>(json, settings);

        // Assert that pointers in "ListOfB" are equal to pointers in A.Items
        Debug.Assert(test2.ListOfB.All(i2 => test2.A.Items.Contains(i2, new ReferenceEqualityComparer<B>())));

        // Assert deserialized data is the same as the original data.
        Debug.Assert(test2.A.SomeProperty == test.A.SomeProperty);
        Debug.Assert(test2.A.SomeOtherProperty == test.A.SomeOtherProperty);
        Debug.Assert(test2.A.Items.Select(i => i.Index).SequenceEqual(test.A.Items.Select(i => i.Index)));

        var json2 = JsonConvert.SerializeObject(test2, Formatting.Indented, settings);
        Debug.WriteLine(json2);
        Debug.Assert(json2 == json);
    }
}

B class A, B, , TestClass, A B, A. , JSON:

{
  "$id": "1",
  "A": {
    "$id": "2",
    "Items": [
      {
        "$id": "3",
        "Index": 101
      },
      {
        "$id": "4",
        "Index": 102
      }
    ],
    "SomeProperty": "foobar",
    "SomeOtherProperty": "something else"
  },
  "ListOfB": [
    {
      "$ref": "4"
    },
    {
      "$ref": "3"
    }
  ]
}

, , , B ListOfB B a.Items. , , , , .

, ?

B :

public class ReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
{
    #region IEqualityComparer<T> Members

    public bool Equals(T x, T y)
    {
        return object.ReferenceEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        return (obj == null ? 0 : obj.GetHashCode());
    }

    #endregion
}
+1

All Articles