Metalama relies on serialization to handle situations when an aspect or cross-project effect, i.e., when it affects not only the current project but also, transitively, referencing projects.
This happens in the following scenarios:
- Inheritable aspects (see Applying aspects to derived types): inheritable instances of IAspect but also, if defined, of their respective IAspectState, are serialized.
- Reference validators (see Validating code from an aspect): implementations of BaseReferenceValidator and, if you're using Metalama.Extensions.Architecture, any ReferencePredicate, are serialized.
- Hierarchical options of non-sealed declarations (see IHierarchicalOptions<T> and Exposing a configuration API).
- Annotations on non-sealed declarations (see IAnnotation)
When any aspect or fabric has some cross-project effect, the following process is executed:
- In the current project:
- The objects are serialized into a binary stream.
- The binary stream is stored in a managed resource in the current project.
- In all referenced projects:
- The objects are deserialized from the managed resource.
How are objects serialized?
Metalama uses a custom serializer, which is implemented in the Metalama.Framework.Serialization namespace and has a similar behavior as Microsoft's legacy BinaryFormatter
serializable.
Unlike more familiar JSON or XML serializers, Metalama's serializer:
- supports cyclic graphs instead of just trees,
- serializes the inner object structure, i.e., private fields, instead of the public interface.
These characteristics allow the serialization process to happen almost transparently.
System-defined serializable types
The following types are serializable by default:
- Primitive types:
bool
,byte
,char
,short
,int
,long
,ushort
,sbyte
,uint
,ulong
,float
,double
,decimal
,double
. - All
enum
types. - Arrays of any supported type (including
object[]
arrays, as long as items are of a supported type). - Common system types: DateTime, TimeSpan, Guid, CultureInfo.
- Collection types: List<T>, Dictionary<TKey, TValue>.
- Immutable collection types: ImmutableDictionary<TKey, TValue>, ImmutableArray<T>, ImmutableHashSet<T>.
- Metalama types: IRef<T>, SerializableDeclarationId, SerializableTypeId, TypedConstant, IncrementalKeyedCollection<TKey, TValue>, IncrementalHashSet<T>.
Warning
Code model declarations (IDeclaration) and types (IType) are, by design, NOT serializable. If you want to serialize a declaration, you must serialize a reference to it, obtained through the ToRef method. The deserialized reference must then be resolved in its new context using the IRef.GetTarget method.
Custom serializable types
Metalama automatically generates serializers for any type deriving from the ICompileTimeSerializable interface. This includes any aspect, fabric, or class implementing IAspectState, IAnnotation, IHierarchicalOptions, BaseReferenceValidator, ReferencePredicate, ...
You normally don't need to worry about the serialization process since it should usually work transparently. However, here are a few tricks to cope with corner cases:
Skipping a field or property
To waive a field or automatic property from being serialized, annotate it with the [NonCompileTimeSerialized] attribute.
Overriding the serializer when you own the type
If you can edit the source code of the class, you can override the default serializer by adding a nested class called Serializer
and implementing the ValueTypeSerializer<T> or ReferenceTypeSerializer<T> class. Your nested class must have a default public constructor.
Implementing a serializer for a third-party type
If you must implement serialization for a class whose you don't own the source code (or to which you don't want to add a package reference to Metalama), follow these steps:
- Create a class derived from ValueTypeSerializer<T> or ReferenceTypeSerializer<T> class. The class must have a default public constructor.
- Register the serializer by using the assembly-level ImportSerializerAttribute.
For generic types, the serializer type must have the same type arguments as the serialized type.
Security and obfuscation
Although it is inspired by Microsoft's BinaryFormatter
, which has been deprecated for security reasons, using the Metalama.Framework.Serialization namespace does not pause a security risk. Although the serializer might in theory allow for arbitrary code execution, it is only designed to deserialize binary data stored in a binary library. Since this library also, in essence, allows for arbitrary code execution, the use of the serializer does not increase the risk. Developers should not use untrusted libraries in the first place.
Warning
The Metalama.Framework.Serialization namespace is NOT compatible with obfuscation. The serialized binary stream contains full names of declarations in clear text, partially defeating the purpose of serialization. Additionally, serialization will fail if these names are changed after compilation by the obfuscation process.