📜 ⬆️ ⬇️

Singleton serialization or rock garden

image

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:

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:

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:

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.

Source: https://habr.com/ru/post/190380/


All Articles