Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0079253
feat(serialization): add transactional closed serializer factories
ReubenBond Oct 1, 2026
496ab65
test(serialization): link static factory contract fixtures
ReubenBond Oct 1, 2026
6a57eba
fix(serialization): scope generated-only mode and initialize automati…
ReubenBond Oct 1, 2026
4867c22
fix(serialization): place generated-only configuration check in provi…
ReubenBond Oct 1, 2026
f58d270
fix(serialization): isolate pending graphs from legacy and DI cache p…
ReubenBond Oct 1, 2026
29e140d
feat(serialization): generate explicit closed serializer contexts for…
ReubenBond Oct 1, 2026
6d34abc
fix(serialization): defer unused context generation attribute
ReubenBond Oct 1, 2026
58a2705
feat(codegen): activate context declaration attribute with generated …
ReubenBond Oct 1, 2026
65b6adb
fix(codegen): validate context identity and preserve referenced regis…
ReubenBond Oct 1, 2026
8bd38c7
fix(serialization): bound construction resolution and transactional h…
ReubenBond Oct 1, 2026
d8cfd83
fix(serialization): preserve complete compound alias metadata and tra…
ReubenBond Oct 2, 2026
289ac62
fix(serialization): isolate keyed services and fault base codec const…
ReubenBond Oct 2, 2026
1124160
fix(serialization): retain nullable copier constructor callers
ReubenBond Oct 2, 2026
2d3ef98
fix(serialization): preserve keyed construction facade capabilities
ReubenBond Oct 2, 2026
4c5c030
fix(codegen): close canonical value and array context services
ReubenBond Oct 2, 2026
8e6a788
test(serialization): cover canonical context services and nullable cy…
ReubenBond Oct 2, 2026
b8aaa40
fix(codegen): validate referenced producer accessor contracts
ReubenBond Oct 2, 2026
7eb810f
test(codegen): cover referenced accessor strategy boundaries
ReubenBond Oct 2, 2026
c6f4035
refactor(serialization): unify generated-first service and type resol…
ReubenBond Oct 2, 2026
f7f02e1
fix(serialization): guard trim annotations on netstandard
ReubenBond Oct 2, 2026
03c97e5
fix(serialization): retain trim annotations with an internal polyfill
ReubenBond Oct 2, 2026
235f31c
fix(codegen): follow actual referenced dependencies and preserve name…
ReubenBond Oct 2, 2026
9041029
fix(serialization): close metadata-only argument aliases and normaliz…
ReubenBond Oct 2, 2026
9e93fb9
fix(codegen): include interface and declaring-type alias metadata
ReubenBond Oct 2, 2026
5b498f2
fix(codegen): compose identical alias and type-id registrations safely
ReubenBond Oct 2, 2026
3a1a992
fix(codegen): respect accessibility of metadata-only type references
ReubenBond Oct 2, 2026
e2489ce
fix(codegen): bound recursive metadata dependency expansion
ReubenBond Oct 2, 2026
f6a5289
test(codegen): cover expanding array interface metadata
ReubenBond Oct 2, 2026
afdf173
fix(codegen): honor selected collection dependency factories
ReubenBond Oct 3, 2026
f968586
fix(codegen): remove generated field-accessor markers
ReubenBond Oct 3, 2026
eb8699a
fix(serialization): preserve construction and activation guarantees
ReubenBond Oct 3, 2026
057cfb1
fix(serialization): allocate arrays from closed array types
ReubenBond Oct 3, 2026
7170e14
style(tests): format multidimensional array initializer
ReubenBond Oct 3, 2026
ad417d0
fix(serialization): preserve recursive implementation ownership
ReubenBond Oct 3, 2026
b81797f
fix(serialization): reject factory cycles before instance construction
ReubenBond Oct 3, 2026
2b1ab62
fix(serialization): construct cyclic dependency graphs using partial …
ReubenBond Oct 3, 2026
27acfbe
fix(serialization): cache completed shallow-copy results
ReubenBond Oct 3, 2026
a2e60f6
feat(serialization): declare serializer context roots using generic a…
ReubenBond Oct 4, 2026
38ffcf3
refactor(serialization): scope pending construction state to root res…
ReubenBond Oct 4, 2026
8cba90d
fix(serialization): separate allowed names from cached type identities
ReubenBond Oct 5, 2026
87e050a
fix(serialization): bound mixed dictionary copier trait queries
ReubenBond Oct 5, 2026
694be6b
fix(serialization): integrate explicit manifest selection after rebase
ReubenBond Oct 5, 2026
f20a90b
fix(codegen): register canonical generated base service aliases
ReubenBond Oct 5, 2026
14f1504
fix(tests): bind metadata smoke resolution to its provider
ReubenBond Oct 5, 2026
9d56dd2
fix(serialization): express non-null copier dependency guarantees
ReubenBond Oct 6, 2026
0377f4d
fix(serialization): bind DI-first callers to holder providers
ReubenBond Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/Docs.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
<Folder Name="/site/src/content/docs/host/configuration-guide/snippets/">
<Project Path="site/src/content/docs/host/configuration-guide/snippets/serialization/serialization.csproj" />
</Folder>
<Folder Name="/site/src/content/docs/host/configuration-guide/snippets/serializer-contexts/">
<Project Path="site/src/content/docs/host/configuration-guide/snippets/serializer-contexts/SerializerContexts.csproj" />
</Folder>
<Folder Name="/site/src/content/docs/host/monitoring/" />
<Folder Name="/site/src/content/docs/host/monitoring/snippets/" />
<Folder Name="/site/src/content/docs/host/monitoring/snippets/observability/">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Compile-time serializer contexts
description: Register a closed graph of Orleans serialization types for NativeAOT.
ms.topic: how-to
---

# Compile-time serializer contexts

A <xref:Orleans.Serialization.SerializerContext> registers a closed graph of types with statically constructed Orleans codecs and deep copiers. Use a context when publishing a serialization component with NativeAOT on .NET 10 and later, so the native compiler can compile each required generic implementation.

## Declare the type graph

Apply <xref:Orleans.GenerateSerializerContextAttribute`1> to a top-level, non-generic partial class derived from `SerializerContext`. The attribute's type argument declares one closed root type, using C# 11 or later. The compiler enforces closed attribute type arguments. The generator follows serialized model members and collection elements, registering every supported dependency. A root such as `List<Dictionary<string, int>>` includes the list, dictionary, string, and integer implementations.

:::code language="csharp" source="snippets/serializer-contexts/SerializerContextExample.cs" id="serializer_context_declaration":::

Models use the existing <xref:Orleans.GenerateSerializerAttribute> and stable <xref:Orleans.IdAttribute> member identifiers. Referenced assemblies provide generated codecs and copiers whose actual constructor and service-member metadata determine the closed dependency graph. NativeAOT implementations use direct member access or static accessors for each serialized member; JIT applications also support generated dynamic accessors. For traversal of referenced models, provide complete implementation metadata to the compiler, for example by setting `ProduceReferenceAssembly=false` on the model project. This preserves the member information consumed by the existing Orleans model generator.

Referenced dependency discovery follows the producer's emitted codec and copier service contracts, including private implementation fields and generated service properties. The producer's selected members and field identifiers therefore determine serialization and copying even when the consumer uses different field-ID settings. Provide complete implementation metadata for models using assembly-level implicit member selection.

Metadata discovery also follows generic arguments, array elements, implemented interfaces, and declaring types. It preserves accessible alias metadata independently of codec registration. This traversal has separate limits of 1,024 closed metadata types and 128 nested dependencies; expanding generic-interface shapes produce `ORLEANS0115` with guidance to declare finite type metadata. Already visited types terminate finite metadata cycles.

The supported graph includes primitive leaf codecs with parameterless construction, generated enums, `List<T>`, `Dictionary<TKey, TValue>`, nullable value types, and single-dimensional zero-based arrays. Generated models use default construction and an `object` base for classes. NativeAOT uses the existing model generator's direct-access and static-accessor support. Model hooks, custom activation, and additional collection families require their corresponding implementation support before being included in a context. The generator reports `ORLEANS0115` for an unsupported dependency or a graph exceeding 1,024 closed types. Native publication diagnoses referenced implementations which require unavailable runtime code generation.

## Register and use the context

<xref:Orleans.Serialization.ServiceCollectionExtensions.AddSerializerContext*> registers the normal <xref:Orleans.Serialization.Serializer> and <xref:Orleans.Serialization.DeepCopier> services. Resolution uses closed factories first, then registered implementation metadata and dependency injection. Repeated calls combine contexts. Duplicate closed registrations use the first registered implementation.

:::code language="csharp" source="snippets/serializer-contexts/SerializerContextExample.cs" id="serializer_context_usage":::

The context's closed serializer registrations seed the type names used during deserialization in the common type resolver's cache. <xref:Orleans.Serialization.Configuration.TypeManifestOptions.AddAllowedType*> authorizes formatted names, while additional names resolve through reflection when the application preserves their metadata. Type-name filters and component validation apply to both forms of resolution. Codecs and copiers preserve Orleans field identifiers, reference tracking, and deep-copy isolation. Object cycles and shared references retain their identity within the restored or copied graph.

Generated struct value serializers and field codecs share one canonical codec instance. Closed generic models include the concrete array services requested by their generated implementations, while direct byte-array serialization and copying retain the optimized byte-array implementations.

## Dependency construction and concurrency

Generated factories construct known closed concrete codecs and copiers. Generated model code retains its concrete dependency fields. Recursive dependencies reuse partially constructed instances through the provider-specific, caller-aware service-resolution stack and statically closed holders. Constructors retain those references. Serialization, copying, activation, and optional shallow-copy queries run after construction completes. Optional copiers cache their completed shallow-copy result when first queried, and subsequent queries and copies reuse that per-instance result.

Each service provider caches one completed instance per implementation type. A transient construction scope on the caller-aware resolution stack owns pending services and typed cache indexes. It acquires the provider's cache lock when a closed factory joins and publishes the complete dependency graph after its outermost constructor succeeds. If construction fails, the scope discards the pending graph and releases its state; the next resolution constructs fresh dependencies, preserving previously completed services. A nested failure caught by a constructor is rethrown by the scope as the original error before publication. A factory cycle encountered before an instance is available faults the graph with guidance to use caller-aware generated-code helpers or statically closed holders for recursive constructor dependencies. Serializer sessions and copy contexts track each operation's reference identities independently.

Closed factories and metadata-based activation compose cyclic dependency graphs in the same resolution pipeline. Automatic services and cache entries created during a factory transaction participate in its publication and rollback. When a metadata constructor enters a closed factory, publication follows completion of that outer metadata constructor. Ordinary metadata activation resolves its dependency-injection services through the application's built provider before a closed factory joins the graph.

During construction, dependencies resolve through provider-owned serialization contracts, metadata-registered implementations, and explicit closed factories registered with <xref:Orleans.Serialization.Configuration.TypeManifestOptions.AddSerializerService*>. Generated model constructors can compose registered collection codecs and copiers with closed element factories in the same graph. Register a shared instance using a factory which returns the captured instance, or provide a closed constructor factory which supplies its external dependencies explicitly. This keeps each pending graph's dependencies stable even when the application builds multiple service providers from a mutable service collection. External dependency-injection lookups during construction produce this registration guidance and fault the pending graph. Outside construction, the service facade delegates ordinary and keyed lookups to the application's built service provider, preserving its registration snapshot, selection rules, and lifetimes.

Reflection-based activation uses constructors preserved by generated manifests, annotated manual registrations, or typed generated dependencies. NativeAOT can materialize a generic implementation when its closed native code is rooted. Context factories make those instantiations visible to the compiler. Forward value-type holders required before ordinary metadata constructors allocate their callers use statically closed constructor or dependency-injection factories. A runtime request for an unavailable native instantiation produces registration guidance; add its closed graph to a context or supply closed factories.

## Diagnose unsupported input

`ORLEANS0114` identifies an invalid context declaration. `ORLEANS0115` identifies the unsupported closed type and the required change. Declare every concrete type which can occur in the supported runtime graph, including types introduced through additional roots.

<xref:Orleans.Serialization.CodecNotFoundException> identifies an unregistered runtime type or serialization service. Register its closed graph before requesting serialization or copying.

The generated dictionary registration serializes dictionaries using `EqualityComparer<TKey>.Default`. A custom comparer in a value or serialized payload produces `NotSupportedException`. Applications with custom comparer requirements can register a comparer-aware closed codec through <xref:Orleans.Serialization.Configuration.TypeManifestOptions.AddSerializer*>.

Register that codec and copier before the context so they become the selected implementations. Collection factories bind their element dependencies to the selected per-type registrations, preserving the same comparer behavior for a root dictionary and for dictionaries inside a list. Context-first registration retains the context's default-comparer implementation for both paths.

For a NativeAOT executable, enable native publication:

:::code language="xml" source="snippets/serializer-contexts/NativeContextPublish.props" id="serializer_context_native_publish":::

Import these properties into the executable project and register a context using `AddSerializerContext`. JIT and NativeAOT applications use the same generated-first resolution pipeline. Register the complete graph used by the application so each required codec, copier, and native generic implementation is available.

Keep trim and AOT warnings as errors. The repository's centralized native smoke matrix exercises the strict `Contexts` and `Factories` scenarios on .NET 10, including closed metadata activation, rooted generic materialization, and reflection type lookup alongside generated registrations.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<!-- <serializer_context_native_publish> -->
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
<!-- </serializer_context_native_publish> -->
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.Serialization;

namespace Documentation.SerializerContexts;

#region serializer_context_declaration
[GenerateSerializerContext<List<Dictionary<string, int>>>]
[GenerateSerializerContext<DocumentationPayload<int>>]
public partial class ApplicationSerializerContext : SerializerContext;

[GenerateSerializer]
public sealed class DocumentationPayload<T>
{
[Id(0)]
public T Value { get; set; } = default!;

[Id(1)]
public List<Dictionary<string, int>> Items { get; set; } = [];
}

[GenerateSerializer]
public sealed class DocumentationPrimitivePayload
{
[Id(0)]
public int Value { get; set; }
}
#endregion

[GenerateSerializer(GenerateFieldIds = GenerateFieldIds.PublicProperties)]
public sealed class DocumentationImplicitPayload<T>
{
public List<T> Values { get; set; } = [];
public T[] Flat { get; set; } = [];
public T[][] Nested { get; set; } = [];
}

public static class SerializerContextExample
{
public static List<Dictionary<string, int>> SerializeAndCopy()
{
#region serializer_context_usage
using var services = new ServiceCollection()
.AddSerializerContext(new ApplicationSerializerContext())
.BuildServiceProvider();

var serializer = services.GetRequiredService<Serializer>();
var copier = services.GetRequiredService<DeepCopier>();
var input = new List<Dictionary<string, int>>
{
new() { ["count"] = 3 },
new() { ["count"] = 5 }
};

var bytes = serializer.SerializeToArray(input);
var restored = serializer.Deserialize<List<Dictionary<string, int>>>(bytes)!;
var copy = copier.Copy(restored);
copy[0]["count"] = 7;
#endregion

if (restored[0]["count"] != 3)
{
throw new InvalidOperationException("The copied dictionary must have independent mutable state.");
}

return copy;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<SourceRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\..\..\..\..\..\..\..\'))</SourceRoot>
<OrleansBuildTimeCodeGen>true</OrleansBuildTimeCodeGen>
<OrleansHotReload>true</OrleansHotReload>
<IsOrleansFrameworkPart>false</IsOrleansFrameworkPart>
<IsAotCompatible>true</IsAotCompatible>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(SourceRoot)src\Orleans.Serialization\Orleans.Serialization.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions docs/site/src/content/docs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ items:
href: host/configuration-guide/serialization-immutability.md
- name: Configure serialization
href: host/configuration-guide/serialization-configuration.md
- name: Compile-time serializer contexts
href: host/configuration-guide/serializer-contexts.md
- name: Customize serialization
href: host/configuration-guide/serialization-customization.md
- name: Code generation overview
Expand Down
2 changes: 2 additions & 0 deletions src/Orleans.CodeGenerator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ ORLEANS0110 | Usage | Error | ReferenceAssemblyWithGenerateSerializerDiagnostic
ORLEANS0111 | Usage | Error | Invalid invokable base type mapping
ORLEANS0112 | Usage | Error | Invalid RPC parameter field identifier
ORLEANS0113 | Usage | Warning | CancellationToken parameter is not last
ORLEANS0114 | Usage | Error | Invalid serializer context declaration
ORLEANS0115 | Usage | Error | Unsupported serializer context dependency
2 changes: 2 additions & 0 deletions src/Orleans.CodeGenerator/Diagnostics/DiagnosticRuleId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ internal static class DiagnosticRuleId
public const string InvalidInvokableBaseTypeMapping = "ORLEANS0111";
public const string InvalidRpcParameterId = "ORLEANS0112";
public const string CancellationTokenNotLast = "ORLEANS0113";
public const string InvalidSerializerContext = "ORLEANS0114";
public const string UnsupportedSerializerContextType = "ORLEANS0115";
}
36 changes: 20 additions & 16 deletions src/Orleans.CodeGenerator/MetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,18 @@ private ClassDeclarationSyntax GenerateIncrementalMetadata()
AddContracts(body, configParam, type, addActivatorMethod);
}

var addWellKnownTypeIdMethod = configParam.Member("WellKnownTypeIds").Member("Add");
foreach (var type in model.ReferenceAssemblyData.WellKnownTypeIds)
{
body.Add(ExpressionStatement(InvocationExpression(addWellKnownTypeIdMethod,
ArgumentList(SeparatedList(
[
Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(type.Id))),
Argument(CreateTypeOfExpression(type.Type)),
])))));
body.Add(CreateTypeMetadataRegistration(configParam.Member("WellKnownTypeIds"),
LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(type.Id)),
CreateTypeOfExpression(type.Type)));
}

var addTypeAliasMethod = configParam.Member("WellKnownTypeAliases").Member("Add");
foreach (var type in model.ReferenceAssemblyData.TypeAliases)
{
body.Add(ExpressionStatement(InvocationExpression(addTypeAliasMethod,
ArgumentList(SeparatedList(
[
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(type.Alias))),
Argument(CreateTypeOfExpression(type.Type)),
])))));
body.Add(CreateTypeMetadataRegistration(configParam.Member("WellKnownTypeAliases"),
LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(type.Alias)),
CreateTypeOfExpression(type.Type)));
}

foreach (var provider in model.ReferenceAssemblyData.RegisteredProviders)
Expand Down Expand Up @@ -265,12 +257,12 @@ private void AddCompoundTypeAliases(
body.Add(LocalDeclarationStatement(VariableDeclaration(
ParseTypeName("var"),
SingletonSeparatedList(VariableDeclarator(nodeName.Identifier).WithInitializer(EqualsValueClause(InvocationExpression(
tree.Member("Add"),
tree.Member(aliases.HasValue ? "Add" : "GetOrAdd"),
ArgumentList(SeparatedList(addArguments)))))))));
}
else
{
body.Add(ExpressionStatement(InvocationExpression(tree.Member("Add"), ArgumentList(SeparatedList(addArguments)))));
body.Add(ExpressionStatement(InvocationExpression(tree.Member(aliases.HasValue ? "Add" : "GetOrAdd"), ArgumentList(SeparatedList(addArguments)))));
}
}

Expand Down Expand Up @@ -744,6 +736,18 @@ private static ExpressionSyntax GetSerializationTypeExpression(SerializationType
arguments.AddRange(description.Arguments.Select(argument => Argument(GetSerializationTypeExpression(argument))));
return InvocationExpression(type.Member("Create"), ArgumentList(SeparatedList(arguments)));
}
internal static StatementSyntax CreateTypeMetadataRegistration(
ExpressionSyntax dictionary, ExpressionSyntax key, ExpressionSyntax type)
=> ParseStatement($$"""
{
var registeredType = {{type}};
if ({{dictionary}}.TryGetValue({{key}}, out var existingType) && existingType != registeredType)
{
throw new global::System.InvalidOperationException("Conflicting type metadata registration for " + {{key}} + ".");
}
{{dictionary}}[{{key}}] = registeredType;
}
""");

private static bool ShouldGenerateActivator(SerializableTypeModel type)
=> !type.IsAbstractType
Expand Down
Loading
Loading