Despite the fact that using the serialization mechanism when programming in C # is quite simple and convenient, there are some points to consider. About what rake you can step on while working with serialization, about code examples in which these rakes are hidden, and also how PVS-Studio will help you to avoid bumps on your forehead, and this article will be.
Who is the article for?
This article will be especially useful for developers who are just starting to get acquainted with the serialization mechanism. More experienced programmers can also learn something interesting for themselves, or just make sure that even professionals sometimes make mistakes.
')
However, it is understood that the reader is already familiar with the serialization mechanism.
What does PVS-Studio have to do with it? In
release 6.05 , 6 diagnostic rules were added that detect suspicious code associated with the use of the serialization mechanism. These diagnostics are mainly looking for problem areas associated with the
[Serializable] attribute or the implementation of the
ISerializable interface.
Note.It should be understood that the statements described in the article are relevant for some serializers, for example,
BinaryFormatter and
SoapFormatter , and for others, for example, with a hand-written serializer, the behavior may differ. For example, the absence of the
[Serializable] attribute of a class may not interfere with its serialization and deserialization with its own serializer.
By the way, if you are working with serialization, I advise you to
download a trial version of the analyzer and check your code for the presence of suspicious places.
Realizing ISerializable, do not forget about the serialization designer
The implementation of the interface type
ISerializable allows
you to control the serialization, choosing which members need to be serialized, which are not, which values need to be recorded when members are serialized, and so on.
The
ISerializable interface contains a single method
declaration ,
GetObjectData , which will be called when the object is serialized. But together with this method, a constructor must be implemented, which will be called when an object is deserialized. Since the interface cannot oblige you to implement some kind of constructor in your type, this task falls on the shoulders of the programmer who implements the serializable type. The serialization constructor has the following signature:
Ctor(SerializationInfo, StreamingContext)
Without this constructor, the serialization of the object will succeed (assuming the correct implementation of the
GetObjectData method), but it cannot be restored (deserialized) - an exception of the type
SerializationException will be generated.
Let's look at an example of similar code from the Glimpse project:
[Serializable] internal class SerializableTestObject : ISerializable { public string TestProperty { get; set; } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("TestProperty", this.TestProperty); } }
PVS-Studio warning : V3094 Possible exception when deserializing. The SerializableTestObject (SerializationInfo, StreamingContext) constructor is missing. Glimpse.Test.AspNet SessionModelConverterShould.cs 111
Serialization of an instance of this class will succeed, but an exception will occur during deserialization, since there is no corresponding constructor. Most likely, this is not an error (based on the name of the class and the file), but as an illustration of the described situation - what you need.
The serialization constructor for this class might look like this:
protected SerializableTestObject(SerializationInfo info, StreamingContext context) { TestProperty = info.GetString(nameof(TestProperty)); }
Pay attention to the serialization constructor access modifier
When developing a type that implements the
ISerializable interface, it is important to correctly define the access modifier for the serialization constructor. There are several possible cases:
- serialization constructor declared with private modifier in unprinted class;
- serialization constructor declared with public or internal access modifier;
- serialization constructor declared with protected modifier in sealed class.
Of greatest interest is the first of the above options, as it carries the greatest danger. Let us briefly dwell on the second paragraph, but the third will not be considered - declaring the member with the
protected modifier in the structure will not be given by the compiler (compilation error), if such a member is declared in the sealed class - the compiler will issue a warning.
The serialization constructor in the unprinted class has the access modifier 'private'
This is the most dangerous case of improper use of access modifiers to serialization constructors. If the type is unprinted, it is assumed that it may have heirs. However, if the serialization constructor has a
private access modifier, it cannot be called from a child class.
In this case, the child class developer has 2 choices - refuse to use this parent class or manually deserialize the members of the base class. It is worth noting that the second case can hardly be considered a solution to the problem:
- it’s not a fact that the base class provided for trivial deserialization of members;
- the developer of the child class may forget to deserialize any member of the base class;
- with all the desire, it will not be possible to deserialize the private members of the base class.
Therefore, when developing an unprinted serializable class, pay attention to which access modifier the serialization constructor has.
When analyzing the projects, it was possible to find several such in which the rule described above was not observed.
NHibernate [Serializable] public class ConnectionManager : ISerializable, IDeserializationCallback { .... private ConnectionManager(SerializationInfo info, StreamingContext context) { .... } .... }
PVS-Studio Warning: V3103 A Private Ctor (SerializationInfo, StreamingContext) constructor in deserializing derived types. NHibernate ConnectionManager.cs 276
Roslyn [Serializable] private class TestDiagnostic : Diagnostic, ISerializable { .... private TestDiagnostic (SerializationInfo info, StreamingContext context) { .... } .... }
PVS-Studio Warning: V3103 A private TestDiagnostic (SerializationInfo, StreamingContext) constructor in unsealed type. DiagnosticAnalyzerTests.cs 100
In both of the examples above, the serialization constructor should be set to the
protected access modifier so that child classes could call it when deserializing.
Do not declare the serialization constructor with the 'public' or 'internal' modifiers.
This is a good programming style tip. Declaring a serialization constructor with a
public or
internal modifier will not lead to an error, but there is no point in this - this constructor should not be called from the outside, and the serializer no matter what access modifier the constructor has.
When checking open source projects, several of them met in which this rule was not observed.
Msbuild [Serializable] private sealed class FileState : ISerializable { .... internal SystemState(SerializationInfo info, StreamingContext context) { .... } .... }
PVS-Studio Warning: V3103 The Ctor (SerializationInfo, StreamingContext) constructor should not be used for deserialization. Making it internal is not recommended. Consider making it private. Microsoft.Build.Tasks SystemState.cs 218
[Serializable] private sealed class FileState : ISerializable { .... internal FileState(SerializationInfo info, StreamingContext context) { .... } .... }
PVS-Studio Warning: V3103 The Ctor (SerializationInfo, StreamingContext) constructor should not be used for deserialization. Making it internal is not recommended. Consider making it private. Microsoft.Build.Tasks SystemState.cs 139
In both cases, the serialization constructor should be set to the
private access modifier, so both of the above classes are sealed.
NHibernate [Serializable] public class StatefulPersistenceContext : IPersistenceContext, ISerializable, IDeserializationCallback { .... internal StatefulPersistenceContext(SerializationInfo info, StreamingContext context) { .... } .... }
PVS-Studio Warning: V3103 The Ctor (SerializationInfo, StreamingContext) constructor should not be used for deserialization. Making it internal is not recommended. Consider making it protected. NHibernate StatefulPersistenceContext.cs 1478
[Serializable] public class Configuration : ISerializable { .... public Configuration(SerializationInfo info, StreamingContext context) { .... } .... }
PVS-Studio Warning: V3103 The Ctor (SerializationInfo, StreamingContext) constructor should not be used for deserialization. Making it public is not recommended. Consider making it protected. NHibernate Configuration.cs 84
Due to the fact that both classes are unprinted, for the serialization constructors, the
protected access modifier should be set.
Implement the GetObjectData virtual method in unprinted classes
The rule is simple - if you are developing an unprinted class that implements the
ISerializable interface, declare the
GetObjectData method with the
virtual modifier. This will allow child classes to correctly serialize the object when using polymorphism.
To better understand the essence of the problem, I propose to consider a few examples.
Suppose we have the following declarations of the parent and child classes.
[Serializable] class Base : ISerializable { .... public void GetObjectData(SerializationInfo info, StreamingContext context) { .... } } [Serializable] sealed class Derived : Base { .... public new void GetObjectData(SerializationInfo info, StreamingContext context) { .... } }
Suppose there is a method for serializing and deserializing an object of the following form:
void Foo(BinaryFormatter bf, MemoryStream ms) { Base obj = new Derived(); bf.Serialize(ms, obj); ms.Seek(0, SeekOrigin.Begin); Derived derObj = (Derived)bf.Deserialize(ms); }
In this case, the serialization will be performed incorrectly due to the fact that the
GetObjectData method will be called not of the child, but of the parent class. Therefore, members of the child class will not be serialized. If during deserialization, the values of the members added to the
GetObjectData method of the child class are retrieved from the object of the
SerializationInfo type, an exception will be generated because the object of the
SerializationInfo type will not contain the requested keys.
To correct an error in the parent class, the
virtual modifier must be added to the
GetObjectData method, and the
override in the derived class.
If only an explicit implementation of the
ISerializable interface is present in the parent class, you cannot add the
virtual modifier to it. However, leaving everything as it is, you risk complicating the lives of the developers of the child classes.
Consider an example of the implementation of the parent and child classes:
[Serializable] class Base : ISerializable { .... void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { .... } } [Serializable] sealed class Derived : Base, ISerializable { .... public void GetObjectData(SerializationInfo info, StreamingContext context) { .... } }
In this case, the child class will not be able to access the
GetObjectData method of the parent class. And if private members are serialized in the base method, they will also fail to access them from the child class, which means that they will not be able to perform the correct serialization. To correct the error, in addition to the explicit implementation, the implicit implementation of the virtual method
GetObjectData must be added to the base class. Then the corrected code might look like this:
[Serializable] class Base : ISerializable { .... void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { GetObjectData(info, context); } public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { .... } } [Serializable] sealed class Derived : Base { .... public override void GetObjectData(SerializationInfo info, StreamingContext context) { .... base.GetObjectData(info, context); } }
Alternatively, if inheritance of this class is not implied, it should be sealed by adding the
sealed modifier to the class declaration.
Roslyn [Serializable] private class TestDiagnostic : Diagnostic, ISerializable { private readonly string _kind; .... private readonly string _message; .... void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("id", _descriptor.Id); info.AddValue("kind", _kind); info.AddValue("message", _message); info.AddValue("location", _location, typeof(Location)); info.AddValue("severity", _severity, typeof(DiagnosticSeverity)); info.AddValue("defaultSeverity", _descriptor.DefaultSeverity, typeof(DiagnosticSeverity)); info.AddValue("arguments", _arguments, typeof(object[])); } .... }
PVS-Studio warning : V3104 'GetObjectData' implementation in the unsealed type 'TestDiagnostic' is not virtual, it is not possible. CSharpCompilerSemanticTest DiagnosticAnalyzerTests.cs 112
The
TestDiagnostic class is unsealed (albeit private, so it is possible to inherit from it within the same class), but it only has an explicit implementation of the
ISerializable interface, in which, among other things, private members are serialized. This means one thing - the child class developer will not be able to serialize the necessary members in any way: the
GetObjectData method will not be available to it, and the access modifier will not allow accessing members directly.
It would be more correct to bring all the serialization code given above to the virtual method
GetObjectData , which can be referenced from an explicit interface implementation:
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { GetObjectData(info, context); } public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("id", _descriptor.Id); info.AddValue("kind", _kind); info.AddValue("message", _message); info.AddValue("location", _location, typeof(Location)); info.AddValue("severity", _severity, typeof(DiagnosticSeverity)); info.AddValue("defaultSeverity", _descriptor.DefaultSeverity, typeof(DiagnosticSeverity)); info.AddValue("arguments", _arguments, typeof(object[])); }
All serializable members must be serializable.
This condition is mandatory for correct object serialization regardless of whether automatic serialization occurs (when the type is decorated with the
[Serializable] attribute and does not implement the
ISerializable interface) or the serialization is performed manually (implemented by
ISerializable ).
Otherwise, if during serialization a member is encountered that is not decorated with the
[Serializable] attribute, an exception of type
SerializationException will be thrown.
If it is necessary to serialize an object without considering members that have a non-realizable type, several approaches are possible:
- make the non-serializable type serializable;
- if automatic serialization occurs, decorate fields that do not need to be serialized with the [NonSerialized] attribute;
- if manual serialization occurs, simply ignore those members that you do not need.
It is worth paying attention to the fact that the
[NonSerialized] attribute is applicable only to fields. Thus, you cannot prohibit the serialization of a property, but if it has a non-serializable type, you will get an exception. For example, when trying to serialize the class
SerializedClass , which is defined below:
sealed class NonSerializedType { } [Serializable] sealed class SerializedClass { private Int32 value; public NonSerializedType NSProp { get; set; } }
You can
get around this situation by implementing the property through the field decorated with the
[NonSerialized] attribute:
[Serializable] sealed class SerializedClass { private Int32 value; [NonSerialized] private NonSerializedType nsField; public NonSerializedType NSProp { get { return nsField; } set { nsField = value; } } }
Such errors, when the type being serialized has members of non-realizable types that are not decorated with the
[NonSerialized] attribute, detects the
V3097 diagnostic rule of the PVS-Studio static code analyzer.
I remind you that this warning does not necessarily indicate the presence of an error - it all depends on the serializer being used.
Consider a few code examples in which the described condition was violated.
Subtext public class BlogUrlHelper { .... } [Serializable] public class AkismetSpamService : ICommentSpamService { .... readonly BlogUrlHelper _urlHelper; .... }
PVS-Studio warning : V3097 Possible exception: the 'AkismetSpamService' type marked by [Serialable] contains non-serializable members not marked by [NonSerialized]. Subtext.Framework AkismetSpamService.cs 31
The
BlogUrlHelper type of the
_urlHelper field
is not serializable, so when some serializers attempt to serialize an instance of the
AkismetSpamService class,
an SerializationException will be generated. Solve the problem you need, starting from the situation. If serializers of the
BinaryFormatter or
SoapFormatter type are
used , it is necessary either to decorate the field with the
[NonSerialized] attribute, or to decorate with the
[Serializable] attribute the
BlogUrlHepler type. If you use other serializers that do not require the presence of the
[Serializable] attribute in the serializable fields, you can not score a head.
NHibernate public class Organisation { .... } [Serializable] public class ResponsibleLegalPerson { .... private Organisation organisation; .... }
PVS-Studio warning : V3097 Possible exception: the 'ResponsibleLegalPerson' type marked by [Serialable] contains non-serializable members not marked by [NonSerialized]. NHibernate.Test ResponsibleLegalPerson.cs 9
The situation is similar to that described above - either pan or gone. It all depends on the serializer used.
Do not forget about the attribute [Serializable] when implementing the interface ISerializable
This advice applies more to those who are just starting to work with serialization. Managing serialization manually, through the implementation of the
ISerializable interface, it is easy to forget to decorate the type with the
[Serializable] attribute, which potentially leads to the generation of an exception of type
SerializationException . Serializers like
BinaryFormatter require this attribute.
SharpDevelopInteresting examples of this error met in the SharpDevelop project.
public class SearchPatternException : Exception, ISerializable { .... protected SearchPatternException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
PVS-Studio warning : V3096 Possible exception when serializing 'SearchPatternException' type. [Serializable] attribute is missing. ICSharpCode.AvalonEdit ISearchStrategy.cs 80
public class DecompilerException : Exception, ISerializable { .... protected DecompilerException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
PVS-Studio warning : V3096 Possible exception when serializing 'DecompilerException' type. [Serializable] attribute is missing. ICSharpCode.Decompiler DecompilerException.cs 28
To transfer an exception object between application domains, it is serialized and deserialized. Accordingly, native exception types must be serializable. In the examples above, the
SearchPatternException and
DecompilerException types inherit from
Exception and implement serialization constructors, but they are not decorated with the
[Serializable] attribute, which means that when trying to serialize these types of objects (for example, to pass between domains), a
SerializationException will be generated. Thus, for example, by throwing an exception in another application domain, in the current application you will catch not a generated exception, but a
SerializationException .
Make sure that the required members of the type are serialized in the GetObjectData method.
By implementing the
ISerializable interface and defining the
GetObjectData method, you take responsibility for which type members will be serialized and what values will be written to them. In this case, developers have a lot of control over serialization: as a serializable value associated with a member (or to be more honest with any string), you can write the actual value of the serialized object, the result of a method, a constant or literal value - whatever you want.
However, in this case, a large responsibility falls on the shoulders of the developer, because it is necessary not to forget any member to be serialized, even if it is in the base class. We are all human, so sometimes some members are still forgotten.
In order to diagnose such situations, the diagnostic rule
V3099 is provided in the static code analyzer PVS-Studio. I suggest to get acquainted with some examples of the code found by this rule.
SharpDevelop [Serializable] public abstract class XshdElement { public int LineNumber { get; set; } public int ColumnNumber { get; set; } public abstract object AcceptVisitor(IXshdVisitor visitor); } [Serializable] public class XshdColor : XshdElement, ISerializable { .... public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { if (info == null) throw new ArgumentNullException("info"); info.AddValue("Name", this.Name); info.AddValue("Foreground", this.Foreground); info.AddValue("Background", this.Background); info.AddValue("HasUnderline", this.Underline.HasValue); if (this.Underline.HasValue) info.AddValue("Underline", this.Underline.Value); info.AddValue("HasWeight", this.FontWeight.HasValue); if (this.FontWeight.HasValue) info.AddValue("Weight", this.FontWeight .Value .ToOpenTypeWeight()); info.AddValue("HasStyle", this.FontStyle.HasValue); if (this.FontStyle.HasValue) info.AddValue("Style", this.FontStyle.Value.ToString()); info.AddValue("ExampleText", this.ExampleText); } }
PVS-Studio warning : V3099 XshdColor 'type are serialized inside' GetObjectData 'method: LineNumber, ColumnNumber. ICSharpCode.AvalonEdit XshdColor.cs 101
In this code, there are no problems described earlier, such as incorrect access modifiers for the serialization constructor, the absence of the
[Serializable] attribute or the
virtual modifier for the
GetObjectData method.
Alas, there is still a mistake here. The
GetObjectData method
does not take into account the properties of the base class, which means that some of the data will be lost during serialization. As a result, during deserialization, an object with a different state will be restored.
In this case, the solution is to manually add the necessary values, for example, in this way:
info.AddValue(nameof(LineNumber), LineNumber); info.AddValue(nameof(ColumnNumber), ColumnNumber);
If the base class also implemented the
ISerializable interface, the solution would be more elegant - a call in the
GetObjectData derived method of the base method.
NHibernate [Serializable] public sealed class SessionImpl : AbstractSessionImpl, IEventSource, ISerializable, IDeserializationCallback { .... void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { log.Debug("writting session to serializer"); if (!connectionManager.IsReadyForSerialization) { throw new InvalidOperationException("Cannot serialize a Session while connected"); } info.AddValue("factory", Factory, typeof(SessionFactoryImpl)); info.AddValue("persistenceContext", persistenceContext, typeof(StatefulPersistenceContext)); info.AddValue("actionQueue", actionQueue, typeof(ActionQueue)); info.AddValue("timestamp", timestamp); info.AddValue("flushMode", flushMode); info.AddValue("cacheMode", cacheMode); info.AddValue("interceptor", interceptor, typeof(IInterceptor)); info.AddValue("enabledFilters", enabledFilters, typeof(IDictionary<string, IFilter>)); info.AddValue("enabledFilterNames", enabledFilterNames, typeof(List<string>)); info.AddValue("connectionManager", connectionManager, typeof(ConnectionManager)); } .... private string fetchProfile; .... }
PVS-Studio warning : V3099 SortImpl 'type are serialized inside' GetObjectData 'method: fetchProfile. NHibernate SessionImpl.cs 141
This time they forgot to serialize the field of the current class (
fetchProfile ). As can be seen from the definition, it is not decorated with the
[NonSerialized] attribute (unlike other fields that are not serializable in the
GetObjectData method).
There are two more similar places in this project:
- V3099 type are serialized inside the GetObjectData method: currentDocumentName, preMappingBuildProcessed. NHibernate Configuration.cs 127
- V3099 type: "ConnectionManager" type are serialized inside "GetObjectData" method: flushingFromDtcTransaction. NHibernate ConnectionManager.cs 290
An interesting peculiarity is related to such errors - they either lead to the generation of an exception, or to subtle logical errors.An exception will be generated if the serialization constructor tries to get the value of the field that has not been added (will be addressed using the missing key). If a member is completely forgotten (both in the GetObjectData method and in the serialization constructor), the state of the object will be damaged .Generalization
Briefly summarizing all the information above, you can formulate a few tips and rules:- Decorate with the [Serializable] attribute types that implement the ISerializable interface ;
- , [Serializable] ;
- ISerializable , ( Ctor(SerializationInfo, StreamingContext) );
- private , — protected ;
- , ISerializable , GetObjectData ;
- , GetObjectData , , .
Conclusion
I hope you learned something new from the article and became the best expert on serialization issues. By adhering to the above tips and rules, you can spend less time debugging, making life easier for yourself and other developers working with your classes. And the PVS-Studio analyzer will make life even easier, allowing you to detect such errors immediately after they appear.Additional Information
- V3094. Possible exception when deserializing type. The Ctor (SerializationInfo, StreamingContext) constructor is missing
- V3096. Possible exception when serializing type. [Serializable] attribute is missing
- V3097. Possible exception: type marked by [Serialable] contains non-serializable
- V3099. Not all members of the type are serialized inside 'GetObjectData' method
- V3103. A private Ctor (SerializationInfo, StreamingContext) constructor in unsealed type
- V3104. "GetObjectData"
- Msdn Serialization in the .NET Framework
- Msdn Custom Serialization
If you want to share this article with an English-speaking audience, then please use the link to the translation: Sergey Vasiliev. How to not serialize .