c# - Reference to automatically created objects -
i attempting serialize , deserialize complex object graph:
class a
contains read property containing immutable array of objects of type b
. objects of type b
, immutable array, created in constructor of type a
.
other types contain references objects of type b
obtained accessing array of object of type a
.
during deserialization, need references b
end pointing @ appropriate object created a
constructor index, rather making brand new b
objects json. i'm trying use preservereferenceshandling
json.net. understandably not work, because attempts use deserialized versions of b
rather a
-constructed versions.
is there strategy can use here without modifying types?
edit: clarify , make extremely clear, solution must not modify type itself. can touch contract resolver, binder, reference resolver, etc. not type. also, b
types cannot deserialized. must made a
's constructor.
update
your question doesn't give example of trying accomplish, i'm guessing of design requirements. confirm, situation is:
- you have complex graph of objects serialize json.net
- throughout graph, there many instances of class
a
. a
contains immutable array of instances of classb
that can ever constructed inside constructor ofa
.- each instance of
a
might or might not have properties serialize (not specified) - each instance of
b
might or might not have properties serialize (not specified). - also throughout graph there many references instances of
b
, in cases these references point instance ofb
inside 1 of instances ofa
. - when deserialize graph, need references instance of
b
to point instance ofb
inside instance ofa
corresponding original instance, array index. - you don't have code collect , discover instances of
a
in object graph. - you cannot touch c# code classes in way, not add data contract attributes or private properties.
let's model situation following classes:
public abstract class b { public int index { get; set; } // property modified. } public class { public class bactual : b { } static int nextid = -1; readonly b[] items; // private read-only array never changed. public a() { items = enumerable.range(101 + 10 * interlocked.increment(ref nextid), 2).select(i => new bactual { index = }).toarray(); } public string someproperty { get; set; } public ienumerable<b> items { { foreach (var b in items) yield return b; } } public string someotherproperty { get; set; } } public class midclass { public midclass() { anothera = new a(); } public anothera { get; set; } } public class mainclass { public mainclass() { a1 = new a(); midclass = new midclass(); a2 = new a(); } public list<b> listofb { get; set; } public a2 { get; set; } public midclass midclass { get; set; } public a1 { get; set; } }
then, serialize, need use json.net collect instances of a
in object graph. next, preservereferenceshandling = preservereferenceshandling.objects
set, serialize proxy class containing table of instances of a
first item, root object second item.
to deserialize, preservereferenceshandling.objects
must deserialize proxy class using jsonconverter
a
deserializes properties (if any) of a
, b
, , adds reference serialized "$ref"
references b
new instances of b
allocated in constructor of a
.
thus:
// used enable json.net traverse object hierarchy without writing data. public class nulljsonwriter : jsonwriter { public nulljsonwriter() : base() { } public override void flush() { // nothing. } } public class typeinstancecollector<t> : jsonconverter t : class { readonly list<t> instancelist = new list<t>(); readonly hashset<t> instances = new hashset<t>(); public list<t> instancelist { { return instancelist; } } public override bool canconvert(type objecttype) { return typeof(t).isassignablefrom(objecttype); } public override bool canread { { 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 write here. null suffices. writer.writenull(); } } public class adeserializer : jsonconverter { public override bool canconvert(type objecttype) { return typeof(a).isassignablefrom(objecttype); } public override bool canwrite { { 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; var refid = (string)obj["$ref"]; if (refid != null) { = (a)serializer.referenceresolver.resolvereference(serializer, refid); if (a != null) return a; } = ((a)existingvalue) ?? new a(); var items = obj["items"]; obj.remove("items"); // populate properties other items, if // updates referenceresolver table. using (var objreader = obj.createreader()) serializer.populate(objreader, a); // populate properties of b items, if if (items != null) { if (items.type != jtokentype.array) throw new jsonserializationexception("items not 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 b class has no properties deserialize, var id = (string)pair.jobj["$id"]; if (id != null) serializer.referenceresolver.addreference(serializer, id, pair.itemb); #else // if b class has properties deserialize, using (var objreader = pair.jobj.createreader()) { // again, populate updates 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 instances of 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); } // serialize proxt class collected instances of @ beginning, establish reference ids 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) { // testing. in case apparently can't 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, can use [jsonconstructor]
attribute specify json.net should use non-default constructor deserialize class a
. doing allow deserialize immutable collection. constructor can private, can continue create instances of b
in pre-existing public constructor. note constructor argument names must match original property names.
secondly, if set preservereferenceshandling = preservereferenceshandling.objects
, other objects in object graph refer directly instances of b
held immutable array will, when serialized , deserialized, continue refer directly instances in deserialized immutable array. i.e., should work.
consider following test case:
public class b { public int index { get; set; } } public class { static int nextid = -1; readonly b [] items; // private read-only array never changed. [jsonconstructor] private a(ienumerable<b> items, string someproperty) { this.items = (items ?? enumerable.empty<b>()).toarray(); this.someproperty = someproperty; } // // create instances of "b" different properties each time default constructor called. public a() : this(enumerable.range(101 + 10*interlocked.increment(ref nextid), 2).select(i => new b { index = }), "foobar") { } public ienumerable<b> items { { foreach (var b in items) yield return b; } } [jsonignore] public int count { { 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 { get; set; } public list<b> listofb { get; set; } public static void test() { var = new a() { someotherproperty = "something else" }; var test = new testclass { = 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 pointers in "listofb" equal pointers in a.items debug.assert(test2.listofb.all(i2 => test2.a.items.contains(i2, new referenceequalitycomparer<b>()))); // assert deserialized data same 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 case have created class b
data, class a
contains immutable collection of b
creates in public constructor, , encompassing class testclass
contains instance of a
, list of items b
taken a
. when serialize this, 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 deserialize it, assert deserialized items b
in listofb
have pointer equality 1 of instances of b
in a.items
. assert deserialized properties have same values in originals, confirming non-default private constructor called deserialize immutable collection.
is want?
for checking pointer equality of instances of b
, use:
public class referenceequalitycomparer<t> : iequalitycomparer<t> 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 }
Comments
Post a Comment