diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 942a2a1695..a78931c8e0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -142,6 +142,72 @@ jobs:
shell: bash
run: python ./ci/run_ci.py java --version windows_java21
+ csharp:
+ name: C# CI
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Set up .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "8.0.x"
+ cache: true
+ cache-dependency-path: |
+ csharp/**/*.csproj
+ csharp/Fory.sln
+ - name: Restore C# dependencies
+ run: |
+ cd csharp
+ dotnet restore Fory.sln
+ - name: Build C# solution
+ run: |
+ cd csharp
+ dotnet build Fory.sln -c Release --no-restore
+ - name: Run C# tests
+ run: |
+ cd csharp
+ dotnet test tests/Fory.Tests/Fory.Tests.csproj -c Release --no-build
+
+ csharp_xlang:
+ name: C# Xlang Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Set up .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "8.0.x"
+ cache: true
+ cache-dependency-path: |
+ csharp/**/*.csproj
+ csharp/Fory.sln
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: 21
+ distribution: "temurin"
+ - name: Cache Maven local repository
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+ - name: Restore and build C# xlang peer
+ run: |
+ cd csharp
+ dotnet restore tests/Fory.XlangPeer/Fory.XlangPeer.csproj
+ dotnet build tests/Fory.XlangPeer/Fory.XlangPeer.csproj -c Debug --no-restore
+ - name: Run C# Xlang Test
+ env:
+ FORY_CSHARP_JAVA_CI: "1"
+ ENABLE_FORY_DEBUG_OUTPUT: "1"
+ run: |
+ cd java
+ mvn -T16 --no-transfer-progress clean install -DskipTests
+ cd fory-core
+ mvn -T16 --no-transfer-progress test -Dtest=org.apache.fory.xlang.CSharpXlangTest
+
swift:
name: Swift CI
runs-on: macos-latest
diff --git a/.gitignore b/.gitignore
index db9be7edff..2c0f134e6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,4 +106,13 @@ benchmarks/**/report/
ignored/**
ci-logs/**
**/*.log
-swift/.build
\ No newline at end of file
+swift/.build
+
+csharp/src/Fory.Generator/bin/
+csharp/src/Fory.Generator/obj/
+csharp/src/Fory/bin/
+csharp/src/Fory/obj/
+csharp/tests/Fory.Tests/bin/
+csharp/tests/Fory.Tests/obj/
+csharp/tests/Fory.XlangPeer/bin/
+csharp/tests/Fory.XlangPeer/obj/
\ No newline at end of file
diff --git a/csharp/Fory.sln b/csharp/Fory.sln
new file mode 100644
index 0000000000..b2f9edf7b1
--- /dev/null
+++ b/csharp/Fory.sln
@@ -0,0 +1,50 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C3FE93C6-2294-475C-88DD-BE1FFFADC4D6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory", "src\Fory\Fory.csproj", "{309709EA-773D-449D-AE12-D1C2B9518793}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory.Generator", "src\Fory.Generator\Fory.Generator.csproj", "{4DBAD732-2820-4B15-B655-19384F100997}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02DC4D41-0522-42EA-B643-471F4BB0747E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory.Tests", "tests\Fory.Tests\Fory.Tests.csproj", "{65701195-E254-4D93-9CD0-F587FDFCE769}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fory.XlangPeer", "tests\Fory.XlangPeer\Fory.XlangPeer.csproj", "{8E1D5E47-AF72-46FF-B60F-B1C6210654A4}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {309709EA-773D-449D-AE12-D1C2B9518793}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {309709EA-773D-449D-AE12-D1C2B9518793}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {309709EA-773D-449D-AE12-D1C2B9518793}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {309709EA-773D-449D-AE12-D1C2B9518793}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4DBAD732-2820-4B15-B655-19384F100997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4DBAD732-2820-4B15-B655-19384F100997}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4DBAD732-2820-4B15-B655-19384F100997}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4DBAD732-2820-4B15-B655-19384F100997}.Release|Any CPU.Build.0 = Release|Any CPU
+ {65701195-E254-4D93-9CD0-F587FDFCE769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {65701195-E254-4D93-9CD0-F587FDFCE769}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {65701195-E254-4D93-9CD0-F587FDFCE769}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {65701195-E254-4D93-9CD0-F587FDFCE769}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E1D5E47-AF72-46FF-B60F-B1C6210654A4}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {309709EA-773D-449D-AE12-D1C2B9518793} = {C3FE93C6-2294-475C-88DD-BE1FFFADC4D6}
+ {4DBAD732-2820-4B15-B655-19384F100997} = {C3FE93C6-2294-475C-88DD-BE1FFFADC4D6}
+ {65701195-E254-4D93-9CD0-F587FDFCE769} = {02DC4D41-0522-42EA-B643-471F4BB0747E}
+ {8E1D5E47-AF72-46FF-B60F-B1C6210654A4} = {02DC4D41-0522-42EA-B643-471F4BB0747E}
+ EndGlobalSection
+EndGlobal
diff --git a/csharp/README.md b/csharp/README.md
new file mode 100644
index 0000000000..a3300bee6c
--- /dev/null
+++ b/csharp/README.md
@@ -0,0 +1 @@
+# Apache Fory™ C\#
diff --git a/csharp/src/Fory.Generator/Fory.Generator.csproj b/csharp/src/Fory.Generator/Fory.Generator.csproj
new file mode 100644
index 0000000000..48864e20cf
--- /dev/null
+++ b/csharp/src/Fory.Generator/Fory.Generator.csproj
@@ -0,0 +1,16 @@
+
+
+ netstandard2.0
+ 12.0
+ enable
+ enable
+ true
+ true
+ false
+
+
+
+
+
+
+
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
new file mode 100644
index 0000000000..6cb40c931c
--- /dev/null
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -0,0 +1,1546 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Collections.Immutable;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Apache.Fory.Generator;
+
+[Generator(LanguageNames.CSharp)]
+public sealed class ForyObjectGenerator : IIncrementalGenerator
+{
+ private static readonly SymbolDisplayFormat FullNameFormat =
+ SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
+ SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
+
+ private static readonly DiagnosticDescriptor GenericTypeNotSupported = new(
+ id: "FORY001",
+ title: "Generic types are not supported by ForyObject generator",
+ messageFormat: "Type '{0}' is generic and is not supported by [ForyObject].",
+ category: "Fory",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor MissingCtor = new(
+ id: "FORY002",
+ title: "Missing parameterless constructor",
+ messageFormat: "Class '{0}' must declare an accessible parameterless constructor for [ForyObject].",
+ category: "Fory",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor UnsupportedEncoding = new(
+ id: "FORY003",
+ title: "Unsupported Field encoding",
+ messageFormat: "Member '{0}' uses unsupported [Field] encoding for type '{1}'.",
+ category: "Fory",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ IncrementalValuesProvider typeModels = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ "Apache.Fory.ForyObjectAttribute",
+ static (node, _) => node is TypeDeclarationSyntax || node is EnumDeclarationSyntax,
+ static (syntaxContext, ct) => BuildTypeModel(syntaxContext, ct))
+ .Where(static m => m is not null);
+
+ context.RegisterSourceOutput(
+ typeModels.Collect(),
+ static (spc, models) => Emit(spc, models));
+ }
+
+ private static void Emit(SourceProductionContext context, ImmutableArray maybeModels)
+ {
+ if (maybeModels.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ Dictionary models = new(StringComparer.Ordinal);
+ foreach (TypeModel? maybeModel in maybeModels)
+ {
+ if (maybeModel is null)
+ {
+ continue;
+ }
+
+ models[maybeModel.TypeName] = maybeModel;
+ }
+
+ if (models.Count == 0)
+ {
+ return;
+ }
+
+ StringBuilder sb = new();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("namespace Apache.Fory.Generated;");
+ sb.AppendLine();
+
+ foreach (KeyValuePair entry in models.OrderBy(kv => kv.Key, StringComparer.Ordinal))
+ {
+ TypeModel model = entry.Value;
+ if (model.Kind == DeclKind.Struct || model.Kind == DeclKind.Class)
+ {
+ EmitObjectSerializer(sb, model);
+ sb.AppendLine();
+ }
+ }
+
+ sb.AppendLine("internal static class __ForyGeneratedModuleInitializer");
+ sb.AppendLine("{");
+ sb.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]");
+ sb.AppendLine(" internal static void Register()");
+ sb.AppendLine(" {");
+ foreach (KeyValuePair entry in models.OrderBy(kv => kv.Key, StringComparer.Ordinal))
+ {
+ TypeModel model = entry.Value;
+ if (model.Kind == DeclKind.Enum)
+ {
+ sb.AppendLine(
+ $" global::Apache.Fory.TypeResolver.RegisterGenerated<{model.TypeName}, global::Apache.Fory.EnumSerializer<{model.TypeName}>>();");
+ }
+ else
+ {
+ sb.AppendLine(
+ $" global::Apache.Fory.TypeResolver.RegisterGenerated<{model.TypeName}, {model.SerializerName}>();");
+ }
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+
+ context.AddSource("Fory.GeneratedSerializers.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
+ }
+
+ private static void EmitObjectSerializer(StringBuilder sb, TypeModel model)
+ {
+ sb.AppendLine($"file sealed class {model.SerializerName} : global::Apache.Fory.Serializer<{model.TypeName}>");
+ sb.AppendLine("{");
+ sb.AppendLine(" private static global::Apache.Fory.RefMode __ForyRefMode(bool nullable, bool trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return global::Apache.Fory.RefMode.Tracking;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" return nullable ? global::Apache.Fory.RefMode.NullOnly : global::Apache.Fory.RefMode.None;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" private static bool __ForyNeedsTypeInfoForField(global::Apache.Fory.TypeId typeId)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return typeId switch");
+ sb.AppendLine(" {");
+ sb.AppendLine(" global::Apache.Fory.TypeId.Struct or");
+ sb.AppendLine(" global::Apache.Fory.TypeId.CompatibleStruct or");
+ sb.AppendLine(" global::Apache.Fory.TypeId.NamedStruct or");
+ sb.AppendLine(" global::Apache.Fory.TypeId.NamedCompatibleStruct or");
+ sb.AppendLine(" global::Apache.Fory.TypeId.Ext or");
+ sb.AppendLine(" global::Apache.Fory.TypeId.NamedExt or");
+ sb.AppendLine(" global::Apache.Fory.TypeId.Unknown => true,");
+ sb.AppendLine(" _ => false,");
+ sb.AppendLine(" };");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" private static uint __ForySchemaHash(bool trackRef, global::Apache.Fory.TypeResolver typeResolver)");
+ sb.AppendLine(" {");
+ sb.Append(" return global::Apache.Fory.SchemaHash.StructHash32(");
+ sb.Append(BuildSchemaFingerprintExpression(model.Members));
+ sb.AppendLine(");");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" public override global::Apache.Fory.TypeId StaticTypeId => global::Apache.Fory.TypeId.Struct;");
+ if (model.Kind == DeclKind.Class)
+ {
+ sb.AppendLine(" public override bool IsNullableType => true;");
+ sb.AppendLine(" public override bool IsReferenceTrackableType => true;");
+ sb.AppendLine($" public override {model.TypeName} DefaultValue => null!;");
+ sb.AppendLine($" public override bool IsNone(in {model.TypeName} value) => value is null;");
+ }
+ else
+ {
+ sb.AppendLine($" public override {model.TypeName} DefaultValue => new {model.TypeName}();");
+ }
+
+ sb.AppendLine();
+ sb.AppendLine(" public override global::System.Collections.Generic.IReadOnlyList CompatibleTypeMetaFields(bool trackRef)");
+ sb.AppendLine(" {");
+ if (model.SortedMembers.Length == 0)
+ {
+ sb.AppendLine(" return global::System.Array.Empty();");
+ }
+ else
+ {
+ sb.AppendLine(" return new global::Apache.Fory.TypeMetaFieldInfo[]");
+ sb.AppendLine(" {");
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ sb.AppendLine(
+ $" new global::Apache.Fory.TypeMetaFieldInfo(null, \"{EscapeString(member.Name)}\", {BuildCompatibleTypeMetaExpression(member.TypeMeta, "trackRef")}),");
+ }
+
+ sb.AppendLine(" };");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(
+ $" public override void WriteData(ref global::Apache.Fory.WriteContext context, in {model.TypeName} value, bool hasGenerics)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _ = hasGenerics;");
+ sb.AppendLine(" if (context.Compatible)");
+ sb.AppendLine(" {");
+ if (model.SortedMembers.Length == 0)
+ {
+ sb.AppendLine(" return;");
+ }
+ else
+ {
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ EmitWriteMember(sb, member, true);
+ }
+
+ sb.AppendLine(" return;");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" context.Writer.WriteInt32(unchecked((int)__ForySchemaHash(context.TrackRef, context.TypeResolver)));");
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ EmitWriteMember(sb, member, false);
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine($" public override {model.TypeName} ReadData(ref global::Apache.Fory.ReadContext context)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (context.Compatible)");
+ sb.AppendLine(" {");
+ sb.AppendLine($" global::Apache.Fory.TypeMeta typeMeta = context.ConsumeCompatibleTypeMeta(typeof({model.TypeName}));");
+ sb.AppendLine($" {model.TypeName} value = new {model.TypeName}();");
+ if (model.Kind == DeclKind.Class)
+ {
+ sb.AppendLine(" context.BindPendingReference(value);");
+ }
+
+ sb.AppendLine(" foreach (global::Apache.Fory.TypeMetaFieldInfo remoteField in typeMeta.Fields)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" global::Apache.Fory.RefMode remoteRefMode = __ForyRefMode(remoteField.FieldType.Nullable, remoteField.FieldType.TrackRef);");
+ sb.AppendLine(" bool remoteReadTypeInfo = __ForyNeedsTypeInfoForField((global::Apache.Fory.TypeId)remoteField.FieldType.TypeId);");
+ sb.AppendLine(" switch (remoteField.FieldName)");
+ sb.AppendLine(" {");
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ sb.AppendLine($" case \"{EscapeString(member.FieldIdentifier)}\":");
+ sb.AppendLine(" {");
+ EmitReadMemberAssignment(sb, member, "remoteRefMode", "remoteReadTypeInfo", "value", "Compat", 7, false);
+ sb.AppendLine(" break;");
+ sb.AppendLine(" }");
+ }
+
+ sb.AppendLine(" default:");
+ sb.AppendLine(" global::Apache.Fory.FieldSkipper.SkipFieldValue(ref context, remoteField.FieldType);");
+ sb.AppendLine(" break;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine(" return value;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" uint schemaHash = unchecked((uint)context.Reader.ReadInt32());");
+ sb.AppendLine(" uint expectedHash = __ForySchemaHash(context.TrackRef, context.TypeResolver);");
+ sb.AppendLine(" if (schemaHash != expectedHash)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" throw new global::Apache.Fory.InvalidDataException($\"class version hash mismatch: expected {expectedHash}, got {schemaHash}\");");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine($" {model.TypeName} valueSchema = new {model.TypeName}();");
+ if (model.Kind == DeclKind.Class)
+ {
+ sb.AppendLine(" context.BindPendingReference(valueSchema);");
+ }
+
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ EmitReadMemberAssignment(sb, member, BuildWriteRefModeExpression(member), "false", "valueSchema", "Schema", 2, true);
+ }
+
+ sb.AppendLine(" return valueSchema;");
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+ }
+
+ private static void EmitWriteMember(StringBuilder sb, MemberModel member, bool compatibleMode)
+ {
+ string refModeExpr = BuildWriteRefModeExpression(member);
+ string memberAccess = $"value.{member.Name}";
+ string hasGenerics = member.IsCollection ? "true" : "false";
+ string writeTypeInfo = compatibleMode
+ ? $"__ForyNeedsTypeInfoForField(context.TypeResolver.GetTypeInfo<{member.TypeName}>().StaticTypeId)"
+ : "false";
+
+ switch (member.DynamicAnyKind)
+ {
+ case DynamicAnyKind.AnyValue:
+ sb.AppendLine(
+ $" global::Apache.Fory.DynamicAnyCodec.WriteAny(ref context, {memberAccess}, {refModeExpr}, true, false);");
+ return;
+ case DynamicAnyKind.None:
+ break;
+ default:
+ throw new InvalidOperationException($"unsupported dynamic any kind {member.DynamicAnyKind}");
+ }
+
+ if (!member.IsNullable && TryBuildDirectFieldWrite(member, memberAccess, out string? directWriteCode))
+ {
+ sb.AppendLine($" {directWriteCode}");
+ return;
+ }
+
+ if (TryBuildNullableFixedTaggedFieldWrite(member, memberAccess, out string? nullableWriteCode))
+ {
+ sb.AppendLine($" {nullableWriteCode}");
+ return;
+ }
+
+ sb.AppendLine(
+ $" context.TypeResolver.GetSerializer<{member.TypeName}>().Write(ref context, {memberAccess}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});");
+ }
+
+ private static void EmitReadMemberAssignment(
+ StringBuilder sb,
+ MemberModel member,
+ string refModeExpr,
+ string readTypeInfoExpr,
+ string valueVar,
+ string variableSuffix,
+ int indentLevel,
+ bool allowDirectRead)
+ {
+ string indent = new(' ', indentLevel * 2);
+ string assignmentTarget = $"{valueVar}.{member.Name}";
+ string typeOfTypeName = StripNullableForTypeOf(member.TypeName);
+ switch (member.DynamicAnyKind)
+ {
+ case DynamicAnyKind.AnyValue:
+ sb.AppendLine(
+ $"{indent}{assignmentTarget} = ({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAny(ref context, {refModeExpr}, true), typeof({typeOfTypeName}))!;");
+ return;
+ case DynamicAnyKind.None:
+ break;
+ default:
+ throw new InvalidOperationException($"unsupported dynamic any kind {member.DynamicAnyKind}");
+ }
+
+ if (allowDirectRead && !member.IsNullable && TryBuildDirectFieldRead(member, out string? directReadExpr))
+ {
+ sb.AppendLine($"{indent}{assignmentTarget} = {directReadExpr};");
+ return;
+ }
+
+ if (allowDirectRead && TryBuildNullableFixedTaggedFieldRead(member, assignmentTarget, variableSuffix, indent, out string? nullableReadCode))
+ {
+ sb.AppendLine(nullableReadCode);
+ return;
+ }
+
+ sb.AppendLine(
+ $"{indent}{assignmentTarget} = context.TypeResolver.GetSerializer<{member.TypeName}>().Read(ref context, {refModeExpr}, {readTypeInfoExpr});");
+ }
+
+ private static string StripNullableForTypeOf(string typeName)
+ {
+ return typeName.Replace("?", string.Empty);
+ }
+
+ private static bool TryBuildDirectFieldWrite(MemberModel member, string memberAccess, out string? writeCode)
+ {
+ writeCode = null;
+ if (!CanUseDirectBuiltInFieldAccess(member))
+ {
+ return false;
+ }
+
+ return TryBuildDirectPayloadWrite(member.Classification.TypeId, memberAccess, out writeCode);
+ }
+
+ private static bool TryBuildDirectFieldRead(MemberModel member, out string? readExpr)
+ {
+ readExpr = null;
+ if (!CanUseDirectBuiltInFieldAccess(member))
+ {
+ return false;
+ }
+
+ return TryBuildDirectPayloadRead(member.Classification.TypeId, out readExpr);
+ }
+
+ private static bool TryBuildNullableFixedTaggedFieldWrite(MemberModel member, string memberAccess, out string? writeCode)
+ {
+ writeCode = null;
+ if (!member.IsNullableValueType || !IsFixedTaggedTypeId(member.Classification.TypeId))
+ {
+ return false;
+ }
+
+ if (!TryBuildDirectPayloadWrite(member.Classification.TypeId, $"{memberAccess}.Value", out string? payloadWriteCode))
+ {
+ return false;
+ }
+
+ writeCode = $"if (!{memberAccess}.HasValue) {{ context.Writer.WriteInt8((sbyte)global::Apache.Fory.RefFlag.Null); }} else {{ context.Writer.WriteInt8((sbyte)global::Apache.Fory.RefFlag.NotNullValue); {payloadWriteCode} }}";
+ return true;
+ }
+
+ private static bool TryBuildNullableFixedTaggedFieldRead(
+ MemberModel member,
+ string assignmentTarget,
+ string variableSuffix,
+ string indent,
+ out string code)
+ {
+ code = string.Empty;
+ if (!member.IsNullableValueType || !IsFixedTaggedTypeId(member.Classification.TypeId))
+ {
+ return false;
+ }
+
+ if (!TryBuildDirectPayloadRead(member.Classification.TypeId, out string? payloadReadExpr))
+ {
+ return false;
+ }
+
+ string refFlagVar = $"__{member.Name}RefFlag{variableSuffix}";
+ string nestedIndent = indent + " ";
+ StringBuilder sb = new();
+ sb.AppendLine($"{indent}sbyte {refFlagVar} = context.Reader.ReadInt8();");
+ sb.AppendLine($"{indent}if ({refFlagVar} == (sbyte)global::Apache.Fory.RefFlag.Null)");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{nestedIndent}{assignmentTarget} = ({member.TypeName})null!;");
+ sb.AppendLine($"{indent}}}");
+ sb.AppendLine($"{indent}else");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{nestedIndent}{assignmentTarget} = {payloadReadExpr};");
+ sb.Append($"{indent}}}");
+ code = sb.ToString();
+ return true;
+ }
+
+ private static bool IsFixedTaggedTypeId(uint typeId)
+ {
+ return typeId is 4 or 6 or 8 or 11 or 13 or 15;
+ }
+
+ private static bool TryBuildDirectPayloadWrite(uint typeId, string valueExpr, out string? writeCode)
+ {
+ writeCode = null;
+ switch (typeId)
+ {
+ case 1:
+ writeCode = $"context.Writer.WriteUInt8({valueExpr} ? (byte)1 : (byte)0);";
+ return true;
+ case 2:
+ writeCode = $"context.Writer.WriteInt8({valueExpr});";
+ return true;
+ case 3:
+ writeCode = $"context.Writer.WriteInt16({valueExpr});";
+ return true;
+ case 4:
+ writeCode = $"context.Writer.WriteInt32({valueExpr});";
+ return true;
+ case 5:
+ writeCode = $"context.Writer.WriteVarInt32({valueExpr});";
+ return true;
+ case 6:
+ writeCode = $"context.Writer.WriteInt64({valueExpr});";
+ return true;
+ case 7:
+ writeCode = $"context.Writer.WriteVarInt64({valueExpr});";
+ return true;
+ case 8:
+ writeCode = $"context.Writer.WriteTaggedInt64({valueExpr});";
+ return true;
+ case 9:
+ writeCode = $"context.Writer.WriteUInt8({valueExpr});";
+ return true;
+ case 10:
+ writeCode = $"context.Writer.WriteUInt16({valueExpr});";
+ return true;
+ case 11:
+ writeCode = $"context.Writer.WriteUInt32({valueExpr});";
+ return true;
+ case 12:
+ writeCode = $"context.Writer.WriteVarUInt32({valueExpr});";
+ return true;
+ case 13:
+ writeCode = $"context.Writer.WriteUInt64({valueExpr});";
+ return true;
+ case 14:
+ writeCode = $"context.Writer.WriteVarUInt64({valueExpr});";
+ return true;
+ case 15:
+ writeCode = $"context.Writer.WriteTaggedUInt64({valueExpr});";
+ return true;
+ case 19:
+ writeCode = $"context.Writer.WriteFloat32({valueExpr});";
+ return true;
+ case 20:
+ writeCode = $"context.Writer.WriteFloat64({valueExpr});";
+ return true;
+ case 21:
+ writeCode = $"global::Apache.Fory.StringSerializer.WriteString(ref context, {valueExpr});";
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static bool TryBuildDirectPayloadRead(uint typeId, out string? readExpr)
+ {
+ readExpr = null;
+ switch (typeId)
+ {
+ case 1:
+ readExpr = "context.Reader.ReadUInt8() != 0";
+ return true;
+ case 2:
+ readExpr = "context.Reader.ReadInt8()";
+ return true;
+ case 3:
+ readExpr = "context.Reader.ReadInt16()";
+ return true;
+ case 4:
+ readExpr = "context.Reader.ReadInt32()";
+ return true;
+ case 5:
+ readExpr = "context.Reader.ReadVarInt32()";
+ return true;
+ case 6:
+ readExpr = "context.Reader.ReadInt64()";
+ return true;
+ case 7:
+ readExpr = "context.Reader.ReadVarInt64()";
+ return true;
+ case 8:
+ readExpr = "context.Reader.ReadTaggedInt64()";
+ return true;
+ case 9:
+ readExpr = "context.Reader.ReadUInt8()";
+ return true;
+ case 10:
+ readExpr = "context.Reader.ReadUInt16()";
+ return true;
+ case 11:
+ readExpr = "context.Reader.ReadUInt32()";
+ return true;
+ case 12:
+ readExpr = "context.Reader.ReadVarUInt32()";
+ return true;
+ case 13:
+ readExpr = "context.Reader.ReadUInt64()";
+ return true;
+ case 14:
+ readExpr = "context.Reader.ReadVarUInt64()";
+ return true;
+ case 15:
+ readExpr = "context.Reader.ReadTaggedUInt64()";
+ return true;
+ case 19:
+ readExpr = "context.Reader.ReadFloat32()";
+ return true;
+ case 20:
+ readExpr = "context.Reader.ReadFloat64()";
+ return true;
+ case 21:
+ readExpr = "global::Apache.Fory.StringSerializer.ReadString(ref context)";
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static bool CanUseDirectBuiltInFieldAccess(MemberModel member)
+ {
+ if (member.IsNullable ||
+ member.DynamicAnyKind != DynamicAnyKind.None ||
+ member.IsCollection ||
+ member.Classification.IsMap)
+ {
+ return false;
+ }
+
+ return member.Classification.IsPrimitive || member.Classification.TypeId == 21;
+ }
+
+ private static string BuildSchemaFingerprintExpression(ImmutableArray members)
+ {
+ if (members.IsDefaultOrEmpty)
+ {
+ return "\"\"";
+ }
+
+ IEnumerable ordered = members
+ .OrderBy(m => m.FieldIdentifier, StringComparer.Ordinal)
+ .ThenBy(m => m.OriginalIndex);
+
+ StringBuilder sb = new();
+ bool first = true;
+ foreach (MemberModel member in ordered)
+ {
+ uint fingerprintTypeId = (member.Classification.IsPrimitive || member.Classification.IsBuiltIn)
+ ? member.Classification.TypeId
+ : 0;
+ string trackRefExpr = member.DynamicAnyKind switch
+ {
+ DynamicAnyKind.AnyValue => "(trackRef ? 1 : 0)",
+ _ => member.Classification.IsBuiltIn
+ ? "0"
+ : $"((trackRef && typeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)",
+ };
+ string nullable = member.IsNullable ? "1" : "0";
+ string piece = $"\"{EscapeString(member.FieldIdentifier)},{fingerprintTypeId},\" + {trackRefExpr} + \",{nullable};\"";
+ if (!first)
+ {
+ sb.Append(" + ");
+ }
+
+ first = false;
+ sb.Append(piece);
+ }
+
+ return sb.ToString();
+ }
+
+ private static string BuildCompatibleTypeMetaExpression(TypeMetaFieldTypeModel model, string trackRefExpr)
+ {
+ string localTrackRefExpr = model.TrackRefByContext ? trackRefExpr : "false";
+ if (model.Generics.Length > 0)
+ {
+ string generics = string.Join(
+ ", ",
+ model.Generics.Select(g => BuildCompatibleTypeMetaExpression(g, "false")));
+ return
+ $"new global::Apache.Fory.TypeMetaFieldType({model.TypeIdExpr}, {BoolLiteral(model.Nullable)}, {localTrackRefExpr}, new global::Apache.Fory.TypeMetaFieldType[] {{ {generics} }})";
+ }
+
+ return $"new global::Apache.Fory.TypeMetaFieldType({model.TypeIdExpr}, {BoolLiteral(model.Nullable)}, {localTrackRefExpr})";
+ }
+
+ private static string BuildWriteRefModeExpression(MemberModel member)
+ {
+ return member.DynamicAnyKind switch
+ {
+ DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)",
+ _ => member.Classification.IsBuiltIn
+ ? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)"
+ : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && context.TypeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType)",
+ };
+ }
+
+ private static TypeModel? BuildTypeModel(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
+ {
+ _ = cancellationToken;
+ if (context.TargetSymbol is not INamedTypeSymbol typeSymbol)
+ {
+ return null;
+ }
+
+ if (typeSymbol.TypeParameters.Length > 0)
+ {
+ return null;
+ }
+
+ string typeName = typeSymbol.ToDisplayString(FullNameFormat);
+ string serializerName = "__ForySerializer_" + Sanitize(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ if (typeSymbol.TypeKind == TypeKind.Enum)
+ {
+ return new TypeModel(
+ typeName,
+ serializerName,
+ DeclKind.Enum,
+ ImmutableArray.Empty,
+ ImmutableArray.Empty);
+ }
+
+ DeclKind kind = typeSymbol.TypeKind switch
+ {
+ TypeKind.Struct => DeclKind.Struct,
+ TypeKind.Class => DeclKind.Class,
+ _ => DeclKind.Unknown,
+ };
+
+ if (kind == DeclKind.Unknown)
+ {
+ return null;
+ }
+
+ if (kind == DeclKind.Class && !HasAccessibleParameterlessCtor(typeSymbol))
+ {
+ return null;
+ }
+
+ List members = [];
+ foreach (ISymbol member in typeSymbol.GetMembers())
+ {
+ if (member.IsStatic)
+ {
+ continue;
+ }
+
+ if (member is IFieldSymbol field)
+ {
+ if (field.IsConst || field.IsReadOnly || !IsReadableWritableAccessibility(field.DeclaredAccessibility))
+ {
+ continue;
+ }
+
+ MemberModel? parsedField = BuildMemberModel(field.Name, field.Type, field, MemberDeclKind.Field);
+ if (parsedField is not null)
+ {
+ members.Add(parsedField);
+ }
+
+ continue;
+ }
+
+ if (member is IPropertySymbol property)
+ {
+ if (property.IsIndexer || property.GetMethod is null || property.SetMethod is null)
+ {
+ continue;
+ }
+
+ if (property.SetMethod.IsInitOnly)
+ {
+ continue;
+ }
+
+ if (!IsReadableWritableAccessibility(property.GetMethod.DeclaredAccessibility) ||
+ !IsReadableWritableAccessibility(property.SetMethod.DeclaredAccessibility))
+ {
+ continue;
+ }
+
+ MemberModel? parsedProperty = BuildMemberModel(
+ property.Name,
+ property.Type,
+ property,
+ MemberDeclKind.Property);
+ if (parsedProperty is not null)
+ {
+ members.Add(parsedProperty);
+ }
+ }
+ }
+
+ ImmutableArray ordered = members
+ .OrderBy(m => m.OriginalIndex)
+ .ToImmutableArray();
+ ImmutableArray sorted = SortMembers(ordered);
+
+ return new TypeModel(typeName, serializerName, kind, ordered, sorted);
+ }
+
+ private static MemberModel? BuildMemberModel(
+ string name,
+ ITypeSymbol memberType,
+ ISymbol memberSymbol,
+ MemberDeclKind memberDeclKind)
+ {
+ (bool isOptional, ITypeSymbol unwrappedType) = UnwrapNullable(memberType);
+ FieldEncoding fieldEncoding = FieldEncoding.None;
+ foreach (AttributeData attribute in memberSymbol.GetAttributes())
+ {
+ string? attrName = attribute.AttributeClass?.ToDisplayString();
+ if (!string.Equals(attrName, "Apache.Fory.FieldAttribute", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ foreach (KeyValuePair namedArg in attribute.NamedArguments)
+ {
+ if (!string.Equals(namedArg.Key, "Encoding", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (namedArg.Value.Value is int encoding)
+ {
+ fieldEncoding = (FieldEncoding)encoding;
+ }
+ }
+ }
+
+ DynamicAnyKind dynamicAnyKind = ResolveDynamicAnyKind(unwrappedType);
+ TypeResolution resolution = ResolveTypeResolution(unwrappedType, fieldEncoding);
+ if (!resolution.Supported)
+ {
+ return null;
+ }
+
+ TypeClassification classification = resolution.Classification;
+ int group = classification.IsPrimitive
+ ? (isOptional ? 2 : 1)
+ : classification.IsMap
+ ? 5
+ : classification.IsCollection
+ ? 4
+ : classification.IsBuiltIn
+ ? 3
+ : 6;
+
+ int index = int.MaxValue;
+ Location? sourceLocation = memberSymbol.Locations.FirstOrDefault(loc => loc.IsInSource);
+ if (sourceLocation is not null)
+ {
+ index = sourceLocation.SourceSpan.Start;
+ }
+
+ string typeName = memberType.ToDisplayString(FullNameFormat);
+ TypeMetaFieldTypeModel typeMeta = BuildTypeMetaFieldTypeModel(
+ memberType,
+ isOptional,
+ dynamicAnyKind,
+ resolution.Classification.TypeId);
+
+ return new MemberModel(
+ name,
+ ToSnakeCase(name),
+ index,
+ memberDeclKind,
+ typeName,
+ isOptional,
+ memberType is INamedTypeSymbol nts &&
+ nts.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T,
+ classification,
+ group,
+ classification.IsCollection || classification.IsMap,
+ dynamicAnyKind == DynamicAnyKind.None ? DynamicAnyKind.None : dynamicAnyKind,
+ typeMeta);
+ }
+
+ private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel(
+ ITypeSymbol memberType,
+ bool nullable,
+ DynamicAnyKind dynamicAnyKind,
+ uint explicitTypeId)
+ {
+ (bool _, ITypeSymbol unwrapped) = UnwrapNullable(memberType);
+
+ if (TryGetListElementType(unwrapped, out ITypeSymbol? listElementType))
+ {
+ bool elementNullable = GenericNullable(listElementType!);
+ TypeMetaFieldTypeModel element = BuildTypeMetaFieldTypeModel(
+ listElementType!,
+ elementNullable,
+ ResolveDynamicAnyKind(UnwrapNullable(listElementType!).Item2),
+ 0);
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.List",
+ nullable,
+ false,
+ ImmutableArray.Create(element));
+ }
+
+ if (TryGetSetElementType(unwrapped, out ITypeSymbol? setElementType))
+ {
+ bool elementNullable = GenericNullable(setElementType!);
+ TypeMetaFieldTypeModel element = BuildTypeMetaFieldTypeModel(
+ setElementType!,
+ elementNullable,
+ ResolveDynamicAnyKind(UnwrapNullable(setElementType!).Item2),
+ 0);
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.Set",
+ nullable,
+ false,
+ ImmutableArray.Create(element));
+ }
+
+ if (TryGetMapTypeArguments(unwrapped, out ITypeSymbol? keyType, out ITypeSymbol? valueType))
+ {
+ bool keyNullable = GenericNullable(keyType!);
+ bool valueNullable = GenericNullable(valueType!);
+ TypeMetaFieldTypeModel key = BuildTypeMetaFieldTypeModel(
+ keyType!,
+ keyNullable,
+ ResolveDynamicAnyKind(UnwrapNullable(keyType!).Item2),
+ 0);
+ TypeMetaFieldTypeModel value = BuildTypeMetaFieldTypeModel(
+ valueType!,
+ valueNullable,
+ ResolveDynamicAnyKind(UnwrapNullable(valueType!).Item2),
+ 0);
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.Map",
+ nullable,
+ false,
+ ImmutableArray.Create(key, value));
+ }
+
+ TypeClassification classification = ClassifyType(unwrapped);
+ if (explicitTypeId != 0 && classification.IsPrimitive && classification.TypeId != explicitTypeId)
+ {
+ return new TypeMetaFieldTypeModel(
+ explicitTypeId.ToString(),
+ nullable,
+ false,
+ ImmutableArray.Empty);
+ }
+
+ if (IsUnionType(unwrapped))
+ {
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.Union",
+ nullable,
+ true,
+ ImmutableArray.Empty);
+ }
+
+ if (dynamicAnyKind == DynamicAnyKind.AnyValue)
+ {
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.Unknown",
+ nullable,
+ true,
+ ImmutableArray.Empty);
+ }
+
+ if (unwrapped.TypeKind == TypeKind.Enum)
+ {
+ return new TypeMetaFieldTypeModel(
+ "(uint)global::Apache.Fory.TypeId.Enum",
+ nullable,
+ false,
+ ImmutableArray.Empty);
+ }
+
+ return new TypeMetaFieldTypeModel(
+ $"(uint){classification.TypeId}",
+ nullable,
+ !classification.IsBuiltIn && unwrapped.TypeKind != TypeKind.Enum,
+ ImmutableArray.Empty);
+ }
+
+ private static ImmutableArray SortMembers(ImmutableArray members)
+ {
+ return members
+ .OrderBy(m => m.Group)
+ .ThenBy(m =>
+ {
+ if (m.Group is 1 or 2)
+ {
+ return m.Classification.IsCompressedNumeric ? 1 : 0;
+ }
+
+ return 0;
+ })
+ .ThenByDescending(m => m.Group is 1 or 2 ? m.Classification.PrimitiveSize : 0)
+ .ThenBy(m =>
+ {
+ if (m.Group is 1 or 2)
+ {
+ return (int)(uint.MaxValue - m.Classification.TypeId);
+ }
+
+ if (m.Group is 3 or 4 or 5)
+ {
+ return (int)m.Classification.TypeId;
+ }
+
+ return 0;
+ })
+ .ThenBy(m => m.FieldIdentifier, StringComparer.Ordinal)
+ .ThenBy(m => m.Name, StringComparer.Ordinal)
+ .ThenBy(m => m.OriginalIndex)
+ .ToImmutableArray();
+ }
+
+ private static bool GenericNullable(ITypeSymbol type)
+ {
+ (bool optional, ITypeSymbol unwrapped) = UnwrapNullable(type);
+ if (optional)
+ {
+ return true;
+ }
+
+ TypeClassification c = ClassifyType(unwrapped);
+ return !c.IsPrimitive;
+ }
+
+ private static bool IsReadableWritableAccessibility(Accessibility accessibility)
+ {
+ return accessibility is Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal;
+ }
+
+ private static bool HasAccessibleParameterlessCtor(INamedTypeSymbol type)
+ {
+ foreach (IMethodSymbol ctor in type.InstanceConstructors)
+ {
+ if (ctor.Parameters.Length != 0)
+ {
+ continue;
+ }
+
+ if (ctor.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static TypeResolution ResolveTypeResolution(ITypeSymbol type, FieldEncoding encoding)
+ {
+ TypeClassification baseType = ClassifyType(type);
+ if (encoding == FieldEncoding.None)
+ {
+ return new TypeResolution(true, baseType);
+ }
+
+ bool isInt32 = type.SpecialType == SpecialType.System_Int32;
+ bool isUInt32 = type.SpecialType == SpecialType.System_UInt32;
+ bool isInt64 = type.SpecialType == SpecialType.System_Int64;
+ bool isUInt64 = type.SpecialType == SpecialType.System_UInt64;
+
+ if (isInt32)
+ {
+ return encoding switch
+ {
+ FieldEncoding.Varint => new TypeResolution(true, baseType),
+ FieldEncoding.Fixed => new TypeResolution(
+ true,
+ new TypeClassification(4, true, true, false, false, false, 4)),
+ _ => new TypeResolution(false, baseType),
+ };
+ }
+
+ if (isUInt32)
+ {
+ return encoding switch
+ {
+ FieldEncoding.Varint => new TypeResolution(true, baseType),
+ FieldEncoding.Fixed => new TypeResolution(
+ true,
+ new TypeClassification(11, true, true, false, false, false, 4)),
+ _ => new TypeResolution(false, baseType),
+ };
+ }
+
+ if (isInt64)
+ {
+ return encoding switch
+ {
+ FieldEncoding.Varint => new TypeResolution(true, baseType),
+ FieldEncoding.Fixed => new TypeResolution(
+ true,
+ new TypeClassification(6, true, true, false, false, false, 8)),
+ FieldEncoding.Tagged => new TypeResolution(
+ true,
+ new TypeClassification(8, true, true, false, false, true, 8)),
+ _ => new TypeResolution(false, baseType),
+ };
+ }
+
+ if (isUInt64)
+ {
+ return encoding switch
+ {
+ FieldEncoding.Varint => new TypeResolution(true, baseType),
+ FieldEncoding.Fixed => new TypeResolution(
+ true,
+ new TypeClassification(13, true, true, false, false, false, 8)),
+ FieldEncoding.Tagged => new TypeResolution(
+ true,
+ new TypeClassification(15, true, true, false, false, true, 8)),
+ _ => new TypeResolution(false, baseType),
+ };
+ }
+
+ return new TypeResolution(false, baseType);
+ }
+
+ private static TypeClassification ClassifyType(ITypeSymbol type)
+ {
+ if (ResolveDynamicAnyKind(type) == DynamicAnyKind.AnyValue)
+ {
+ return new TypeClassification(0, false, true, false, false, false, 0);
+ }
+
+ if (type.SpecialType == SpecialType.System_Boolean)
+ {
+ return new TypeClassification(1, true, true, false, false, false, 1);
+ }
+
+ if (type.SpecialType == SpecialType.System_SByte)
+ {
+ return new TypeClassification(2, true, true, false, false, false, 1);
+ }
+
+ if (type.SpecialType == SpecialType.System_Int16)
+ {
+ return new TypeClassification(3, true, true, false, false, false, 2);
+ }
+
+ if (type.SpecialType == SpecialType.System_Int32)
+ {
+ return new TypeClassification(5, true, true, false, false, true, 4);
+ }
+
+ if (type.SpecialType == SpecialType.System_Int64)
+ {
+ return new TypeClassification(7, true, true, false, false, true, 8);
+ }
+
+ if (type.SpecialType == SpecialType.System_Byte)
+ {
+ return new TypeClassification(9, true, true, false, false, false, 1);
+ }
+
+ if (type.SpecialType == SpecialType.System_UInt16)
+ {
+ return new TypeClassification(10, true, true, false, false, false, 2);
+ }
+
+ if (type.SpecialType == SpecialType.System_UInt32)
+ {
+ return new TypeClassification(12, true, true, false, false, true, 4);
+ }
+
+ if (type.SpecialType == SpecialType.System_UInt64)
+ {
+ return new TypeClassification(14, true, true, false, false, true, 8);
+ }
+
+ if (type.SpecialType == SpecialType.System_Single)
+ {
+ return new TypeClassification(19, true, true, false, false, false, 4);
+ }
+
+ if (type.SpecialType == SpecialType.System_Double)
+ {
+ return new TypeClassification(20, true, true, false, false, false, 8);
+ }
+
+ if (type.SpecialType == SpecialType.System_String)
+ {
+ return new TypeClassification(21, false, true, false, false, false, 0);
+ }
+
+ if (IsDateType(type))
+ {
+ return new TypeClassification(39, false, true, false, false, false, 0);
+ }
+
+ if (IsTimestampType(type))
+ {
+ return new TypeClassification(38, false, true, false, false, false, 0);
+ }
+
+ if (IsDurationType(type))
+ {
+ return new TypeClassification(37, false, true, false, false, false, 0);
+ }
+
+ if (type is IArrayTypeSymbol arrayType)
+ {
+ TypeClassification elem = ClassifyType(arrayType.ElementType);
+ uint typeId = elem.TypeId switch
+ {
+ 9 => 41,
+ 1 => 43,
+ 2 => 44,
+ 3 => 45,
+ 5 => 46,
+ 7 => 47,
+ 10 => 49,
+ 12 => 50,
+ 14 => 51,
+ 19 => 55,
+ 20 => 56,
+ _ => 22,
+ };
+ return new TypeClassification(typeId, false, true, true, false, false, 0);
+ }
+
+ if (TryGetListElementType(type, out _))
+ {
+ return new TypeClassification(22, false, true, true, false, false, 0);
+ }
+
+ if (TryGetSetElementType(type, out _))
+ {
+ return new TypeClassification(23, false, true, true, false, false, 0);
+ }
+
+ if (TryGetMapTypeArguments(type, out _, out _))
+ {
+ return new TypeClassification(24, false, true, false, true, false, 0);
+ }
+
+ if (IsUnionType(type))
+ {
+ return new TypeClassification(33, false, false, false, false, false, 0);
+ }
+
+ return new TypeClassification(27, false, false, false, false, false, 0);
+ }
+
+ private static DynamicAnyKind ResolveDynamicAnyKind(ITypeSymbol type)
+ {
+ if (type.SpecialType == SpecialType.System_Object)
+ {
+ return DynamicAnyKind.AnyValue;
+ }
+
+ return DynamicAnyKind.None;
+ }
+
+ private static bool IsDateType(ITypeSymbol symbol)
+ {
+ return string.Equals(symbol.ToDisplayString(), "System.DateOnly", StringComparison.Ordinal);
+ }
+
+ private static bool IsTimestampType(ITypeSymbol symbol)
+ {
+ string name = symbol.ToDisplayString();
+ return string.Equals(name, "System.DateTime", StringComparison.Ordinal) ||
+ string.Equals(name, "System.DateTimeOffset", StringComparison.Ordinal);
+ }
+
+ private static bool IsDurationType(ITypeSymbol symbol)
+ {
+ return string.Equals(symbol.ToDisplayString(), "System.TimeSpan", StringComparison.Ordinal);
+ }
+
+ private static bool IsUnionType(ITypeSymbol symbol)
+ {
+ INamedTypeSymbol? current = symbol as INamedTypeSymbol;
+ while (current is not null)
+ {
+ if (string.Equals(current.ToDisplayString(), "Apache.Fory.Union", StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ current = current.BaseType;
+ }
+
+ return false;
+ }
+
+ private static bool TryGetListElementType(ITypeSymbol type, out ITypeSymbol? elementType)
+ {
+ elementType = null;
+ if (type is IArrayTypeSymbol arrayType)
+ {
+ elementType = arrayType.ElementType;
+ return true;
+ }
+
+ if (type is not INamedTypeSymbol named)
+ {
+ return false;
+ }
+
+ string genericName = named.ConstructedFrom.ToDisplayString();
+ if (genericName is
+ "System.Collections.Generic.List" or
+ "System.Collections.Generic.IList" or
+ "System.Collections.Generic.IReadOnlyList")
+ {
+ elementType = named.TypeArguments[0];
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryGetSetElementType(ITypeSymbol type, out ITypeSymbol? elementType)
+ {
+ elementType = null;
+ if (type is not INamedTypeSymbol named)
+ {
+ return false;
+ }
+
+ string genericName = named.ConstructedFrom.ToDisplayString();
+ if (genericName is
+ "System.Collections.Generic.HashSet" or
+ "System.Collections.Generic.ISet" or
+ "System.Collections.Generic.IReadOnlySet")
+ {
+ elementType = named.TypeArguments[0];
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryGetMapTypeArguments(ITypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? valueType)
+ {
+ keyType = null;
+ valueType = null;
+ if (type is not INamedTypeSymbol named)
+ {
+ return false;
+ }
+
+ string genericName = named.ConstructedFrom.ToDisplayString();
+ if (genericName is
+ "System.Collections.Generic.Dictionary" or
+ "System.Collections.Generic.IDictionary" or
+ "System.Collections.Generic.IReadOnlyDictionary" or
+ "Apache.Fory.NullableKeyDictionary")
+ {
+ keyType = named.TypeArguments[0];
+ valueType = named.TypeArguments[1];
+ return true;
+ }
+
+ return false;
+ }
+
+ private static (bool, ITypeSymbol) UnwrapNullable(ITypeSymbol type)
+ {
+ if (type is INamedTypeSymbol named &&
+ named.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
+ {
+ return (true, named.TypeArguments[0]);
+ }
+
+ if (type.IsReferenceType && type.NullableAnnotation == NullableAnnotation.Annotated)
+ {
+ return (true, type.WithNullableAnnotation(NullableAnnotation.NotAnnotated));
+ }
+
+ return (false, type);
+ }
+
+ private static string BoolLiteral(bool value) => value ? "true" : "false";
+
+ private static string EscapeString(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"");
+
+ private static string ToSnakeCase(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return name;
+ }
+
+ StringBuilder sb = new(name.Length + 4);
+ for (int i = 0; i < name.Length; i++)
+ {
+ char c = name[i];
+ if (char.IsUpper(c))
+ {
+ if (i > 0)
+ {
+ bool prevUpper = char.IsUpper(name[i - 1]);
+ bool nextUpperOrEnd = i + 1 >= name.Length || char.IsUpper(name[i + 1]);
+ if (!prevUpper || !nextUpperOrEnd)
+ {
+ sb.Append('_');
+ }
+ }
+
+ sb.Append(char.ToLowerInvariant(c));
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ private static string Sanitize(string name)
+ {
+ StringBuilder sb = new(name.Length + 8);
+ foreach (char c in name)
+ {
+ sb.Append(char.IsLetterOrDigit(c) ? c : '_');
+ }
+
+ return sb.ToString();
+ }
+
+ private sealed class TypeResolution
+ {
+ public TypeResolution(bool supported, TypeClassification classification)
+ {
+ Supported = supported;
+ Classification = classification;
+ }
+
+ public bool Supported { get; }
+ public TypeClassification Classification { get; }
+ }
+
+ private sealed class TypeClassification
+ {
+ public TypeClassification(
+ uint typeId,
+ bool isPrimitive,
+ bool isBuiltIn,
+ bool isCollection,
+ bool isMap,
+ bool isCompressedNumeric,
+ int primitiveSize)
+ {
+ TypeId = typeId;
+ IsPrimitive = isPrimitive;
+ IsBuiltIn = isBuiltIn;
+ IsCollection = isCollection;
+ IsMap = isMap;
+ IsCompressedNumeric = isCompressedNumeric;
+ PrimitiveSize = primitiveSize;
+ }
+
+ public uint TypeId { get; }
+ public bool IsPrimitive { get; }
+ public bool IsBuiltIn { get; }
+ public bool IsCollection { get; }
+ public bool IsMap { get; }
+ public bool IsCompressedNumeric { get; }
+ public int PrimitiveSize { get; }
+ }
+
+ private sealed class TypeMetaFieldTypeModel
+ {
+ public TypeMetaFieldTypeModel(
+ string typeIdExpr,
+ bool nullable,
+ bool trackRefByContext,
+ ImmutableArray generics)
+ {
+ TypeIdExpr = typeIdExpr;
+ Nullable = nullable;
+ TrackRefByContext = trackRefByContext;
+ Generics = generics;
+ }
+
+ public string TypeIdExpr { get; }
+ public bool Nullable { get; }
+ public bool TrackRefByContext { get; }
+ public ImmutableArray Generics { get; }
+ }
+
+ private sealed class TypeModel
+ {
+ public TypeModel(
+ string typeName,
+ string serializerName,
+ DeclKind kind,
+ ImmutableArray members,
+ ImmutableArray sortedMembers)
+ {
+ TypeName = typeName;
+ SerializerName = serializerName;
+ Kind = kind;
+ Members = members;
+ SortedMembers = sortedMembers;
+ }
+
+ public string TypeName { get; }
+ public string SerializerName { get; }
+ public DeclKind Kind { get; }
+ public ImmutableArray Members { get; }
+ public ImmutableArray SortedMembers { get; }
+ }
+
+ private sealed class MemberModel
+ {
+ public MemberModel(
+ string name,
+ string fieldIdentifier,
+ int originalIndex,
+ MemberDeclKind declKind,
+ string typeName,
+ bool isNullable,
+ bool isNullableValueType,
+ TypeClassification classification,
+ int group,
+ bool isCollection,
+ DynamicAnyKind dynamicAnyKind,
+ TypeMetaFieldTypeModel typeMeta)
+ {
+ Name = name;
+ FieldIdentifier = fieldIdentifier;
+ OriginalIndex = originalIndex;
+ DeclKind = declKind;
+ TypeName = typeName;
+ IsNullable = isNullable;
+ IsNullableValueType = isNullableValueType;
+ Classification = classification;
+ Group = group;
+ IsCollection = isCollection;
+ DynamicAnyKind = dynamicAnyKind;
+ TypeMeta = typeMeta;
+ }
+
+ public string Name { get; }
+ public string FieldIdentifier { get; }
+ public int OriginalIndex { get; }
+ public MemberDeclKind DeclKind { get; }
+ public string TypeName { get; }
+ public bool IsNullable { get; }
+ public bool IsNullableValueType { get; }
+ public TypeClassification Classification { get; }
+ public int Group { get; }
+ public bool IsCollection { get; }
+ public DynamicAnyKind DynamicAnyKind { get; }
+ public TypeMetaFieldTypeModel TypeMeta { get; }
+ }
+
+ private enum MemberDeclKind
+ {
+ Field,
+ Property,
+ }
+
+ private enum DeclKind
+ {
+ Unknown,
+ Class,
+ Struct,
+ Enum,
+ }
+
+ private enum DynamicAnyKind
+ {
+ None,
+ AnyValue,
+ }
+
+ private enum FieldEncoding
+ {
+ None = -1,
+ Varint = 0,
+ Fixed = 1,
+ Tagged = 2,
+ }
+}
diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs
new file mode 100644
index 0000000000..9ca4008b09
--- /dev/null
+++ b/csharp/src/Fory/AnySerializer.cs
@@ -0,0 +1,445 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+namespace Apache.Fory;
+
+public sealed class DynamicAnyObjectSerializer : Serializer