Metalama uses serialization when an aspect has a cross-project effect, meaning 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 JSON or XML serializers, Metalama's serializer:
- supports cyclic graphs instead of just trees,
- serializes the inner object structure (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. - All
enumtypes. - 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.
The serialization process usually works transparently. However, here are a few techniques to handle corner cases:
Skipping a field or property
To exclude 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
To 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
Metalama's serializer is inspired by Microsoft's BinaryFormatter, which has been deprecated for security reasons. However, using the Metalama.Framework.Serialization namespace doesn't present any security risk. The serializer is only designed to deserialize binary data stored in a binary library. Since this library also allows for arbitrary code execution, using the serializer doesn't increase the risk.
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. Before the aspect, the field is represented by an object of type IField 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.
Things get more complex when 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'll get an IRef<IField>. If you resolve the reference after the aspect, you might wonder what happens because the field is now a property.
When you resolve an IRef<IField>, you'll always get an IField. If the field has been overridden, you'll get a shim representing what is actually an IProperty. However, this field isn't navigable through the INamedType.Fields properties, but only, as an IProperty, through INamedType.Properties. Navigate to the "real" property using the IField.OverridingProperty property. The inverse relationship is the IProperty.OriginalField property. The IRef.As method can convert an overridden IField into its overriding IProperty and conversely.