Skip to content

Serialization: built-in serializers declare their ids in code (#8676, part 1) - #8694

Merged
Aaronontheweb merged 3 commits into
akkadotnet:devfrom
Aaronontheweb:feature/serializer-ids-in-code
Sep 30, 2026
Merged

Aaronontheweb merged 3 commits into
akkadotnet:devfrom
Aaronontheweb:feature/serializer-ids-in-code

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Purpose

Part 1 of the plan to move first-party serializer registration into C# (#8676). Every built-in legacy serializer now declares its wire id in code with public override int Identifier => N; (or a GetType()-guarded version of it, see below), instead of reading it lazily from akka.actor.serialization-identifiers via SerializerIdentifierHelper.GetSerializerIdentifierFromConfig. That HOCON lookup is lazy on first access to Identifier - it is never called from the Serializer constructor - so nothing about serializer construction changes. No wire bytes change: every id below is exactly what each module's .conf already ships.

Serializers and ids

  • Remote: ProtobufSerializer 2, DaemonMsgCreateSerializer 3, MessageContainerSerializer 6, MiscMessageSerializer 16, PrimitiveSerializers 17, SystemMessageSerializer 22
  • Cluster: ClusterMessageSerializer 5, ReliableDeliverySerializer 36 (internal)
  • Cluster.Tools: DistributedPubSubMessageSerializer 9, ClusterSingletonMessageSerializer 14, ClusterClientMessageSerializer 15
  • Cluster.Sharding: ClusterShardingMessageSerializer 13
  • DistributedData: ReplicatedDataSerializer 11, ReplicatorMessageSerializer 12
  • Cluster.Metrics: ClusterMetricsMessageSerializer 10
  • Persistence: PersistenceMessageSerializer 7, PersistenceSnapshotSerializer 8
  • Streams: StreamRefSerializer 30
  • Core: ByteArraySerializer 4, NewtonSoftJsonSerializer 1

NullSerializer already overrode Identifier, so it's untouched. ArteryControlMessageSerializer is skipped: its id already comes from the [AkkaSerializer<T>] V2 source-generator attribute, not from HOCON. Hyperion (src/contrib/serializers) is a third-party serializer and is out of scope; it stays on HOCON/SerializationSetup.

The GetType() guard on public, unsealed serializers

Serializer.Identifier resolves by GetType(), not by the declaring type. A plain public override int Identifier => N; on a serializer that a user can subclass would report N for the subclass too, even if the subclass is registered under its own HOCON id - silently taking over GetSerializerById(N) for messages the subclass never wrote. So every serializer that is public and unsealed guards the constant on an exact type check and falls back to the base (HOCON) lookup otherwise:

public override int Identifier => GetType() == typeof(ProtobufSerializer) ? 2 : base.Identifier;

The 12 affected classes: ProtobufSerializer, DaemonMsgCreateSerializer, MessageContainerSerializer, ClusterMessageSerializer, DistributedPubSubMessageSerializer, ClusterSingletonMessageSerializer, ClusterClientMessageSerializer, ClusterShardingMessageSerializer, ClusterMetricsMessageSerializer, PersistenceSnapshotSerializer, ByteArraySerializer, NewtonSoftJsonSerializer.

The other 8 (MiscMessageSerializer, PrimitiveSerializers, SystemMessageSerializer, ReplicatedDataSerializer, ReplicatorMessageSerializer, PersistenceMessageSerializer, StreamRefSerializer - all sealed - and ReliableDeliverySerializer, which is internal sealed) can't be subclassed, so they keep a plain constant.

No wire change

Ids come from code now but are identical to what HOCON already assigned. Existing serialization and remote round-trip specs pass unchanged.

Breaking changes

A built-in serializer's id can no longer be overridden via akka.actor.serialization-identifiers. Before this change, an application that set, for example, "Akka.Remote.Serialization.MiscMessageSerializer, Akka.Remote" = 99 in its own HOCON would make that node write id 99 on the wire. Now the code id (16, in that example) always wins, because it's a hardcoded Identifier override rather than a config lookup. A user subclass of a built-in serializer keeps resolving its own id from HOCON - only the built-in type itself is pinned.

  • Migration: ids of built-in serializers are wire contracts and can no longer be overridden. Remove any serialization-identifiers override of a built-in Akka.NET serializer from your HOCON; it no longer has any effect.

This is expected to affect very few, if any, real applications, since overriding a built-in serializer's id would already have broken wire compatibility with any node that didn't carry the same override.

Tests

  • One [Fact] in Akka.API.Tests - the only project that references all 9 affected modules (Remote, Cluster, Cluster.Tools, Cluster.Sharding, DistributedData, Cluster.Metrics, Persistence, Streams, core) - merges every module's default config and, for every row in akka.actor.serialization-identifiers, builds the serializer with RuntimeHelpers.GetUninitializedObject (skipping the constructor, leaving the actor-system field null) and asserts Identifier matches the row. A serializer whose id still resolved lazily from HOCON would touch that null field and throw, so this test also proves D1 for every built-in serializer in one place.
  • One regression spec in Akka.Tests: a NewtonSoftJsonSerializer subclass registered under its own HOCON id keeps that id, and the built-in NewtonSoftJsonSerializer still reports 1.
  • Ran the full Serialization-filtered tests in Akka.Tests, Akka.Remote.Tests, Akka.Cluster.Tests, Akka.Persistence.Tests and Akka.Streams.Tests (all green), plus the serializer specs in Akka.Cluster.Tools.Tests, Akka.Cluster.Sharding.Tests, Akka.DistributedData.Tests and Akka.Cluster.Metrics.Tests.
  • dotnet test src/core/Akka.API.Tests -c Release -f net10.0: adding the Identifier overrides changes the public API surface for 9 assemblies (the property signature is unchanged, only its body), so both the DotNet and Net approval files were updated with just the added Identifier override lines.

Next steps

PR 2 moves alias and binding registration into the modules' ModuleSerializers tables, so the C# tables carry everything HOCON carries today (behavior unchanged, verified by parity specs). Later PRs load those tables from code and delete the built-in HOCON rows.

…tnet#8676, part 1)

Every built-in legacy serializer now declares its wire id with
public override int Identifier => N, instead of reading it lazily from
akka.actor.serialization-identifiers via SerializerIdentifierHelper. This
removes the last reason a legacy serializer needs HOCON to work; wire ids
are unchanged.

Remote: ProtobufSerializer 2, DaemonMsgCreateSerializer 3,
MessageContainerSerializer 6, MiscMessageSerializer 16,
PrimitiveSerializers 17, SystemMessageSerializer 22.
Cluster: ClusterMessageSerializer 5, ReliableDeliverySerializer 36.
Cluster.Tools: DistributedPubSubMessageSerializer 9,
ClusterSingletonMessageSerializer 14, ClusterClientMessageSerializer 15.
Sharding: ClusterShardingMessageSerializer 13.
DistributedData: ReplicatedDataSerializer 11, ReplicatorMessageSerializer 12.
Metrics: ClusterMetricsMessageSerializer 10.
Persistence: PersistenceMessageSerializer 7, PersistenceSnapshotSerializer 8.
Streams: StreamRefSerializer 30.
Core: ByteArraySerializer 4, NewtonSoftJsonSerializer 1.

ArteryControlMessageSerializer is out of scope: its id already comes from
the [AkkaSerializer] V2 generator attribute. Hyperion (src/contrib/serializers)
is a third-party serializer and stays on HOCON/SerializationSetup.

Adds a BuiltInSerializerIdentifierSpec per module test project asserting
that each serializer's code Identifier equals the value in that module's
shipped serialization-identifiers HOCON, plus a D1 spec in Akka.Remote.Tests
proving a HOCON override of a built-in id is now ignored. Updates the
Akka.API.Tests approval files with the added Identifier override lines.
@Aaronontheweb Aaronontheweb added serialization AOT Ahead-of-Time (AOT) Compilation labels Sep 30, 2026
Guard the Identifier override on every public, unsealed built-in
serializer with a GetType() check so a user subclass keeps resolving its
own id from HOCON instead of inheriting the built-in's hardcoded constant:
ProtobufSerializer, DaemonMsgCreateSerializer, MessageContainerSerializer,
ClusterMessageSerializer, DistributedPubSubMessageSerializer,
ClusterSingletonMessageSerializer, ClusterClientMessageSerializer,
ClusterShardingMessageSerializer, ClusterMetricsMessageSerializer,
PersistenceSnapshotSerializer, ByteArraySerializer and
NewtonSoftJsonSerializer. Sealed and internal serializers keep a plain
constant, since they can't be subclassed. Adds a regression spec in
Akka.Tests: a NewtonSoftJsonSerializer subclass registered under its own
HOCON id keeps that id, and the built-in still reports 1.

Replaces the 9 per-module BuiltInSerializerIdentifierSpec files and the
Akka.Remote.Tests D1 spec with one Fact in Akka.API.Tests, the only
project that references all 9 modules. It merges every module's default
config, and for each akka.actor.serialization-identifiers row, builds the
serializer with RuntimeHelpers.GetUninitializedObject (skipping the
constructor) and asserts Identifier matches. A serializer whose id still
resolved lazily from HOCON would throw on the null actor-system field,
so this also proves D1.

Reverts two whitespace-only changes picked up incidentally while adding
the Identifier overrides to ClusterMessageSerializer.cs and
ClusterMetricsMessageSerializer.cs.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLGTM

@Aaronontheweb
Aaronontheweb merged commit 3daa5e4 into akkadotnet:dev Sep 30, 2026
16 checks passed
Aaronontheweb added a commit that referenced this pull request Oct 1, 2026
…8698 (#8700)

* Audit BREAKING_CHANGES_V1.6.md against v1.5.71 and catch up through #8698

Restate the definition (binary / source / behavioral compatibility against
the last stable v1.5 release), drop rows that are not breaking under it
(Artery-only, DynamicTypeLoading-off-only, fixes, dev-only comparisons),
fold the TargetInvocationException and trim-annotation rows into one row
each, shorten the rest, mark every row Merged with its PR, and add the
breaking changes from #8694 and #8698.

* Add pre-ledger breaking changes to BREAKING_CHANGES_V1.6.md

Cover changes on dev since the v1.5 line diverged that landed before the
ledger existed: net10.0-only targeting and ByteString removal (#8132),
App.config HOCON loading removal (#7456), and the AddOrSet removal (#7622).

* Apply review feedback to BREAKING_CHANGES_V1.6.md

Add rows for #8222 (Serializer overloads, ByteArraySerializer manifest),
#8465 (explicit TLS hostname check), #8132 (TCP connects bypass akka.io.dns)
and #7557 (object handler without predicate blocks later Receive calls).
State net10.0-only once, folding in #8594 and the dependency bumps; fix the
ByteString row's write types; narrow the #8698 row to module serializers;
drop the .Internal AddOrSet row and say why in the preamble; shorten rows;
use asterisk list markers.

* Drop the #7557 ReceiveActor row from BREAKING_CHANGES_V1.6.md

The Receive(typeof(object), Func<object, bool>) regression is being fixed
in a separate PR instead of documented.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AOT Ahead-of-Time (AOT) Compilation serialization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant