
In the development process, I wanted to serialize a singleton, but with the preservation and restoration of the values of its fields. At first glance, the uncomplicated process turned into a fascinating journey into a rock and cobblestone garden: from receiving two singletones to the lack of field serialization.
But why make a garden garden?
The first logical question is: if so problems, then why is it even needed? Such cunning, indeed, is not often required. Although many people are just starting out with WPF or WinForms, they are trying to implement the application settings file this way. Please do not waste your time and do not reinvent the wheel: there is an Application and User Settings (you can read about it
here and
here ). Here are examples when serialization may be required:
I want to send a singleton over the network or between the AppDomain. For example, the client and the server simultaneously work with the same ftp and synchronize their information about it. Information about ftp can be stored in singleton (and attach methods for working with it in the same place).
The class that is assigned to different elements is serialized, but the value must be the same for all. An example of such a class is
DBNull .
Singleton
As a simple example, take the following singleton:
public sealed class Settings : ISerializable { private static readonly Settings Inst = new Settings(); private Settings() { } public static Settings Instance { get { return Inst; } } public String ServerAddress { get { return _servAddr; } set { _servAddr = value; } } public String Port { get { return _port; } set { _port = value; } } private String _port = ""; }
Immediately make a few comments on the code:
- Intentionally there are no lazy calculations to keep the code simple.
- Properties cannot be made automatic, because the names of hidden class fields are generated again at each compilation, and once an honestly serialized object can no longer be deserialized due to the difference of these names.
First look
In simple cases, for serialization in C #, adding the
Serializable attribute is enough. Well, let's not think much about how complicated our case is and add this attribute. Now let's try to serialize our singleton in three versions:
SOAP ,
Binary, and plain
XML .
For example, serialize and deserialize binary (other methods are similar):
using (var mem = new MemoryStream()) { var serializer = new BinaryFormatter(); serializer.Serialize(mem, Settings.Instance); mem.Seek(0, SeekOrigin.Begin); object o = serializer.Deserialize(mem); Console.WriteLine((Settings)o == Settings.Instance); }
(Not) expectedly on the console will be displayed false, which means that we received two singleton objects.
Such a result can be foreseen if we recall that in the process of deserialization using reflection, a private constructor is called and all deserialized values are assigned to a new object .
UPD : As
kekekeks rightly noted,
it ’s not the private constructor that will be called, but
BinaryFormatter uses FormatterServices.GetSafeUninitializedObject, which allows you to create an instance of an object without calling a constructor.
It is this singularity feature that puts the first stone in our garden: the singleton ceases to be a singleton.
Complicate and ... we put more stones.
Since it didn’t work out easy, we’ll have to complicate it. If we turn to the more “manual” serialization process via the
ISerializable interface, then at first glance there seems to be no benefit: the past misfortune has not disappeared, and the complexity has increased. Therefore, for further actions, we still need a rarely used
IObjectReference interface. All that it does: shows that the object of the class implementing this interface points to another object. It sounds strange, doesn’t it? But we need another feature: after deserialization of such an object, the pointer will be returned not to itself, but to the object to which it points. In our case, it would be logical to return a pointer to a singleton. The class will look like this:
[Serializable] internal sealed class SettingsSerializationHelper : IObjectReference { public Object GetRealObject(StreamingContext context) { return Settings.Instance; } }
Now we can serialize an object of the class
SettingsSerializationHelper , and when deserializing we can get
Settings.Instance . True, there are two more two stones:
- Before you serialize a singleton, you need to create an object of another class.
- Singleton fields are still not serialized.
Consider the first stone, which is not very critical, but clearly not pleasant. The solution to the problem lies in replacing the class for serialization inside
GetObjectData . It will look like this (inside the singleton):
public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SettingsSerializationHelper)); }
Now when we serialize a singleton instead of it, the
SettingsSerializationHelper object will be saved, and during deserialization we will get our singleton back. Checking the console output from the previously described serialization example, we will see that in the case of Binary and SOAP, true will be output to the console, but false for XML serialization. Therefore,
XMLSerializer does not call
GetObjectData and simply processes all public fields / properties on its own.
')
Dirty hacks
The problem with the serialization of fields is the largest stone in our garden. Unfortunately, I did not manage to find a very elegant and honest solution, but it turned out to build a not very honest, fairly flexible “hack”.
To begin with, in the
GetObjectData method
, add saving singleton fields. It will look like this:
public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SettignsSerializeHelper)); info.AddValue("_servAddr", ServerAddressr); info.AddValue("_port", Port); }
If you now do SOAP serialization, you can see that all the fields are truly serialized. However, in reality, we serialized
SettignsSerializationHelper , which lacks these fields, which means that when deserializing, problems arise. There are two solutions:
- Completely repeat all singleton fields in SettignsSerializationHelper . The deserializer completely consumes such a substitution, fills all the fields, and within the GetRealObject method , they must be assigned back to the singleton. This approach has one big and serious drawback: manual support for duplicating fields, adding them for serialization and deserialization. This is clearly not our
bro . - To call for help reflection, surrogate selector and a little linq, so that everything is done for us. Consider this in more detail.
In the beginning, change the
GetObjectData method:
public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof (SettignsSerializeHelper)); var fields = from field in typeof (Settings).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) where field.GetCustomAttribute(typeof (NonSerializedAttribute)) == null select field; foreach (var field in fields) { info.AddValue(field.Name, field.GetValue(Settings.Instance)); } }
Great, now when we want to add a field to a singleton, it will also be serialized without hands. Let us turn to deserialization.
All singleton fields should be repeated in
SettignsSerializationHelper , but in order to avoid their real duplication, apply a
surrogate selector and change
SettignsSerializationHelper .
New
SettignsSerializationHelper :
[Serializable] internal sealed class SettignsSerializeHelper : IObjectReference { public readonly Dictionary<String, object> infos = (from field in typeof (Settings).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) where field.GetCustomAttribute(typeof (NonSerializedAttribute)) == null select field).ToDictionary(x => x.Name, x => new object()); public object GetRealObject(StreamingContext context) { foreach (var info in infos) { typeof (Settings).GetField(info.Key, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).SetValue(Settings.Instance, info.Value); } return Settings.Instance; } }
And so, inside
SettignsSerializationHelper , a hash map is created, where key is the names of the fields to be serialized, and value in the future will become the values of these fields after deserialization. Here, for more encapsulation, you can make infos as private and write a method to access its key-value pairs, but we will not complicate the example. Inside
GetRealObject, we set its deserialized field values to a singleton and return a reference to it.
Now you just have to fill infos with the field values. A selector will be used for this.
internal sealed class SettingsSurrogate : ISerializationSurrogate { public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) { throw new NotImplementedException(); } public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { var ssh = new SettignsSerializeHelper(); foreach (var val in info) { ssh.infos[val.Name] = val.Value; } return ssh; } }
Since the selector will be used only for deserialization, we will only write
SetObjectData . When obj (the object being deserialized) comes inside the selector, its fields are filled with 0 and null, regardless of the circumstances (obj is obtained after calling
GetUninitializedObject from
FormatterServices in the process of deserializing). Therefore, in our case, it is easier to create a new
SettignsSerializationHelper and return it (this object will be considered deserialized). Next, inside the foreach, fill in infos with deserialized data, which will then be assigned to the singleton fields.
And now an example of the serialization / deserialization process itself:
/: using (var mem = new MemoryStream()) { var soapSer = new SoapFormatter(); soapSer.Serialize(mem, Settings.Instance); var ss = new SurrogateSelector(); ss.AddSurrogate(typeof(SettignsSerializeHelper), soapSer.Context, new SettingsSurrogate()); soapSer.SurrogateSelector = ss; mem.Seek(0, SeekOrigin.Begin); var o = soapSer.Deserialize(mem); Console.WriteLine((Settings)o == Settings.Instance); }
True will be displayed on the console and all fields will be restored. Finally, we finished and brought our rock garden into the proper form.