How to Serialize an Object Graph Without Losing Shared References
Two properties can contain equal objects without referring to the same object. That distinction becomes important when serializing a document model, dependency graph or editor state. If two views share one child, rebuilding two independent copies can change the application's behaviour. If a child refers back to its parent, a serializer also needs a way to represent that cycle without endlessly nesting another copy.
Sphere10's serialization machinery tracks references within a graph. This tutorial creates a root with two links to one child, then gives that child a link back to the root. After a binary round-trip, we check the relationships with ReferenceEquals. The result is a new graph in memory whose internal sharing and cycle match the original.
Contents
Define What Must Survive
There are three different requirements in this example. The child's data must survive. Both root properties must still refer to one child instance. Finally, the child's parent link must refer to the reconstructed root, not to the discarded original root. Comparing names alone proves only the first requirement and does not check the other two.
Diagram source
flowchart TB
root["Reconstructed root object"]
child["One reconstructed shared child"]
root -->|"Primary"| child
root -->|"Secondary"| child
child -->|"Primary: back-reference"| root
This is graph serialization, not persistence by business identity. If two independently constructed customers happen to have the same customer number, reference tracking does not automatically merge them into one entity. Choose the identity rule deliberately. The serializer preserves the supplied object relationships, while an ObjectSpace identifier index addresses business identity.
Install the Framework
dotnet new console -n GraphRoundTrip --framework net10.0
dotnet add GraphRoundTrip package Sphere10.Framework --version 3.1.3
The example uses a public model with a parameterless constructor and writable properties. The two relationship properties use object, allowing the factory's polymorphic/reference machinery to describe their concrete values. That is a compact teaching model. A real document format should declare the supported model types and compatibility policy instead of accepting an arbitrary application object graph without review.
Create the Model
namespace ArticleSample;
public sealed class GraphNode {
public string Name { get; set; }
public object Primary { get; set; }
public object Secondary { get; set; }
}
Name is ordinary data. Primary and Secondary are relationships whose identity we will test. Keeping the model small helps separate the interesting reference behaviour from a larger application's business rules. It also makes clear that a missing shared child or broken parent cycle must stop the run.
Round-Trip the Graph
using System;
using Sphere10.Framework;
namespace ArticleSample;
public static class Program {
public static void Main() {
// Both root links share one child; the child points back to the root.
var root = new GraphNode { Name = "Root" };
var child = new GraphNode { Name = "Shared child", Primary = root };
root.Primary = child;
root.Secondary = child;
var serializer = SerializerBuilder.FactoryAssemble<GraphNode>();
var bytes = serializer.SerializeToBytes(root, Endianness.LittleEndian);
var copy = serializer.DeserializeBytes(bytes, Endianness.LittleEndian);
// These are identity assertions: equal text is not sufficient.
var shared = ReferenceEquals(copy.Primary, copy.Secondary);
var cycle = ReferenceEquals(((GraphNode)copy.Primary).Primary, copy);
if (!shared || !cycle || ReferenceEquals(root, copy))
throw new InvalidOperationException("Graph identity was not reconstructed.");
Console.WriteLine($"Shared child restored: {shared}");
Console.WriteLine($"Parent cycle restored: {cycle}");
Console.WriteLine($"Root is a new instance: {!ReferenceEquals(root, copy)}");
}
}
FactoryAssemble<GraphNode>() selects the Framework's tested factory-assembly path. The byte-array convenience method supplies one coherent serialization context for the complete root graph, so nested serializers use the same reference bookkeeping. Splitting each property into an unrelated serialization operation would create separate contexts and would not establish cross-operation shared identity.
The byte order is explicit. The reader must use the same endianness as the writer. Likewise, the type registrations, member layout and supported concrete types belong to the format's compatibility contract. Reflection-based assembly is useful, but adding or rearranging members is not an automatic schema migration for already stored data.
Verify the Relationships
Shared child restored: True
Parent cycle restored: True
Root is a new instance: True
The first line proves that the reconstructed root's two links share one instance. The second proves that the child points back to the reconstructed root. The third proves that the operation did not simply hand the caller its old object back. These checks are complementary because a serializer that duplicated every node could retain equal values while failing the first two.
Try adding a second distinct child with the same name as the first. The distinct instances should remain distinct unless your application deliberately canonicalizes them before serialization. Then add a self-reference to a node and verify that the corresponding reconstructed link returns to that same reconstructed node. Such tests make identity requirements executable instead of leaving them implicit in a diagram.
Keep the Trust and Lifetime Boundaries Clear
Do not confuse this inline graph behaviour with experimental cross-dimension object-store garbage collection. This tutorial serializes one bounded root graph and reconstructs it through its serializer. It does not claim that every object graph can be automatically migrated, that external resources are restored, or that file handles, callbacks and live services belong in a portable data record.
Use the serialization guide to choose explicit member bindings and reference wrappers where the application needs a stable format. The cyclic-reference fixtures provide further focused cases. From there, the Sphere10 Framework storage capabilities show how serialization participates in stream-backed collections and ObjectSpaces.
Download the Example
Download the complete NuGet sample project. The archive contains the project, its model files and run instructions. Keep the pinned package version when reproducing the example.
Continue the Series
How to Store Serializer Definitions Alongside Your Data
How to Build an Embedded Object Database in C#
How to Build a Self-Describing Object Store