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
serializer.
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 extension 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 source code you don't own (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 present any 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.
Accessing a field after it has been overridden
When you override a field, Metalama turns it into a property. That is, before the aspect, the field will be represented by an object of type IField type and exposed in the INamedType.Fields collection. However, after the aspect, the overridden field is represented as an IProperty and exposed in the INamedType.Properties collection. This usually works well, and most of you likely haven't had to think much about it.
However, the devil is in the details. Things get more complex when you are passing a reference to an overridden field to another aspect or to another assembly using transitive aspects.
If you take a reference to a field before the aspect, you will get an IRef<IField>
. If you resolve the reference after the aspect, you might wonder what happens because the field is now a property.
If you attempt to resolve an IRef<IField>
, you will always get an IField. If the field has been overridden, you will get an shim representing what is actually an IProperty. However, this field is not navigable through the INamedType.Fields
properties, but only, as an IProperty
, through INamedType.Properties
. You can navigate to the "real" property using the IField.OverridingProperty property. The inverse relationship is the IProperty.OriginalField property. Also, the IRef.As method is able to convert an overridden an IField into its overriding IProperty and conversely.