// While this function looks complicated, it's actually quite smooth: // The important things to remember is that serialization goes depth first: // The first object to get fully serialised is the first nested one, with // the parent object being last. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var xxxx = serializer.ReferenceLoopHandling; if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } ///////////////////////////////////// // Path one: nulls ///////////////////////////////////// if (value == null) { return; } ///////////////////////////////////// // Path two: primitives (string, bool, int, etc) ///////////////////////////////////// if (value.GetType().IsPrimitive || value is string) { FirstEntry = false; var t = JToken.FromObject(value); // bypasses this converter as we do not pass in the serializer t.WriteTo(writer); return; } ///////////////////////////////////// // Path three: Bases ///////////////////////////////////// if (value is Base && !(value is ObjectReference)) { if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } var obj = value as Base; FirstEntry = false; //TotalProcessedCount++; // Append to lineage tracker Lineage.Add(Guid.NewGuid().ToString()); var jo = new JObject(); var propertyNames = obj.GetDynamicMemberNames(); var contract = (JsonDynamicContract)serializer.ContractResolver.ResolveContract(value.GetType()); // Iterate through the object's properties, one by one, checking for ignored ones foreach (var prop in propertyNames) { if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } // Ignore properties starting with a double underscore. if (prop.StartsWith("__")) { continue; } var property = contract.Properties.GetClosestMatchProperty(prop); // Ignore properties decorated with [JsonIgnore]. if (property != null && property.Ignored) { continue; } // Ignore nulls object propValue = obj[prop]; if (propValue == null) { continue; } // Check if this property is marked for detachment: either by the presence of "@" at the beginning of the name, or by the presence of a DetachProperty attribute on a typed property. if (property != null) { var detachableAttributes = property.AttributeProvider.GetAttributes(typeof(DetachProperty), true); if (detachableAttributes.Count > 0) { DetachLineage.Add(((DetachProperty)detachableAttributes[0]).Detachable); } else { DetachLineage.Add(false); } var chunkableAttributes = property.AttributeProvider.GetAttributes(typeof(Chunkable), true); if (chunkableAttributes.Count > 0) { //DetachLineage.Add(true); // NOOPE serializer.Context = new StreamingContext(StreamingContextStates.Other, chunkableAttributes[0]); } else { //DetachLineage.Add(false); serializer.Context = new StreamingContext(); } } else if (prop.StartsWith("@")) // Convention check for dynamically added properties. { DetachLineage.Add(true); } else { DetachLineage.Add(false); } // Set and store a reference, if it is marked as detachable and the transport is not null. if (WriteTransports != null && WriteTransports.Count != 0 && propValue is Base && DetachLineage[DetachLineage.Count - 1]) { var what = JToken.FromObject(propValue, serializer); // Trigger next. if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } var refHash = ((JObject)what).GetValue("id").ToString(); var reference = new ObjectReference() { referencedId = refHash }; TrackReferenceInTree(refHash); jo.Add(prop, JToken.FromObject(reference)); } else { jo.Add(prop, JToken.FromObject(propValue, serializer)); // Default route } // Pop detach lineage. If you don't get this, remember this thing moves ONLY FORWARD, DEPTH FIRST DetachLineage.RemoveAt(DetachLineage.Count - 1); } // Check if we actually have any transports present that would warrant a if ((WriteTransports != null && WriteTransports.Count != 0) && RefMinDepthTracker.ContainsKey(Lineage[Lineage.Count - 1])) { jo.Add("__closure", JToken.FromObject(RefMinDepthTracker[Lineage[Lineage.Count - 1]])); } var hash = Models.Utilities.hashString(jo.ToString()); if (!jo.ContainsKey("id")) { jo.Add("id", JToken.FromObject(hash)); } jo.WriteTo(writer); if ((DetachLineage.Count == 0 || DetachLineage[DetachLineage.Count - 1]) && WriteTransports != null && WriteTransports.Count != 0) { var objString = jo.ToString(); var objId = jo["id"].Value <string>(); OnProgressAction?.Invoke("S", 1); foreach (var transport in WriteTransports) { if (CancellationToken.IsCancellationRequested) { continue; // Check for cancellation } transport.SaveObject(objId, objString); } } // Pop lineage tracker Lineage.RemoveAt(Lineage.Count - 1); return; } ///////////////////////////////////// // Path four: lists/arrays & dicts ///////////////////////////////////// if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } var type = value.GetType(); // TODO: List handling and dictionary serialisation handling can be sped up significantly if we first check by their inner type. // This handles a broader case in which we are, essentially, checking only for object[] or List<object> / Dictionary<string, object> cases. // A much faster approach is to check for List<primitive>, where primitive = string, number, etc. and directly serialize it in full. // Same goes for dictionaries. if (typeof(IEnumerable).IsAssignableFrom(type) && !typeof(IDictionary).IsAssignableFrom(type) && type != typeof(string)) { if (TotalProcessedCount == 0 && FirstEntry) { FirstEntry = false; FirstEntryWasListOrDict = true; TotalProcessedCount += 1; DetachLineage.Add(WriteTransports != null && WriteTransports.Count != 0 ? true : false); } JArray arr = new JArray(); // Chunking large lists into manageable parts. if (DetachLineage[DetachLineage.Count - 1] && serializer.Context.Context is Chunkable chunkInfo) { var maxCount = chunkInfo.MaxObjCountPerChunk; var i = 0; var chunkList = new List <DataChunk>(); var currChunk = new DataChunk(); foreach (var arrValue in ((IEnumerable)value)) { if (i == maxCount) { chunkList.Add(currChunk); currChunk = new DataChunk(); i = 0; } currChunk.data.Add(arrValue); i++; } chunkList.Add(currChunk); value = chunkList; } foreach (var arrValue in ((IEnumerable)value)) { if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } if (arrValue == null) { continue; } if (WriteTransports != null && WriteTransports.Count != 0 && arrValue is Base && DetachLineage[DetachLineage.Count - 1]) { var what = JToken.FromObject(arrValue, serializer); // Trigger next var refHash = ((JObject)what).GetValue("id").ToString(); var reference = new ObjectReference() { referencedId = refHash }; TrackReferenceInTree(refHash); arr.Add(JToken.FromObject(reference)); } else { arr.Add(JToken.FromObject(arrValue, serializer)); // Default route } } if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } arr.WriteTo(writer); if (DetachLineage.Count == 1 && FirstEntryWasListOrDict) // are we in a list entry point case? { DetachLineage.RemoveAt(0); } return; } if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } if (typeof(IDictionary).IsAssignableFrom(type)) { if (TotalProcessedCount == 0 && FirstEntry) { FirstEntry = false; FirstEntryWasListOrDict = true; TotalProcessedCount += 1; DetachLineage.Add(WriteTransports != null && WriteTransports.Count != 0 ? true : false); } var dict = value as IDictionary; var dictJo = new JObject(); foreach (DictionaryEntry kvp in dict) { if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } if (kvp.Value == null) { continue; } JToken jToken; if (WriteTransports != null && WriteTransports.Count != 0 && kvp.Value is Base && DetachLineage[DetachLineage.Count - 1]) { var what = JToken.FromObject(kvp.Value, serializer); // Trigger next var refHash = ((JObject)what).GetValue("id").ToString(); var reference = new ObjectReference() { referencedId = refHash }; TrackReferenceInTree(refHash); jToken = JToken.FromObject(reference); } else { jToken = JToken.FromObject(kvp.Value, serializer); // Default route } dictJo.Add(kvp.Key.ToString(), jToken); } dictJo.WriteTo(writer); if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } if (DetachLineage.Count == 1 && FirstEntryWasListOrDict) // are we in a dictionary entry point case? { DetachLineage.RemoveAt(0); } return; } ///////////////////////////////////// // Path five: everything else (enums?) ///////////////////////////////////// if (CancellationToken.IsCancellationRequested) { return; // Check for cancellation } FirstEntry = false; var lastCall = JToken.FromObject(value); // bypasses this converter as we do not pass in the serializer lastCall.WriteTo(writer); }