问题
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:
- You have some complex graph of objects to serialize with Json.NET
- Throughout the graph, there are many instances of class
A
. A
contains an immutable array of instances of classB
that can only ever be constructed inside the constructor ofA
.- Each instance of
A
might or might not have properties to serialize (not specified) - Each instance of
B
might or might not have properties to serialize (not specified). - Also throughout the graph there are many references to instances of
B
, but in all cases these references actually point to an instance ofB
inside one of the instances ofA
. - When you deserialize your graph, you need all references to an instance of
B
to to point to an instance ofB
inside an instance ofA
corresponding to the original instance, by array index. - You don't have any code to collect and discover all instances of
A
in your object graph. - 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