Skip to content

Serialization and code generation internals

Orleans serialization serves two related pipelines:

  • value serialization, deep copying, and activation for message and storage payloads;
  • RPC code generation for grain references, request objects, dispatch, and responses.

Most application code uses generated components. Reflection-based discovery is deliberately not the default architecture: generated manifests make the participating types explicit and keep runtime dispatch compatible with trimming and ahead-of-time compilation.

The Orleans source generator is a Roslyn source generator implemented using the incremental generator APIs. It discovers types marked with GenerateSerializerAttribute and interfaces marked directly or transitively with GenerateMethodSerializersAttribute. It also reads metadata emitted by referenced assemblies.

Rendering diagram.

Generated output includes serializers, field codecs, deep copiers, activators, grain proxies, invokable method objects, dispatch metadata, aliases, and a type-manifest provider. Diagnostics reject inaccessible types, ambiguous field identity, unsupported RPC shapes, and missing cross-assembly generation metadata before the application starts.

Source: OrleansSourceGenerator and ReferenceAssemblyDataProvider.

IdAttribute identifies a serialized member within its declaring type. IDs are not field order and must remain stable as source is edited.

For example, [Id(0)] and [Id(1)] identify two different members. Adding a new ID is compatible with readers which tolerate an omitted field. Reusing or renumbering an existing ID changes the meaning of bytes on the wire and can corrupt rolling upgrades or persisted data.

GenerateSerializerAttribute.GenerateFieldIds defaults to GenerateFieldIds.None. Automatic public-property IDs are available, but explicit IDs make compatibility review visible. Primary constructor parameters are included by default for records and excluded by default for other types.

Aliases provide stable type identity when CLR names move. A type alias must remain unique in the manifest. Generic and compound aliases are resolved through the manifest’s alias tree.

The wire protocol uses writer and reader sessions to track references and type information across a payload. Reference tracking preserves object identity and cycles. A field codec writes field headers and values; the matching codec reads or skips fields it understands.

Deep copying is a separate operation used when Orleans must preserve isolation without crossing a transport boundary. Immutable values can bypass copying; mutable values require a generated or custom copier. Declaring a mutable type immutable trades safety for speed and must be justified by the type’s actual behavior.

For each grain interface method, generated code captures arguments in an invokable object. The generated proxy submits that object through its proxy base. On the target, generated dispatch metadata invokes the concrete implementation and encodes the response.

The request object is serializable like any other Orleans value. Stable method and interface metadata allow caller and target assemblies to evolve independently within the supported versioning rules. Outgoing and incoming call filters wrap the generated invocation; they do not replace serialization or dispatch.

Each generated assembly carries a TypeManifestProviderAttribute. SerializerBuilderExtensions.AddAssembly finds those providers and contributes their components to TypeManifestOptions.

The manifest records:

  • activators, field codecs, serializers, copiers, and converters;
  • RPC interfaces, proxies, and implementations;
  • well-known numeric type IDs and aliases; and
  • explicitly allowed types and assemblies.

TypeManifestOptions.AllowAllTypes defaults to false. This is a type-resolution boundary: receiving a formatted type name does not make every loadable CLR type valid input.

API: TypeManifestOptions, ISerializerBuilder, and SerializerBuilderExtensions.AddAssembly. Implementation: manifest options, serializer builder extensions, and serializer service registration.

Use the registration APIs exposed by ISerializerBuilder for:

  • a field codec when a type needs custom wire encoding;
  • a deep copier when generated member-wise copy is unsuitable;
  • an activator when construction needs special handling;
  • a serializer when the type owns an external format; or
  • a converter which maps a type to a supported surrogate.

Keep codec and copier behavior paired. A custom serializer which preserves a graph while its copier loses reference identity can produce different local and remote call behavior.

Configuration examples belong in the serialization configuration guide. Implementation behavior is exercised by GeneratedSerializerTests and GeneratedSerializerBitwiseCompatibilityTests.