Reference to automatically created objects

余生长醉 提交于 2019-12-10 17:18:38

问题


I am attempting to serialize and deserialize a complex object graph:

Class A contains an read only property containing an immutable array of objects of type B. The objects of type B, as well as the immutable array, are created in the constructor of type A.

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

During deserialization, I need any references to a B to end up pointing at the appropriate object created by the A constructor by index, rather than making brand new B objects from JSON. I'm trying to use PreserveReferencesHandling with JSON.NET. This understandably does not work, because it attempts to use deserialized versions of B rather than the A-constructed versions.

Is there another strategy I can use here without modifying my types?

Edit: To clarify and make extremely clear, the solution must not modify the type itself. You can touch the contract resolver, binder, reference resolver, etc. but not the type. Also, B types cannot be deserialized. They must be made by A's constructor.


回答1:


Update

Your question doesn't give an example of what you are trying to accomplish, so I'm guessing about some of your design requirements. To confirm, your situation is:

  1. You have some complex graph of objects to serialize with Json.NET
  2. Throughout the graph, there are many instances of class A.
  3. A contains an immutable array of instances of class B that can only ever be constructed inside the constructor of A.
  4. Each instance of A might or might not have properties to serialize (not specified)
  5. Each instance of B might or might not have properties to serialize (not specified).
  6. Also throughout the graph there are many references to instances of B, but in all cases these references actually point to an instance of B inside one of the instances of A.
  7. When you deserialize your graph, you need all references to an instance of B to to point to an instance of B inside an instance of A corresponding to the original instance, by array index.
  8. You don't have any code to collect and discover all instances of A in your object graph.
  9. You cannot touch the c# code for the classes in any way, not even to add data contract attributes or private properties.

Let's model this situation with the following classes:

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

Then, to serialize, you need to use Json.NET to collect all instances of A in your object graph. Next, with PreserveReferencesHandling = PreserveReferencesHandling.Objects set, serialize a proxy class containing a table of all instances of A as the first item, then your root object as the second item.

To deserialize, with PreserveReferencesHandling.Objects you must deserialize your proxy class using a JsonConverter for A that deserializes properties (if any) of A and B, and adds a reference for the serialized "$ref" references to B to the new instances of B allocated in the constructor of A.

Thus:

// 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's 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
    }
}

Original Answer

Firstly, you can use the [JsonConstructor] attribute to specify that Json.NET should use a non-default constructor to deserialize your class A. Doing so will allow you to deserialize into your immutable collection. This constructor can be private, so that you can continue to create your instances of B in the pre-existing public constructor. Note that the constructor argument names must match the original property names.

Secondly, if you set PreserveReferencesHandling = PreserveReferencesHandling.Objects, then any other objects in your object graph that refer directly to instances of B held by the immutable array will, when serialized and deserialized, continue to refer directly to the instances in the deserialized immutable array. I.e., it should just work.

Consider the following test case:

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

In this case I have created class B with some data, class A which contains an immutable collection of B which it creates in its public constructor, and an encompassing class TestClass that contains an instance of A and a list of items B taken from A. When I serialize this, I get the following 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"
    }
  ]
}

Then, when I deserialize it, I assert that all deserialized items B in ListOfB have pointer equality with one of the instances of B in a.Items. I also assert that all deserialized properties have the same values as in the originals, thus confirming that the non-default private constructor was called to deserialize the immutable collection.

Is this what you want?

For checking pointer equality of instances of B, I use:

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
}


来源:https://stackoverflow.com/questions/30648718/reference-to-automatically-created-objects

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!