Generally speaking, the way to deal with this issue is, during object graph serialization, to replace the singleton(s) using a serialization surrogate mechanism, wherein the singleton is replaced with a Data Transfer Object (DTO) that contains nothing other than an identifier for the singleton. Then, later, as the graph is being deserialized, the DTO is initially deserialized, then replaced with the corresponding singleton via a lookup.
For instance, if your MyClass
looks as follows:
public sealed class MyClass
{
private static readonly Dictionary<string, MyClass> Cache;
static MyClass()
{
Cache = new Dictionary<string, MyClass>()
{
{ "one", new MyClass("one") { OtherRuntimeData = "other runtime data 1" } },
{ "two", new MyClass("two") { OtherRuntimeData = "other runtime data 2" } },
};
}
// XmlSerializer required parameterless constructor.
private MyClass() => throw new NotImplementedException();
private MyClass(string name) => this.Name = name;
public string Name { get; }
public string OtherRuntimeData { get; set; }
public static MyClass Parse(string from) => Cache[from];
public static IEnumerable<MyClass> Instances => Cache.Values;
}
Then a DTO containing only the Name
would look like:
public sealed class MyClassDTO
{
public string Name { get; set; }
public static implicit operator MyClassDTO(MyClass obj) => obj == null ? null : new MyClassDTO { Name = obj.Name };
public static implicit operator MyClass(MyClassDTO dto) => dto == null ? null : MyClass.Parse(dto.Name);
}
Notice the implicit operator for converting between the DTO and the original? That will make it easier to inject the surrogate into the serialization graph.
Unfortunately, however, there is no standard way to implement injection of a serialization surrogate DTO in all of the commonly used .Net serializers. Each has its own separate mechanism (or no mechanism at all). To break them down:
The data contract serializers support replacement with a surrogate via the IDataContractSurrogate
interface, as shown e.g. in this answer to DataContractJsonSerializer - share an object instance for the whole graph? or this answer to How to serialize / deserialize immutable list type in c#.
The data contract serializers also support the IObjectReference
interface as shown in this answer to C# DataContract Serialization, how to deserialize to already existing instance.
But unfortunately XmlSerializer
has no convenient support for injection of DTOs via a surrogate mechanism. The closest one can come is the trick from this answer to Most elegant XML serialization of Color structure, which involves setting the XmlElementAttribute.Type
to the DTO type for every property that refers to your singleton type, for instance as follows:
public class RootObject
{
// Technique taken from https://stackoverflow.com/questions/3280362/most-elegant-xml-serialization-of-color-structure
[XmlElement(Type=typeof(MyClassDTO))]
public MyClass MyClass { get; set; }
}
Sample .Net fiddle showing this in action here: https://dotnetfiddle.net/R07NiW. Note this only works because of the implicit operator between the singleton and its DTO that we defined previously.
When serializing via Json.NET, the singleton can be replaced with its DTO inside a custom JsonConverter
.
protobuf.net supports replacement with surrogates via RuntimeTypeModel.Default.Add(typeof(OriginalType), false) .SetSurrogate(typeof(SurrogateType))
as shown in Can I serialize arbitrary types with protobuf-net?, No serializer defined for type: System.Windows.Media.Media3D.Point3D, or the article Protobuf-net: the unofficial manual.
Finally, if you are using BinaryFormatter
(which I do not recommend for reasons explained here), use of the SurrogateSelector
mechanism, which resembles the data contract surrogate replacement mechanism, is supported as is shown in Customized Serialization.
SerializationInfo.SetType()
combined with IObjectReference
is also supported, as is shown in this documentation example.