This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 1cdcb42cf perf(csharp): add csharp benchmarks and optimize hot 
serialization paths (#3396)
1cdcb42cf is described below

commit 1cdcb42cf37d1660bf1fab56ffd1b3f72406fc9e
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Feb 24 12:08:02 2026 +0800

    perf(csharp): add csharp benchmarks and optimize hot serialization paths 
(#3396)
    
    ## Why?
    
    - Add a first-class C# benchmark suite aligned with existing benchmark
    datasets and serializers.
    - Improve C# runtime hot paths while keeping benchmark comparisons fair.
    - Fix enum behavior so undefined numeric enum values can round-trip
    instead of failing.
    
    ## What does this PR do?
    
    - Adds `benchmarks/csharp` benchmark harness (`Program.cs`, models,
    serializer adapters, `run.sh`, and report generator) for `fory`,
    `protobuf-net`, and `MessagePack`.
    - Benchmarks cover `Struct`, `Sample`, `MediaContent` and list variants
    for both serialize/deserialize.
    - Uses directly serializable benchmark models for all three serializers
    (removed protobuf conversion adapter overhead from timed path).
    - Updates benchmark report generation to keep throughput and size
    reporting separate, and adds a C++-style `Serialized Data Sizes (bytes)`
    matrix.
    - Optimizes C# runtime hot paths: reusable `ByteWriter`/`ByteReader`,
    span-based string encode/decode path, and cached allowed wire-type sets.
    - Improves generated/runtime schema handling: `CheckStructVersion` is
    wired through contexts/generated serializers with cached no-trackRef
    schema hash.
    - Optimizes collection and enum hot paths: sealed collection element
    types avoid redundant type-info writes; enum serializer uses cached maps
    and preserves unknown numeric enum values on read.
    - Simplifies config surface by removing skip-root/reflection-fallback
    flags and setting default `MaxDepth` to `20`.
    - Adds/updates tests for schema version behavior and undefined enum
    round-trip.
    
    ## Related issues
    
    #1017 #3387
    #3397
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [x] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    - Command:
      - `cd benchmarks/csharp && ./run.sh --duration 3 --warmup 1`
    - Selected results (ops/sec):
    
    | Datatype         | Operation   | fory      | protobuf  | msgpack   |
    | ---------------- | ----------- | ---------:| ---------:| ---------:|
    | MediaContent     | Serialize   | 2,787,853 | 2,241,239 | 2,111,882 |
    | MediaContent     | Deserialize | 2,601,783 | 1,343,196 | 1,252,873 |
    | MediaContentList | Serialize   |   588,552 |   443,250 |   505,927 |
    | MediaContentList | Deserialize |   570,945 |   289,274 |   279,718 |
    
    - Aggregate throughput (mean ops/sec across all benchmark cases):
      - `fory`: `5,278,631`
      - `protobuf`: `2,020,540`
      - `msgpack`: `1,982,782`
---
 benchmarks/csharp/.gitignore                     |   4 +
 benchmarks/csharp/BenchmarkModels.cs             | 458 +++++++++++++++++++++++
 benchmarks/csharp/BenchmarkSerializers.cs        | 112 ++++++
 benchmarks/csharp/Fory.CSharpBenchmark.csproj    |  19 +
 benchmarks/csharp/Program.cs                     | 377 +++++++++++++++++++
 benchmarks/csharp/README.md                      |  80 ++++
 benchmarks/csharp/benchmark_report.py            | 189 ++++++++++
 benchmarks/csharp/run.sh                         | 112 ++++++
 csharp/src/Fory.Generator/ForyObjectGenerator.cs |  27 +-
 csharp/src/Fory/ByteBuffer.cs                    | 173 +++++++--
 csharp/src/Fory/CollectionSerializers.cs         |  12 +-
 csharp/src/Fory/Config.cs                        |  13 +-
 csharp/src/Fory/Context.cs                       |   8 +
 csharp/src/Fory/EnumSerializer.cs                |  38 +-
 csharp/src/Fory/Fory.cs                          |  46 ++-
 csharp/src/Fory/StringSerializer.cs              |  49 ++-
 csharp/src/Fory/TypeResolver.cs                  |  18 +-
 csharp/tests/Fory.Tests/ForyRuntimeTests.cs      |  18 +-
 18 files changed, 1653 insertions(+), 100 deletions(-)

diff --git a/benchmarks/csharp/.gitignore b/benchmarks/csharp/.gitignore
new file mode 100644
index 000000000..0e7fa6ce0
--- /dev/null
+++ b/benchmarks/csharp/.gitignore
@@ -0,0 +1,4 @@
+bin/
+obj/
+build/
+report/
diff --git a/benchmarks/csharp/BenchmarkModels.cs 
b/benchmarks/csharp/BenchmarkModels.cs
new file mode 100644
index 000000000..8e4a8a5e2
--- /dev/null
+++ b/benchmarks/csharp/BenchmarkModels.cs
@@ -0,0 +1,458 @@
+// 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 Apache.Fory;
+using MessagePack;
+using ProtoBuf;
+
+namespace Apache.Fory.Benchmarks.CSharp;
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class NumericStruct
+{
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(1)]
+    public int F1 { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(2)]
+    public int F2 { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(3)]
+    public int F3 { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(4)]
+    public int F4 { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(5)]
+    public int F5 { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(6)]
+    public int F6 { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(7)]
+    public int F7 { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(8)]
+    public int F8 { get; set; }
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class StructList
+{
+    [ProtoMember(1)]
+    public List<NumericStruct> Values { get; set; } = [];
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class Sample
+{
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(1)]
+    public int IntValue { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(2)]
+    public long LongValue { get; set; }
+
+    [ProtoMember(3)]
+    public float FloatValue { get; set; }
+
+    [ProtoMember(4)]
+    public double DoubleValue { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(5)]
+    public int ShortValue { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(6)]
+    public int CharValue { get; set; }
+
+    [ProtoMember(7)]
+    public bool BooleanValue { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(8)]
+    public int IntValueBoxed { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(9)]
+    public long LongValueBoxed { get; set; }
+
+    [ProtoMember(10)]
+    public float FloatValueBoxed { get; set; }
+
+    [ProtoMember(11)]
+    public double DoubleValueBoxed { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(12)]
+    public int ShortValueBoxed { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(13)]
+    public int CharValueBoxed { get; set; }
+
+    [ProtoMember(14)]
+    public bool BooleanValueBoxed { get; set; }
+
+    [ProtoMember(15)]
+    public int[] IntArray { get; set; } = [];
+
+    [ProtoMember(16)]
+    public long[] LongArray { get; set; } = [];
+
+    [ProtoMember(17)]
+    public float[] FloatArray { get; set; } = [];
+
+    [ProtoMember(18)]
+    public double[] DoubleArray { get; set; } = [];
+
+    [ProtoMember(19)]
+    public int[] ShortArray { get; set; } = [];
+
+    [ProtoMember(20)]
+    public int[] CharArray { get; set; } = [];
+
+    [ProtoMember(21)]
+    public bool[] BooleanArray { get; set; } = [];
+
+    [ProtoMember(22)]
+    public string String { get; set; } = string.Empty;
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class SampleList
+{
+    [ProtoMember(1)]
+    public List<Sample> Values { get; set; } = [];
+}
+
+[ForyObject]
+[ProtoContract]
+public enum Player
+{
+    [ProtoEnum]
+    Java,
+    [ProtoEnum]
+    Flash,
+}
+
+[ForyObject]
+[ProtoContract]
+public enum MediaSize
+{
+    [ProtoEnum]
+    Small,
+    [ProtoEnum]
+    Large,
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class Media
+{
+    [ProtoMember(1)]
+    public string Uri { get; set; } = string.Empty;
+
+    [ProtoMember(2)]
+    public string Title { get; set; } = string.Empty;
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(3)]
+    public int Width { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(4)]
+    public int Height { get; set; }
+
+    [ProtoMember(5)]
+    public string Format { get; set; } = string.Empty;
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(6)]
+    public long Duration { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(7)]
+    public long Size { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(8)]
+    public int Bitrate { get; set; }
+
+    [ProtoMember(9)]
+    public bool HasBitrate { get; set; }
+
+    [ProtoMember(10)]
+    public List<string> Persons { get; set; } = [];
+
+    [ProtoMember(11)]
+    public Player Player { get; set; }
+
+    [ProtoMember(12)]
+    public string Copyright { get; set; } = string.Empty;
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class Image
+{
+    [ProtoMember(1)]
+    public string Uri { get; set; } = string.Empty;
+
+    [ProtoMember(2)]
+    public string Title { get; set; } = string.Empty;
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(3)]
+    public int Width { get; set; }
+
+    [Field(Encoding = FieldEncoding.Fixed)]
+    [ProtoMember(4)]
+    public int Height { get; set; }
+
+    [ProtoMember(5)]
+    public MediaSize Size { get; set; }
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class MediaContent
+{
+    [ProtoMember(1)]
+    public Media Media { get; set; } = new();
+
+    [ProtoMember(2)]
+    public List<Image> Images { get; set; } = [];
+}
+
+[ForyObject]
+[MessagePackObject(keyAsPropertyName: true)]
+[ProtoContract]
+public sealed class MediaContentList
+{
+    [ProtoMember(1)]
+    public List<MediaContent> Values { get; set; } = [];
+}
+
+public static class BenchmarkDataFactory
+{
+    private const int ListSize = 5;
+
+    public static NumericStruct CreateNumericStruct()
+    {
+        return new NumericStruct
+        {
+            F1 = -12345,
+            F2 = 987654321,
+            F3 = -31415,
+            F4 = 27182818,
+            F5 = -32000,
+            F6 = 1000000,
+            F7 = -999999999,
+            F8 = 42,
+        };
+    }
+
+    public static Sample CreateSample()
+    {
+        return new Sample
+        {
+            IntValue = 123,
+            LongValue = 1230000,
+            FloatValue = 12.345f,
+            DoubleValue = 1.234567,
+            ShortValue = 12345,
+            CharValue = '!',
+            BooleanValue = true,
+            IntValueBoxed = 321,
+            LongValueBoxed = 3210000,
+            FloatValueBoxed = 54.321f,
+            DoubleValueBoxed = 7.654321,
+            ShortValueBoxed = 32100,
+            CharValueBoxed = '$',
+            BooleanValueBoxed = false,
+            IntArray = [-1234, -123, -12, -1, 0, 1, 12, 123, 1234],
+            LongArray = [-123400, -12300, -1200, -100, 0, 100, 1200, 12300, 
123400],
+            FloatArray = [-12.34f, -12.3f, -12.0f, -1.0f, 0.0f, 1.0f, 12.0f, 
12.3f, 12.34f],
+            DoubleArray = [-1.234, -1.23, -12.0, -1.0, 0.0, 1.0, 12.0, 1.23, 
1.234],
+            ShortArray = [-1234, -123, -12, -1, 0, 1, 12, 123, 1234],
+            CharArray = ['a', 's', 'd', 'f', 'A', 'S', 'D', 'F'],
+            BooleanArray = [true, false, false, true],
+            String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
+        };
+    }
+
+    public static MediaContent CreateMediaContent()
+    {
+        return new MediaContent
+        {
+            Media = new Media
+            {
+                Uri = "https://apache.org/fory/video/123";,
+                Title = "Apache Fory: Cross-language serialization benchmark",
+                Width = 1920,
+                Height = 1080,
+                Format = "video/mp4",
+                Duration = 145_000,
+                Size = 58_000_000,
+                Bitrate = 3_200,
+                HasBitrate = true,
+                Persons = ["alice", "bob", "charlie", "david"],
+                Player = Player.Java,
+                Copyright = "Apache Software Foundation",
+            },
+            Images =
+            [
+                new Image
+                {
+                    Uri = "https://apache.org/fory/image/1";,
+                    Title = "cover",
+                    Width = 1920,
+                    Height = 1080,
+                    Size = MediaSize.Large,
+                },
+                new Image
+                {
+                    Uri = "https://apache.org/fory/image/2";,
+                    Title = "thumbnail",
+                    Width = 320,
+                    Height = 180,
+                    Size = MediaSize.Small,
+                },
+            ],
+        };
+    }
+
+    public static StructList CreateStructList()
+    {
+        NumericStruct value = CreateNumericStruct();
+        StructList list = new();
+        for (int i = 0; i < ListSize; i++)
+        {
+            list.Values.Add(
+                new NumericStruct
+                {
+                    F1 = value.F1 + i,
+                    F2 = value.F2 - i,
+                    F3 = value.F3 + i,
+                    F4 = value.F4 - i,
+                    F5 = value.F5 + i,
+                    F6 = value.F6 - i,
+                    F7 = value.F7 + i,
+                    F8 = value.F8 + i,
+                });
+        }
+
+        return list;
+    }
+
+    public static SampleList CreateSampleList()
+    {
+        Sample sample = CreateSample();
+        SampleList list = new();
+        for (int i = 0; i < ListSize; i++)
+        {
+            list.Values.Add(new Sample
+            {
+                IntValue = sample.IntValue + i,
+                LongValue = sample.LongValue + i,
+                FloatValue = sample.FloatValue,
+                DoubleValue = sample.DoubleValue,
+                ShortValue = sample.ShortValue,
+                CharValue = sample.CharValue,
+                BooleanValue = sample.BooleanValue,
+                IntValueBoxed = sample.IntValueBoxed,
+                LongValueBoxed = sample.LongValueBoxed,
+                FloatValueBoxed = sample.FloatValueBoxed,
+                DoubleValueBoxed = sample.DoubleValueBoxed,
+                ShortValueBoxed = sample.ShortValueBoxed,
+                CharValueBoxed = sample.CharValueBoxed,
+                BooleanValueBoxed = sample.BooleanValueBoxed,
+                IntArray = sample.IntArray,
+                LongArray = sample.LongArray,
+                FloatArray = sample.FloatArray,
+                DoubleArray = sample.DoubleArray,
+                ShortArray = sample.ShortArray,
+                CharArray = sample.CharArray,
+                BooleanArray = sample.BooleanArray,
+                String = sample.String,
+            });
+        }
+
+        return list;
+    }
+
+    public static MediaContentList CreateMediaContentList()
+    {
+        MediaContent content = CreateMediaContent();
+        MediaContentList list = new();
+        for (int i = 0; i < ListSize; i++)
+        {
+            list.Values.Add(new MediaContent
+            {
+                Media = new Media
+                {
+                    Uri = content.Media.Uri,
+                    Title = content.Media.Title,
+                    Width = content.Media.Width,
+                    Height = content.Media.Height,
+                    Format = content.Media.Format,
+                    Duration = content.Media.Duration,
+                    Size = content.Media.Size,
+                    Bitrate = content.Media.Bitrate,
+                    HasBitrate = content.Media.HasBitrate,
+                    Persons = [.. content.Media.Persons],
+                    Player = content.Media.Player,
+                    Copyright = content.Media.Copyright,
+                },
+                Images =
+                [
+                    .. content.Images.Select(image => new Image
+                    {
+                        Uri = image.Uri,
+                        Title = image.Title,
+                        Width = image.Width,
+                        Height = image.Height,
+                        Size = image.Size,
+                    }),
+                ],
+            });
+        }
+
+        return list;
+    }
+}
diff --git a/benchmarks/csharp/BenchmarkSerializers.cs 
b/benchmarks/csharp/BenchmarkSerializers.cs
new file mode 100644
index 000000000..6f6c70393
--- /dev/null
+++ b/benchmarks/csharp/BenchmarkSerializers.cs
@@ -0,0 +1,112 @@
+// 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 Apache.Fory;
+using MessagePack;
+using MessagePack.Resolvers;
+using ProtoBuf;
+using ForyRuntime = Apache.Fory.Fory;
+using ProtobufNetSerializer = ProtoBuf.Serializer;
+
+namespace Apache.Fory.Benchmarks.CSharp;
+
+internal interface IBenchmarkSerializer<T>
+{
+    string Name { get; }
+
+    byte[] Serialize(T value);
+
+    T Deserialize(byte[] payload);
+}
+
+internal sealed class ForySerializer<T> : IBenchmarkSerializer<T>
+{
+    private readonly ForyRuntime _fory = ForyRuntime.Builder().Build();
+
+    public ForySerializer()
+    {
+        BenchmarkTypeRegistry.RegisterAll(_fory);
+    }
+
+    public string Name => "fory";
+
+    public byte[] Serialize(T value)
+    {
+        return _fory.Serialize(value);
+    }
+
+    public T Deserialize(byte[] payload)
+    {
+        return _fory.Deserialize<T>(payload);
+    }
+}
+
+internal static class BenchmarkTypeRegistry
+{
+    public static void RegisterAll(ForyRuntime fory)
+    {
+        fory.Register<NumericStruct>(1000);
+        fory.Register<StructList>(1001);
+        fory.Register<Sample>(1002);
+        fory.Register<SampleList>(1003);
+        fory.Register<Media>(1004);
+        fory.Register<Image>(1005);
+        fory.Register<MediaContent>(1006);
+        fory.Register<MediaContentList>(1007);
+        fory.Register<Player>(1008);
+        fory.Register<MediaSize>(1009);
+    }
+}
+
+internal sealed class ProtobufSerializer<T> : IBenchmarkSerializer<T>
+{
+    private readonly MemoryStream _writeStream = new(256);
+
+    public string Name => "protobuf";
+
+    public byte[] Serialize(T value)
+    {
+        _writeStream.Position = 0;
+        _writeStream.SetLength(0);
+        ProtobufNetSerializer.Serialize(_writeStream, value);
+        return _writeStream.ToArray();
+    }
+
+    public T Deserialize(byte[] payload)
+    {
+        using MemoryStream stream = new(payload, writable: false);
+        return ProtobufNetSerializer.Deserialize<T>(stream);
+    }
+}
+
+internal sealed class MessagePackRuntimeSerializer<T> : IBenchmarkSerializer<T>
+{
+    private readonly MessagePackSerializerOptions _options = 
TypelessContractlessStandardResolver.Options;
+
+    public string Name => "msgpack";
+
+    public byte[] Serialize(T value)
+    {
+        return MessagePackSerializer.Typeless.Serialize(value, _options);
+    }
+
+    public T Deserialize(byte[] payload)
+    {
+        object? value = MessagePackSerializer.Typeless.Deserialize(payload, 
_options);
+        return (T)value!;
+    }
+}
diff --git a/benchmarks/csharp/Fory.CSharpBenchmark.csproj 
b/benchmarks/csharp/Fory.CSharpBenchmark.csproj
new file mode 100644
index 000000000..7eebcba18
--- /dev/null
+++ b/benchmarks/csharp/Fory.CSharpBenchmark.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <LangVersion>12.0</LangVersion>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="MessagePack" Version="2.5.172" />
+    <PackageReference Include="protobuf-net" Version="3.2.56" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../../csharp/src/Fory/Fory.csproj" />
+    <ProjectReference 
Include="../../csharp/src/Fory.Generator/Fory.Generator.csproj" 
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+  </ItemGroup>
+</Project>
diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs
new file mode 100644
index 000000000..3a3162f50
--- /dev/null
+++ b/benchmarks/csharp/Program.cs
@@ -0,0 +1,377 @@
+// 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.Diagnostics;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace Apache.Fory.Benchmarks.CSharp;
+
+internal sealed record BenchmarkCase(
+    string Serializer,
+    string DataType,
+    string Operation,
+    int SerializedSize,
+    Action Action);
+
+internal sealed record BenchmarkResult(
+    string Serializer,
+    string DataType,
+    string Operation,
+    int SerializedSize,
+    double OperationsPerSecond,
+    double AverageNanoseconds,
+    long Iterations,
+    double ElapsedSeconds);
+
+internal sealed record BenchmarkOutput(
+    string GeneratedAtUtc,
+    string RuntimeVersion,
+    string OsDescription,
+    string OsArchitecture,
+    string ProcessArchitecture,
+    int ProcessorCount,
+    double WarmupSeconds,
+    double DurationSeconds,
+    List<BenchmarkResult> Results);
+
+internal sealed class BenchmarkOptions
+{
+    public HashSet<string> DataFilter { get; init; } = [];
+
+    public HashSet<string> SerializerFilter { get; init; } = [];
+
+    public double WarmupSeconds { get; init; } = 1.0;
+
+    public double DurationSeconds { get; init; } = 3.0;
+
+    public string OutputPath { get; init; } = "benchmark_results.json";
+
+    public bool ShowHelp { get; init; }
+
+    public static BenchmarkOptions Parse(string[] args)
+    {
+        HashSet<string> dataFilter = new(StringComparer.OrdinalIgnoreCase);
+        HashSet<string> serializerFilter = 
new(StringComparer.OrdinalIgnoreCase);
+        double warmupSeconds = 1.0;
+        double durationSeconds = 3.0;
+        string outputPath = "benchmark_results.json";
+        bool showHelp = false;
+
+        for (int i = 0; i < args.Length; i++)
+        {
+            switch (args[i])
+            {
+                case "--help":
+                case "-h":
+                    showHelp = true;
+                    break;
+                case "--data":
+                    RequireValue(args, i);
+                    dataFilter.Add(args[++i]);
+                    break;
+                case "--serializer":
+                    RequireValue(args, i);
+                    serializerFilter.Add(args[++i]);
+                    break;
+                case "--warmup":
+                    RequireValue(args, i);
+                    warmupSeconds = ParsePositiveDouble(args[++i], "warmup");
+                    break;
+                case "--duration":
+                    RequireValue(args, i);
+                    durationSeconds = ParsePositiveDouble(args[++i], 
"duration");
+                    break;
+                case "--output":
+                    RequireValue(args, i);
+                    outputPath = args[++i];
+                    break;
+                default:
+                    throw new ArgumentException($"unknown option: {args[i]}");
+            }
+        }
+
+        return new BenchmarkOptions
+        {
+            DataFilter = dataFilter,
+            SerializerFilter = serializerFilter,
+            WarmupSeconds = warmupSeconds,
+            DurationSeconds = durationSeconds,
+            OutputPath = outputPath,
+            ShowHelp = showHelp,
+        };
+    }
+
+    public bool IsDataEnabled(string dataType)
+    {
+        return DataFilter.Count == 0 || DataFilter.Contains(dataType);
+    }
+
+    public bool IsSerializerEnabled(string serializer)
+    {
+        return SerializerFilter.Count == 0 || 
SerializerFilter.Contains(serializer);
+    }
+
+    private static void RequireValue(string[] args, int index)
+    {
+        if (index + 1 >= args.Length)
+        {
+            throw new ArgumentException($"missing value for option 
{args[index]}");
+        }
+    }
+
+    private static double ParsePositiveDouble(string text, string name)
+    {
+        if (!double.TryParse(text, NumberStyles.Float, 
CultureInfo.InvariantCulture, out double value) || value <= 0)
+        {
+            throw new ArgumentException($"{name} must be a positive number, 
got '{text}'");
+        }
+
+        return value;
+    }
+}
+
+internal static class Program
+{
+    private static object? _sink;
+
+    private static readonly JsonSerializerOptions JsonOptions = new()
+    {
+        WriteIndented = true,
+    };
+
+    private static int Main(string[] args)
+    {
+        BenchmarkOptions options;
+        try
+        {
+            options = BenchmarkOptions.Parse(args);
+        }
+        catch (Exception ex)
+        {
+            Console.Error.WriteLine($"error: {ex.Message}");
+            PrintUsage();
+            return 1;
+        }
+
+        if (options.ShowHelp)
+        {
+            PrintUsage();
+            return 0;
+        }
+
+        List<BenchmarkCase> cases;
+        try
+        {
+            cases = BuildBenchmarkCases(options);
+        }
+        catch (Exception ex)
+        {
+            Console.Error.WriteLine($"failed to build benchmarks: {ex}");
+            return 1;
+        }
+
+        if (cases.Count == 0)
+        {
+            Console.Error.WriteLine("no benchmark cases selected");
+            return 1;
+        }
+
+        Console.WriteLine("=== Fory C# Benchmark ===");
+        Console.WriteLine($"Cases: {cases.Count}");
+        Console.WriteLine($"Warmup: {options.WarmupSeconds.ToString("F2", 
CultureInfo.InvariantCulture)}s");
+        Console.WriteLine($"Duration: {options.DurationSeconds.ToString("F2", 
CultureInfo.InvariantCulture)}s");
+        Console.WriteLine();
+
+        List<BenchmarkResult> results = new(cases.Count);
+        foreach (BenchmarkCase benchmarkCase in cases)
+        {
+            Console.WriteLine($"Running 
{benchmarkCase.Serializer}/{benchmarkCase.DataType}/{benchmarkCase.Operation}...");
+            BenchmarkResult result = RunBenchmarkCase(benchmarkCase, 
options.WarmupSeconds, options.DurationSeconds);
+            results.Add(result);
+        }
+
+        BenchmarkOutput output = new(
+            GeneratedAtUtc: DateTime.UtcNow.ToString("O", 
CultureInfo.InvariantCulture),
+            RuntimeVersion: Environment.Version.ToString(),
+            OsDescription: RuntimeInformation.OSDescription,
+            OsArchitecture: RuntimeInformation.OSArchitecture.ToString(),
+            ProcessArchitecture: 
RuntimeInformation.ProcessArchitecture.ToString(),
+            ProcessorCount: Environment.ProcessorCount,
+            WarmupSeconds: options.WarmupSeconds,
+            DurationSeconds: options.DurationSeconds,
+            Results: results);
+
+        string outputPath = Path.GetFullPath(options.OutputPath);
+        string? parent = Path.GetDirectoryName(outputPath);
+        if (!string.IsNullOrEmpty(parent))
+        {
+            Directory.CreateDirectory(parent);
+        }
+
+        File.WriteAllText(outputPath, JsonSerializer.Serialize(output, 
JsonOptions));
+
+        PrintSummary(results);
+        Console.WriteLine();
+        Console.WriteLine($"Results written to {outputPath}");
+        return 0;
+    }
+
+    private static void PrintUsage()
+    {
+        Console.WriteLine("Usage: dotnet run -c Release -- [OPTIONS]");
+        Console.WriteLine();
+        Console.WriteLine("Options:");
+        Console.WriteLine("  --data 
<struct|sample|mediacontent|structlist|samplelist|mediacontentlist>");
+        Console.WriteLine("  --serializer <fory|protobuf|msgpack>");
+        Console.WriteLine("  --warmup <seconds>");
+        Console.WriteLine("  --duration <seconds>");
+        Console.WriteLine("  --output <path>");
+        Console.WriteLine("  --help");
+    }
+
+    private static BenchmarkResult RunBenchmarkCase(BenchmarkCase 
benchmarkCase, double warmupSeconds, double durationSeconds)
+    {
+        RunForDuration(benchmarkCase.Action, warmupSeconds);
+
+        GC.Collect();
+        GC.WaitForPendingFinalizers();
+        GC.Collect();
+
+        const int batchSize = 256;
+        long iterations = 0;
+        Stopwatch stopwatch = Stopwatch.StartNew();
+        while (stopwatch.Elapsed.TotalSeconds < durationSeconds)
+        {
+            for (int i = 0; i < batchSize; i++)
+            {
+                benchmarkCase.Action();
+            }
+
+            iterations += batchSize;
+        }
+
+        double elapsed = stopwatch.Elapsed.TotalSeconds;
+        double throughput = iterations / elapsed;
+        double nsPerOp = (elapsed * 1_000_000_000.0) / iterations;
+
+        return new BenchmarkResult(
+            benchmarkCase.Serializer,
+            benchmarkCase.DataType,
+            benchmarkCase.Operation,
+            benchmarkCase.SerializedSize,
+            throughput,
+            nsPerOp,
+            iterations,
+            elapsed);
+    }
+
+    private static void RunForDuration(Action action, double durationSeconds)
+    {
+        Stopwatch stopwatch = Stopwatch.StartNew();
+        while (stopwatch.Elapsed.TotalSeconds < durationSeconds)
+        {
+            action();
+        }
+    }
+
+    private static List<BenchmarkCase> BuildBenchmarkCases(BenchmarkOptions 
options)
+    {
+        List<BenchmarkCase> cases = [];
+
+        AddCases("struct", BenchmarkDataFactory.CreateNumericStruct(), 
options, cases);
+        AddCases("sample", BenchmarkDataFactory.CreateSample(), options, 
cases);
+        AddCases("mediacontent", BenchmarkDataFactory.CreateMediaContent(), 
options, cases);
+        AddCases("structlist", BenchmarkDataFactory.CreateStructList(), 
options, cases);
+        AddCases("samplelist", BenchmarkDataFactory.CreateSampleList(), 
options, cases);
+        AddCases("mediacontentlist", 
BenchmarkDataFactory.CreateMediaContentList(), options, cases);
+
+        return cases;
+    }
+
+    private static void AddCases<T>(string dataType, T value, BenchmarkOptions 
options, List<BenchmarkCase> cases)
+    {
+        if (!options.IsDataEnabled(dataType))
+        {
+            return;
+        }
+
+        List<IBenchmarkSerializer<T>> serializers = [];
+        if (options.IsSerializerEnabled("fory"))
+        {
+            serializers.Add(new ForySerializer<T>());
+        }
+
+        if (options.IsSerializerEnabled("protobuf"))
+        {
+            serializers.Add(new ProtobufSerializer<T>());
+        }
+
+        if (options.IsSerializerEnabled("msgpack"))
+        {
+            serializers.Add(new MessagePackRuntimeSerializer<T>());
+        }
+
+        foreach (IBenchmarkSerializer<T> serializer in serializers)
+        {
+            byte[] payload = serializer.Serialize(value);
+            _sink = serializer.Deserialize(payload);
+
+            cases.Add(new BenchmarkCase(
+                serializer.Name,
+                dataType,
+                "serialize",
+                payload.Length,
+                () =>
+                {
+                    _sink = serializer.Serialize(value);
+                }));
+
+            cases.Add(new BenchmarkCase(
+                serializer.Name,
+                dataType,
+                "deserialize",
+                payload.Length,
+                () =>
+                {
+                    _sink = serializer.Deserialize(payload);
+                }));
+        }
+    }
+
+    private static void PrintSummary(List<BenchmarkResult> results)
+    {
+        Console.WriteLine();
+        Console.WriteLine("=== Summary (ops/s) ===");
+
+        IEnumerable<IGrouping<string, BenchmarkResult>> groups = results
+            .OrderBy(r => r.DataType, StringComparer.Ordinal)
+            .ThenBy(r => r.Operation, StringComparer.Ordinal)
+            .GroupBy(r => $"{r.DataType}/{r.Operation}");
+
+        foreach (IGrouping<string, BenchmarkResult> group in groups)
+        {
+            Console.WriteLine(group.Key);
+            foreach (BenchmarkResult result in group.OrderByDescending(r => 
r.OperationsPerSecond))
+            {
+                Console.WriteLine(
+                    $"  {result.Serializer,-8} 
{result.OperationsPerSecond,14:N0} ops/s  {result.AverageNanoseconds,10:N1} 
ns/op  size={result.SerializedSize}");
+            }
+        }
+    }
+}
diff --git a/benchmarks/csharp/README.md b/benchmarks/csharp/README.md
new file mode 100644
index 000000000..c57e8c35c
--- /dev/null
+++ b/benchmarks/csharp/README.md
@@ -0,0 +1,80 @@
+# Fory C# Benchmark
+
+This benchmark compares Apache Fory C#, protobuf-net, and MessagePack-CSharp.
+
+Serializer setup used in this benchmark:
+
+- `fory`: `Fory.Builder().Build()`
+- `protobuf`: `protobuf-net` runtime serializer
+- `msgpack`: `MessagePackSerializer.Typeless` with 
`TypelessContractlessStandardResolver`
+
+## Prerequisites
+
+- .NET SDK 8.0+
+- Python 3.8+
+
+## Quick Start
+
+```bash
+cd benchmarks/csharp
+./run.sh
+```
+
+This runs all benchmark cases and generates:
+
+- `build/benchmark_results.json`
+- `report/REPORT.md`
+
+## Run Options
+
+```bash
+./run.sh --help
+
+Options:
+  --data <struct|sample|mediacontent|structlist|samplelist|mediacontentlist>
+  --serializer <fory|protobuf|msgpack>
+  --duration <seconds>
+  --warmup <seconds>
+```
+
+Examples:
+
+```bash
+# Run only struct benchmarks
+./run.sh --data struct
+
+# Run only Fory benchmarks
+./run.sh --serializer fory
+
+# Use longer runs for stable numbers
+./run.sh --duration 10 --warmup 2
+```
+
+## Benchmark Cases
+
+- `struct`: 8-field integer object
+- `sample`: mixed primitive fields and arrays
+- `mediacontent`: nested object with list fields
+- `structlist`: list of struct payloads
+- `samplelist`: list of sample payloads
+- `mediacontentlist`: list of media content payloads
+
+Each case benchmarks:
+
+- serialize throughput
+- deserialize throughput
+
+## Results
+
+Latest run (`Darwin arm64`, `.NET 8.0.24`, `--duration 2 --warmup 0.5`):
+
+| Serializer | Mean ops/sec across all cases |
+| ---------- | ----------------------------: |
+| fory       |                     2,032,057 |
+| protobuf   |                     1,940,328 |
+| msgpack    |                     1,901,489 |
+
+Per-case winners vary by payload and operation. The full breakdown is 
generated at:
+
+- `benchmarks/csharp/build/benchmark_results.json`
+- `benchmarks/csharp/report/REPORT.md`
diff --git a/benchmarks/csharp/benchmark_report.py 
b/benchmarks/csharp/benchmark_report.py
new file mode 100755
index 000000000..8a06c1656
--- /dev/null
+++ b/benchmarks/csharp/benchmark_report.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+# 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.
+
+import argparse
+import json
+import os
+from collections import defaultdict
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description="Generate markdown report from C# benchmark JSON output"
+    )
+    parser.add_argument(
+        "--json-file",
+        default="benchmark_results.json",
+        help="Benchmark JSON output file",
+    )
+    parser.add_argument("--output-dir", default="report", help="Output 
directory")
+    return parser.parse_args()
+
+
+def load_results(path: str) -> dict:
+    with open(path, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def format_ops(value: float) -> str:
+    return f"{value:,.0f}"
+
+
+def format_ns(value: float) -> str:
+    return f"{value:,.1f}"
+
+
+def format_size(value: float) -> str:
+    return str(int(round(value)))
+
+
+def format_datatype_name(value: str) -> str:
+    mapping = {
+        "struct": "Struct",
+        "sample": "Sample",
+        "mediacontent": "MediaContent",
+        "structlist": "StructList",
+        "samplelist": "SampleList",
+        "mediacontentlist": "MediaContentList",
+    }
+    return mapping.get(value, value)
+
+
+def build_report(data: dict) -> str:
+    lines: list[str] = []
+    lines.append("# Fory C# Benchmark Report")
+    lines.append("")
+    lines.append("## Environment")
+    lines.append("")
+    lines.append(f"- Generated at (UTC): `{data['GeneratedAtUtc']}`")
+    lines.append(f"- Runtime version: `{data['RuntimeVersion']}`")
+    lines.append(f"- OS: `{data['OsDescription']}`")
+    lines.append(f"- OS architecture: `{data['OsArchitecture']}`")
+    lines.append(f"- Process architecture: `{data['ProcessArchitecture']}`")
+    lines.append(f"- CPU logical cores: `{data['ProcessorCount']}`")
+    lines.append(f"- Warmup seconds: `{data['WarmupSeconds']}`")
+    lines.append(f"- Duration seconds: `{data['DurationSeconds']}`")
+    lines.append("")
+
+    grouped: dict[tuple[str, str], list[dict]] = defaultdict(list)
+    for row in data["Results"]:
+        grouped[(row["DataType"], row["Operation"])].append(row)
+
+    lines.append("## Throughput Results")
+    lines.append("")
+
+    for key in sorted(grouped.keys()):
+        data_type, operation = key
+        rows = sorted(
+            grouped[key], key=lambda item: item["OperationsPerSecond"], 
reverse=True
+        )
+        best = rows[0]
+
+        lines.append(f"### `{data_type}` / `{operation}`")
+        lines.append("")
+        lines.append("| Serializer | Ops/sec | ns/op | Relative to best |")
+        lines.append("| ---------- | ------: | ----: | ---------------: |")
+
+        for row in rows:
+            relative = best["OperationsPerSecond"] / row["OperationsPerSecond"]
+            lines.append(
+                "| "
+                f"{row['Serializer']}"
+                " | "
+                f"{format_ops(row['OperationsPerSecond'])}"
+                " | "
+                f"{format_ns(row['AverageNanoseconds'])}"
+                " | "
+                f"{relative:.2f}x"
+                " |"
+            )
+
+        lines.append("")
+
+    size_totals: dict[tuple[str, str], list[int]] = defaultdict(list)
+    for row in data["Results"]:
+        size_totals[(row["DataType"], 
row["Serializer"])].append(row["SerializedSize"])
+
+    lines.append("## Size Comparison")
+    lines.append("")
+    lines.append("### Serialized Data Sizes (bytes)")
+    lines.append("")
+    lines.append("| Datatype | fory | protobuf | msgpack |")
+    lines.append("| -------- | ---- | -------- | ------- |")
+
+    size_by_data_type: dict[str, dict[str, float]] = defaultdict(dict)
+    for (data_type, serializer), values in size_totals.items():
+        size_by_data_type[data_type][serializer] = sum(values) / len(values)
+
+    preferred_order = [
+        "struct",
+        "sample",
+        "mediacontent",
+        "structlist",
+        "samplelist",
+        "mediacontentlist",
+    ]
+    remaining = sorted(
+        key for key in size_by_data_type.keys() if key not in preferred_order
+    )
+    data_type_order = [
+        key for key in preferred_order if key in size_by_data_type
+    ] + remaining
+    serializers = ["fory", "protobuf", "msgpack"]
+    for data_type in data_type_order:
+        cells = [format_datatype_name(data_type)]
+        for serializer in serializers:
+            size = size_by_data_type[data_type].get(serializer)
+            cells.append("-" if size is None else format_size(size))
+        lines.append(f"| {cells[0]} | {cells[1]} | {cells[2]} | {cells[3]} |")
+
+    lines.append("")
+
+    serializer_totals: dict[str, list[float]] = defaultdict(list)
+    for row in data["Results"]:
+        serializer_totals[row["Serializer"]].append(row["OperationsPerSecond"])
+
+    lines.append("## Aggregate Throughput")
+    lines.append("")
+    lines.append("| Serializer | Mean ops/sec across all cases |")
+    lines.append("| ---------- | ----------------------------: |")
+    for serializer in sorted(serializer_totals.keys()):
+        values = serializer_totals[serializer]
+        mean_value = sum(values) / len(values)
+        lines.append(f"| {serializer} | {format_ops(mean_value)} |")
+
+    lines.append("")
+    return "\n".join(lines)
+
+
+def main() -> None:
+    args = parse_args()
+    result = load_results(args.json_file)
+
+    os.makedirs(args.output_dir, exist_ok=True)
+    report_path = os.path.join(args.output_dir, "REPORT.md")
+
+    report = build_report(result)
+    with open(report_path, "w", encoding="utf-8") as f:
+        f.write(report)
+
+    print(f"Report written to {report_path}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/benchmarks/csharp/run.sh b/benchmarks/csharp/run.sh
new file mode 100755
index 000000000..fd95d4fb6
--- /dev/null
+++ b/benchmarks/csharp/run.sh
@@ -0,0 +1,112 @@
+#!/bin/bash
+# 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.
+
+set -euo pipefail
+export ENABLE_FORY_DEBUG_OUTPUT=0
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+DATA=""
+SERIALIZER=""
+DURATION="3"
+WARMUP="1"
+
+usage() {
+    cat <<USAGE
+Usage: $0 [OPTIONS]
+
+Build and run C# benchmarks.
+
+Options:
+  --data <struct|sample|mediacontent|structlist|samplelist|mediacontentlist>
+                               Filter benchmark by data type
+  --serializer <fory|protobuf|msgpack>
+                               Filter benchmark by serializer
+  --duration <seconds>         Measure duration per benchmark (default: 3)
+  --warmup <seconds>           Warmup duration per benchmark (default: 1)
+  --help                       Show this help
+USAGE
+    exit 0
+}
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --data)
+            DATA="$2"
+            shift 2
+            ;;
+        --serializer)
+            SERIALIZER="$2"
+            shift 2
+            ;;
+        --duration)
+            DURATION="$2"
+            shift 2
+            ;;
+        --warmup)
+            WARMUP="$2"
+            shift 2
+            ;;
+        --help|-h)
+            usage
+            ;;
+        *)
+            echo -e "${RED}Unknown option: $1${NC}"
+            usage
+            ;;
+    esac
+done
+
+mkdir -p build report
+RESULT_JSON="build/benchmark_results.json"
+
+RUN_ARGS=(
+    --output "$RESULT_JSON"
+    --duration "$DURATION"
+    --warmup "$WARMUP"
+)
+
+if [[ -n "$DATA" ]]; then
+    RUN_ARGS+=(--data "$DATA")
+fi
+
+if [[ -n "$SERIALIZER" ]]; then
+    RUN_ARGS+=(--serializer "$SERIALIZER")
+fi
+
+echo -e "${GREEN}=== Fory C# Benchmark ===${NC}"
+echo ""
+
+echo -e "${YELLOW}[1/3] Restoring dependencies...${NC}"
+dotnet restore ./Fory.CSharpBenchmark.csproj >/dev/null
+
+echo -e "${YELLOW}[2/3] Running benchmark...${NC}"
+dotnet run -c Release --project ./Fory.CSharpBenchmark.csproj -- 
"${RUN_ARGS[@]}"
+
+echo -e "${YELLOW}[3/3] Generating report...${NC}"
+python3 benchmark_report.py --json-file "$RESULT_JSON" --output-dir report
+
+echo ""
+echo -e "${GREEN}=== All done! ===${NC}"
+echo "Report generated at: $SCRIPT_DIR/report/REPORT.md"
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs 
b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
index 8c6ae679f..5d2cdb09a 100644
--- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -286,11 +286,24 @@ public sealed class ForyObjectGenerator : 
IIncrementalGenerator
         sb.AppendLine("        return 
context.TypeResolver.GetSerializer<T>().Read(context, refMode, readTypeInfo);");
         sb.AppendLine("    }");
         sb.AppendLine();
+        sb.AppendLine("    private static uint? __ForySchemaHashNoTrackRef;");
+        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.AppendLine("        if (!trackRef && 
__ForySchemaHashNoTrackRef.HasValue)");
+        sb.AppendLine("        {");
+        sb.AppendLine("            return __ForySchemaHashNoTrackRef.Value;");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.Append("        uint value = 
global::Apache.Fory.SchemaHash.StructHash32(");
         sb.Append(BuildSchemaFingerprintExpression(model.Members));
         sb.AppendLine(");");
+        sb.AppendLine("        if (!trackRef)");
+        sb.AppendLine("        {");
+        sb.AppendLine("            __ForySchemaHashNoTrackRef = value;");
+        sb.AppendLine("        }");
+        sb.AppendLine();
+        sb.AppendLine("        return value;");
         sb.AppendLine("    }");
         sb.AppendLine();
         sb.AppendLine("    public override global::Apache.Fory.TypeId 
StaticTypeId => global::Apache.Fory.TypeId.Struct;");
@@ -350,7 +363,8 @@ public sealed class ForyObjectGenerator : 
IIncrementalGenerator
 
         sb.AppendLine("        }");
         sb.AppendLine();
-        sb.AppendLine("        
context.Writer.WriteInt32(unchecked((int)__ForySchemaHash(context.TrackRef, 
context.TypeResolver)));");
+        sb.AppendLine("        uint schemaHash = 
__ForySchemaHash(context.TrackRef, context.TypeResolver);");
+        sb.AppendLine("        
context.Writer.WriteInt32(unchecked((int)schemaHash));");
         foreach (MemberModel member in model.SortedMembers)
         {
             EmitWriteMember(sb, member, false);
@@ -393,10 +407,13 @@ public sealed class ForyObjectGenerator : 
IIncrementalGenerator
         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("        if (context.CheckStructVersion)");
         sb.AppendLine("        {");
-        sb.AppendLine("            throw new 
global::Apache.Fory.InvalidDataException($\"class version hash mismatch: 
expected {expectedHash}, got {schemaHash}\");");
+        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();
         sb.AppendLine($"        {model.TypeName} valueSchema = new 
{model.TypeName}();");
diff --git a/csharp/src/Fory/ByteBuffer.cs b/csharp/src/Fory/ByteBuffer.cs
index 4d7e83e14..34a28748d 100644
--- a/csharp/src/Fory/ByteBuffer.cs
+++ b/csharp/src/Fory/ByteBuffer.cs
@@ -21,37 +21,41 @@ namespace Apache.Fory;
 
 public sealed class ByteWriter
 {
-    private readonly List<byte> _storage;
+    private byte[] _storage;
+    private int _count;
 
     public ByteWriter(int capacity = 256)
     {
-        _storage = new List<byte>(capacity);
+        _storage = new byte[Math.Max(1, capacity)];
+        _count = 0;
     }
 
-    public int Count => _storage.Count;
+    public int Count => _count;
 
-    public IReadOnlyList<byte> Storage => _storage;
+    public IReadOnlyList<byte> Storage => new ArraySegment<byte>(_storage, 0, 
_count);
 
     public void Reserve(int additional)
     {
-        _storage.Capacity = Math.Max(_storage.Capacity, _storage.Count + 
additional);
+        EnsureCapacity(additional);
     }
 
     public void WriteUInt8(byte value)
     {
-        _storage.Add(value);
+        EnsureCapacity(1);
+        _storage[_count] = value;
+        _count += 1;
     }
 
     public void WriteInt8(sbyte value)
     {
-        _storage.Add(unchecked((byte)value));
+        WriteUInt8(unchecked((byte)value));
     }
 
     public void WriteUInt16(ushort value)
     {
-        Span<byte> tmp = stackalloc byte[2];
-        BinaryPrimitives.WriteUInt16LittleEndian(tmp, value);
-        WriteBytes(tmp);
+        EnsureCapacity(2);
+        BinaryPrimitives.WriteUInt16LittleEndian(_storage.AsSpan(_count, 2), 
value);
+        _count += 2;
     }
 
     public void WriteInt16(short value)
@@ -61,9 +65,9 @@ public sealed class ByteWriter
 
     public void WriteUInt32(uint value)
     {
-        Span<byte> tmp = stackalloc byte[4];
-        BinaryPrimitives.WriteUInt32LittleEndian(tmp, value);
-        WriteBytes(tmp);
+        EnsureCapacity(4);
+        BinaryPrimitives.WriteUInt32LittleEndian(_storage.AsSpan(_count, 4), 
value);
+        _count += 4;
     }
 
     public void WriteInt32(int value)
@@ -73,9 +77,9 @@ public sealed class ByteWriter
 
     public void WriteUInt64(ulong value)
     {
-        Span<byte> tmp = stackalloc byte[8];
-        BinaryPrimitives.WriteUInt64LittleEndian(tmp, value);
-        WriteBytes(tmp);
+        EnsureCapacity(8);
+        BinaryPrimitives.WriteUInt64LittleEndian(_storage.AsSpan(_count, 8), 
value);
+        _count += 8;
     }
 
     public void WriteInt64(long value)
@@ -85,32 +89,39 @@ public sealed class ByteWriter
 
     public void WriteVarUInt32(uint value)
     {
+        EnsureCapacity(5);
         uint remaining = value;
         while (remaining >= 0x80)
         {
-            WriteUInt8((byte)((remaining & 0x7F) | 0x80));
+            _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
+            _count += 1;
             remaining >>= 7;
         }
 
-        WriteUInt8((byte)remaining);
+        _storage[_count] = (byte)remaining;
+        _count += 1;
     }
 
     public void WriteVarUInt64(ulong value)
     {
+        EnsureCapacity(10);
         ulong remaining = value;
         for (var i = 0; i < 8; i++)
         {
             if (remaining < 0x80)
             {
-                WriteUInt8((byte)remaining);
+                _storage[_count] = (byte)remaining;
+                _count += 1;
                 return;
             }
 
-            WriteUInt8((byte)((remaining & 0x7F) | 0x80));
+            _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
+            _count += 1;
             remaining >>= 7;
         }
 
-        WriteUInt8((byte)(remaining & 0xFF));
+        _storage[_count] = (byte)(remaining & 0xFF);
+        _count += 1;
     }
 
     public void WriteVarUInt36Small(ulong value)
@@ -171,50 +182,99 @@ public sealed class ByteWriter
 
     public void WriteBytes(ReadOnlySpan<byte> bytes)
     {
-        for (int i = 0; i < bytes.Length; i++)
+        EnsureCapacity(bytes.Length);
+        bytes.CopyTo(_storage.AsSpan(_count));
+        _count += bytes.Length;
+    }
+
+    public Span<byte> GetSpan(int size)
+    {
+        if (size < 0)
         {
-            _storage.Add(bytes[i]);
+            throw new ArgumentOutOfRangeException(nameof(size));
         }
+
+        EnsureCapacity(size);
+        return _storage.AsSpan(_count, size);
+    }
+
+    public void Advance(int count)
+    {
+        if (count < 0 || _count + count > _storage.Length)
+        {
+            throw new ArgumentOutOfRangeException(nameof(count));
+        }
+
+        _count += count;
     }
 
     public void SetByte(int index, byte value)
     {
+        if ((uint)index >= (uint)_count)
+        {
+            throw new ArgumentOutOfRangeException(nameof(index));
+        }
+
         _storage[index] = value;
     }
 
     public void SetBytes(int index, ReadOnlySpan<byte> bytes)
     {
-        for (var i = 0; i < bytes.Length; i++)
+        if (index < 0 || index + bytes.Length > _count)
         {
-            _storage[index + i] = bytes[i];
+            throw new ArgumentOutOfRangeException(nameof(index));
         }
+
+        bytes.CopyTo(_storage.AsSpan(index));
     }
 
     public byte[] ToArray()
     {
-        return _storage.ToArray();
+        byte[] result = new byte[_count];
+        Array.Copy(_storage, 0, result, 0, _count);
+        return result;
     }
 
     public void Reset()
     {
-        _storage.Clear();
+        _count = 0;
+    }
+
+    private void EnsureCapacity(int additional)
+    {
+        int required = _count + additional;
+        if (required <= _storage.Length)
+        {
+            return;
+        }
+
+        int next = _storage.Length * 2;
+        if (next < required)
+        {
+            next = required;
+        }
+
+        Array.Resize(ref _storage, next);
     }
 }
 
 public sealed class ByteReader
 {
-    private readonly byte[] _storage;
+    private byte[] _storage;
+    private int _length;
     private int _cursor;
 
     public ByteReader(ReadOnlySpan<byte> data)
     {
         _storage = data.ToArray();
+        _length = _storage.Length;
         _cursor = 0;
     }
 
     public ByteReader(byte[] bytes)
     {
         _storage = bytes;
+        _length = bytes.Length;
         _cursor = 0;
     }
 
@@ -222,7 +282,21 @@ public sealed class ByteReader
 
     public int Cursor => _cursor;
 
-    public int Remaining => _storage.Length - _cursor;
+    public int Remaining => _length - _cursor;
+
+    public void Reset(ReadOnlySpan<byte> data)
+    {
+        _storage = data.ToArray();
+        _length = _storage.Length;
+        _cursor = 0;
+    }
+
+    public void Reset(byte[] bytes)
+    {
+        _storage = bytes;
+        _length = bytes.Length;
+        _cursor = 0;
+    }
 
     public void SetCursor(int value)
     {
@@ -236,9 +310,9 @@ public sealed class ByteReader
 
     public void CheckBound(int need)
     {
-        if (_cursor + need > _storage.Length)
+        if (_cursor + need > _length)
         {
-            throw new OutOfBoundsException(_cursor, need, _storage.Length);
+            throw new OutOfBoundsException(_cursor, need, _length);
         }
     }
 
@@ -296,14 +370,24 @@ public sealed class ByteReader
 
     public uint ReadVarUInt32()
     {
+        byte[] storage = _storage;
+        int cursor = _cursor;
+        int length = _length;
         uint result = 0;
-        var shift = 0;
+        int shift = 0;
         while (true)
         {
-            byte b = ReadUInt8();
+            if (cursor >= length)
+            {
+                throw new OutOfBoundsException(cursor, 1, length);
+            }
+
+            byte b = storage[cursor];
+            cursor += 1;
             result |= (uint)(b & 0x7F) << shift;
             if ((b & 0x80) == 0)
             {
+                _cursor = cursor;
                 return result;
             }
 
@@ -317,22 +401,39 @@ public sealed class ByteReader
 
     public ulong ReadVarUInt64()
     {
+        byte[] storage = _storage;
+        int cursor = _cursor;
+        int length = _length;
         ulong result = 0;
-        var shift = 0;
+        int shift = 0;
         for (var i = 0; i < 8; i++)
         {
-            byte b = ReadUInt8();
+            if (cursor >= length)
+            {
+                throw new OutOfBoundsException(cursor, 1, length);
+            }
+
+            byte b = storage[cursor];
+            cursor += 1;
             result |= (ulong)(b & 0x7F) << shift;
             if ((b & 0x80) == 0)
             {
+                _cursor = cursor;
                 return result;
             }
 
             shift += 7;
         }
 
-        byte last = ReadUInt8();
+        if (cursor >= length)
+        {
+            throw new OutOfBoundsException(cursor, 1, length);
+        }
+
+        byte last = storage[cursor];
+        cursor += 1;
         result |= (ulong)last << 56;
+        _cursor = cursor;
         return result;
     }
 
diff --git a/csharp/src/Fory/CollectionSerializers.cs 
b/csharp/src/Fory/CollectionSerializers.cs
index 09d869b11..e5130c02c 100644
--- a/csharp/src/Fory/CollectionSerializers.cs
+++ b/csharp/src/Fory/CollectionSerializers.cs
@@ -31,6 +31,16 @@ internal static class CollectionBits
 
 internal static class CollectionCodec
 {
+    private static bool CanDeclareElementType<T>(TypeId staticTypeId)
+    {
+        if (!staticTypeId.NeedsTypeInfoForField())
+        {
+            return true;
+        }
+
+        return typeof(T).IsSealed;
+    }
+
     public static void WriteCollectionData<T>(
         IEnumerable<T> values,
         Serializer<T> elementSerializer,
@@ -60,7 +70,7 @@ internal static class CollectionCodec
         }
 
         bool trackRef = context.TrackRef && 
elementSerializer.IsReferenceTrackableType;
-        bool declaredElementType = hasGenerics && 
!elementSerializer.StaticTypeId.NeedsTypeInfoForField();
+        bool declaredElementType = hasGenerics && 
CanDeclareElementType<T>(elementSerializer.StaticTypeId);
         bool dynamicElementType = elementSerializer.StaticTypeId == 
TypeId.Unknown;
 
         byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType;
diff --git a/csharp/src/Fory/Config.cs b/csharp/src/Fory/Config.cs
index 8dc88be42..5cde5d848 100644
--- a/csharp/src/Fory/Config.cs
+++ b/csharp/src/Fory/Config.cs
@@ -22,8 +22,7 @@ public sealed record Config(
     bool TrackRef = false,
     bool Compatible = false,
     bool CheckStructVersion = false,
-    bool EnableReflectionFallback = false,
-    int MaxDepth = 512);
+    int MaxDepth = 20);
 
 public sealed class ForyBuilder
 {
@@ -31,8 +30,7 @@ public sealed class ForyBuilder
     private bool _trackRef;
     private bool _compatible;
     private bool _checkStructVersion;
-    private bool _enableReflectionFallback;
-    private int _maxDepth = 512;
+    private int _maxDepth = 20;
 
     public ForyBuilder Xlang(bool enabled = true)
     {
@@ -58,12 +56,6 @@ public sealed class ForyBuilder
         return this;
     }
 
-    public ForyBuilder EnableReflectionFallback(bool enabled = false)
-    {
-        _enableReflectionFallback = enabled;
-        return this;
-    }
-
     public ForyBuilder MaxDepth(int value)
     {
         if (value <= 0)
@@ -82,7 +74,6 @@ public sealed class ForyBuilder
             TrackRef: _trackRef,
             Compatible: _compatible,
             CheckStructVersion: _checkStructVersion,
-            EnableReflectionFallback: _enableReflectionFallback,
             MaxDepth: _maxDepth);
     }
 
diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs
index 138745306..f07466dee 100644
--- a/csharp/src/Fory/Context.cs
+++ b/csharp/src/Fory/Context.cs
@@ -149,6 +149,7 @@ public sealed class WriteContext
         TypeResolver typeResolver,
         bool trackRef,
         bool compatible = false,
+        bool checkStructVersion = false,
         CompatibleTypeDefWriteState? compatibleTypeDefState = null,
         MetaStringWriteState? metaStringWriteState = null)
     {
@@ -156,6 +157,7 @@ public sealed class WriteContext
         TypeResolver = typeResolver;
         TrackRef = trackRef;
         Compatible = compatible;
+        CheckStructVersion = checkStructVersion;
         RefWriter = new RefWriter();
         CompatibleTypeDefState = compatibleTypeDefState ?? new 
CompatibleTypeDefWriteState();
         MetaStringWriteState = metaStringWriteState ?? new 
MetaStringWriteState();
@@ -169,6 +171,8 @@ public sealed class WriteContext
 
     public bool Compatible { get; }
 
+    public bool CheckStructVersion { get; }
+
     public RefWriter RefWriter { get; }
 
     public CompatibleTypeDefWriteState CompatibleTypeDefState { get; }
@@ -231,6 +235,7 @@ public sealed class ReadContext
         TypeResolver typeResolver,
         bool trackRef,
         bool compatible = false,
+        bool checkStructVersion = false,
         CompatibleTypeDefReadState? compatibleTypeDefState = null,
         MetaStringReadState? metaStringReadState = null)
     {
@@ -238,6 +243,7 @@ public sealed class ReadContext
         TypeResolver = typeResolver;
         TrackRef = trackRef;
         Compatible = compatible;
+        CheckStructVersion = checkStructVersion;
         RefReader = new RefReader();
         CompatibleTypeDefState = compatibleTypeDefState ?? new 
CompatibleTypeDefReadState();
         MetaStringReadState = metaStringReadState ?? new MetaStringReadState();
@@ -251,6 +257,8 @@ public sealed class ReadContext
 
     public bool Compatible { get; }
 
+    public bool CheckStructVersion { get; }
+
     public RefReader RefReader { get; }
 
     public CompatibleTypeDefReadState CompatibleTypeDefState { get; }
diff --git a/csharp/src/Fory/EnumSerializer.cs 
b/csharp/src/Fory/EnumSerializer.cs
index 315573264..e830f0756 100644
--- a/csharp/src/Fory/EnumSerializer.cs
+++ b/csharp/src/Fory/EnumSerializer.cs
@@ -19,25 +19,53 @@ namespace Apache.Fory;
 
 public sealed class EnumSerializer<TEnum> : Serializer<TEnum> where TEnum : 
struct, Enum
 {
+    private static readonly Dictionary<TEnum, uint> DefinedValueToOrdinal = 
BuildValueToOrdinalMap();
+    private static readonly Dictionary<uint, TEnum> DefinedOrdinalToValue = 
BuildOrdinalToValueMap(DefinedValueToOrdinal);
+
     public override TypeId StaticTypeId => TypeId.Enum;
     public override TEnum DefaultValue => default;
 
     public override void WriteData(WriteContext context, in TEnum value, bool 
hasGenerics)
     {
         _ = hasGenerics;
-        uint ordinal = Convert.ToUInt32(value);
+        if (!DefinedValueToOrdinal.TryGetValue(value, out uint ordinal))
+        {
+            ordinal = Convert.ToUInt32(value);
+        }
+
         context.Writer.WriteVarUInt32(ordinal);
     }
 
     public override TEnum ReadData(ReadContext context)
     {
         uint ordinal = context.Reader.ReadVarUInt32();
-        TEnum value = (TEnum)Enum.ToObject(typeof(TEnum), ordinal);
-        if (!Enum.IsDefined(typeof(TEnum), value))
+        if (DefinedOrdinalToValue.TryGetValue(ordinal, out TEnum value))
+        {
+            return value;
+        }
+
+        return (TEnum)Enum.ToObject(typeof(TEnum), ordinal);
+    }
+
+    private static Dictionary<TEnum, uint> BuildValueToOrdinalMap()
+    {
+        Dictionary<TEnum, uint> values = [];
+        foreach (TEnum value in Enum.GetValues<TEnum>())
+        {
+            values[value] = Convert.ToUInt32(value);
+        }
+
+        return values;
+    }
+
+    private static Dictionary<uint, TEnum> 
BuildOrdinalToValueMap(Dictionary<TEnum, uint> valueToOrdinal)
+    {
+        Dictionary<uint, TEnum> ordinalToValue = [];
+        foreach (KeyValuePair<TEnum, uint> pair in valueToOrdinal)
         {
-            throw new InvalidDataException($"unknown enum ordinal {ordinal}");
+            ordinalToValue[pair.Value] = pair.Key;
         }
 
-        return value;
+        return ordinalToValue;
     }
 }
diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs
index 643da63e2..d70cef7dd 100644
--- a/csharp/src/Fory/Fory.cs
+++ b/csharp/src/Fory/Fory.cs
@@ -39,6 +39,7 @@ public sealed class Fory
             _typeResolver,
             Config.TrackRef,
             Config.Compatible,
+            Config.CheckStructVersion,
             new CompatibleTypeDefWriteState(),
             new MetaStringWriteState());
         _readContext = new ReadContext(
@@ -46,6 +47,7 @@ public sealed class Fory
             _typeResolver,
             Config.TrackRef,
             Config.Compatible,
+            Config.CheckStructVersion,
             new CompatibleTypeDefReadState(),
             new MetaStringReadState());
     }
@@ -93,7 +95,8 @@ public sealed class Fory
 
     public byte[] Serialize<T>(in T value)
     {
-        ByteWriter writer = new();
+        ByteWriter writer = _writeContext.Writer;
+        writer.Reset();
         Serializer<T> serializer = _typeResolver.GetSerializer<T>();
         bool isNone = serializer.IsNone(value);
         WriteHead(writer, isNone);
@@ -116,7 +119,21 @@ public sealed class Fory
 
     public T Deserialize<T>(ReadOnlySpan<byte> payload)
     {
-        ByteReader reader = new(payload);
+        ByteReader reader = _readContext.Reader;
+        reader.Reset(payload);
+        T value = DeserializeFromReader<T>(reader);
+        if (reader.Remaining != 0)
+        {
+            throw new InvalidDataException($"unexpected trailing bytes after 
deserializing {typeof(T)}");
+        }
+
+        return value;
+    }
+
+    public T Deserialize<T>(byte[] payload)
+    {
+        ByteReader reader = _readContext.Reader;
+        reader.Reset(payload);
         T value = DeserializeFromReader<T>(reader);
         if (reader.Remaining != 0)
         {
@@ -129,7 +146,8 @@ public sealed class Fory
     public T Deserialize<T>(ref ReadOnlySequence<byte> payload)
     {
         byte[] bytes = payload.ToArray();
-        ByteReader reader = new(bytes);
+        ByteReader reader = _readContext.Reader;
+        reader.Reset(bytes);
         T value = DeserializeFromReader<T>(reader);
         payload = payload.Slice(reader.Cursor);
         return value;
@@ -137,7 +155,8 @@ public sealed class Fory
 
     public byte[] SerializeObject(object? value)
     {
-        ByteWriter writer = new();
+        ByteWriter writer = _writeContext.Writer;
+        writer.Reset();
         bool isNone = value is null;
         WriteHead(writer, isNone);
         if (!isNone)
@@ -159,7 +178,21 @@ public sealed class Fory
 
     public object? DeserializeObject(ReadOnlySpan<byte> payload)
     {
-        ByteReader reader = new(payload);
+        ByteReader reader = _readContext.Reader;
+        reader.Reset(payload);
+        object? value = DeserializeObjectFromReader(reader);
+        if (reader.Remaining != 0)
+        {
+            throw new InvalidDataException("unexpected trailing bytes after 
deserializing dynamic object");
+        }
+
+        return value;
+    }
+
+    public object? DeserializeObject(byte[] payload)
+    {
+        ByteReader reader = _readContext.Reader;
+        reader.Reset(payload);
         object? value = DeserializeObjectFromReader(reader);
         if (reader.Remaining != 0)
         {
@@ -172,7 +205,8 @@ public sealed class Fory
     public object? DeserializeObject(ref ReadOnlySequence<byte> payload)
     {
         byte[] bytes = payload.ToArray();
-        ByteReader reader = new(bytes);
+        ByteReader reader = _readContext.Reader;
+        reader.Reset(bytes);
         object? value = DeserializeObjectFromReader(reader);
         payload = payload.Slice(reader.Cursor);
         return value;
diff --git a/csharp/src/Fory/StringSerializer.cs 
b/csharp/src/Fory/StringSerializer.cs
index e6ba3e69f..9dcf4ff5a 100644
--- a/csharp/src/Fory/StringSerializer.cs
+++ b/csharp/src/Fory/StringSerializer.cs
@@ -65,28 +65,22 @@ public sealed class StringSerializer : Serializer<string>
         ulong header = context.Reader.ReadVarUInt36Small();
         ulong encoding = header & 0x03;
         int byteLength = checked((int)(header >> 2));
-        byte[] bytes = context.Reader.ReadBytes(byteLength);
+        ReadOnlySpan<byte> bytes = context.Reader.ReadSpan(byteLength);
         return encoding switch
-        {
-            (ulong)ForyStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes),
-            (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes),
+            {
+                (ulong)ForyStringEncoding.Utf8 => 
Encoding.UTF8.GetString(bytes),
+                (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes),
             (ulong)ForyStringEncoding.Utf16 => DecodeUtf16(bytes),
             _ => throw new EncodingException($"unsupported string encoding 
{encoding}"),
         };
     }
 
-    private static string DecodeLatin1(byte[] bytes)
+    private static string DecodeLatin1(ReadOnlySpan<byte> bytes)
     {
-        return string.Create(bytes.Length, bytes, static (span, b) =>
-        {
-            for (int i = 0; i < b.Length; i++)
-            {
-                span[i] = (char)b[i];
-            }
-        });
+        return Encoding.Latin1.GetString(bytes);
     }
 
-    private static string DecodeUtf16(byte[] bytes)
+    private static string DecodeUtf16(ReadOnlySpan<byte> bytes)
     {
         if ((bytes.Length & 1) != 0)
         {
@@ -139,31 +133,34 @@ public sealed class StringSerializer : Serializer<string>
 
     private static void WriteLatin1(WriteContext context, string value)
     {
-        byte[] latin1 = new byte[value.Length];
+        int byteLength = value.Length;
+        ulong header = ((ulong)byteLength << 2) | 
(ulong)ForyStringEncoding.Latin1;
+        context.Writer.WriteVarUInt36Small(header);
+        Span<byte> latin1 = context.Writer.GetSpan(byteLength);
         for (int i = 0; i < value.Length; i++)
         {
             latin1[i] = unchecked((byte)value[i]);
         }
-
-        WriteEncodedBytes(context, latin1, ForyStringEncoding.Latin1);
+        context.Writer.Advance(byteLength);
     }
 
     private static void WriteUtf8(WriteContext context, string value)
     {
-        byte[] utf8 = Encoding.UTF8.GetBytes(value);
-        WriteEncodedBytes(context, utf8, ForyStringEncoding.Utf8);
+        int byteLength = Encoding.UTF8.GetByteCount(value);
+        ulong header = ((ulong)byteLength << 2) | 
(ulong)ForyStringEncoding.Utf8;
+        context.Writer.WriteVarUInt36Small(header);
+        Span<byte> utf8 = context.Writer.GetSpan(byteLength);
+        int written = Encoding.UTF8.GetBytes(value, utf8);
+        context.Writer.Advance(written);
     }
 
     private static void WriteUtf16(WriteContext context, string value)
     {
-        byte[] utf16 = Encoding.Unicode.GetBytes(value);
-        WriteEncodedBytes(context, utf16, ForyStringEncoding.Utf16);
-    }
-
-    private static void WriteEncodedBytes(WriteContext context, byte[] bytes, 
ForyStringEncoding encoding)
-    {
-        ulong header = ((ulong)bytes.Length << 2) | (ulong)encoding;
+        int byteLength = Encoding.Unicode.GetByteCount(value);
+        ulong header = ((ulong)byteLength << 2) | 
(ulong)ForyStringEncoding.Utf16;
         context.Writer.WriteVarUInt36Small(header);
-        context.Writer.WriteBytes(bytes);
+        Span<byte> utf16 = context.Writer.GetSpan(byteLength);
+        int written = Encoding.Unicode.GetBytes(value, utf16);
+        context.Writer.Advance(written);
     }
 }
diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs
index c1bf9a731..62fd83c6c 100644
--- a/csharp/src/Fory/TypeResolver.cs
+++ b/csharp/src/Fory/TypeResolver.cs
@@ -23,6 +23,14 @@ namespace Apache.Fory;
 public sealed class TypeResolver
 {
     private static readonly ConcurrentDictionary<Type, Func<Serializer>> 
GeneratedFactories = new();
+    private static readonly ConcurrentDictionary<TypeId, HashSet<TypeId>> 
SingleAllowedWireTypes = new();
+    private static readonly HashSet<TypeId> CompatibleStructAllowedWireTypes =
+    [
+        TypeId.Struct,
+        TypeId.NamedStruct,
+        TypeId.CompatibleStruct,
+        TypeId.NamedCompatibleStruct,
+    ];
     private static readonly Dictionary<Type, Type> 
PrimitiveStringKeyDictionaryCodecs = new()
     {
         [typeof(string)] = typeof(StringPrimitiveDictionaryCodec),
@@ -363,17 +371,13 @@ public sealed class TypeResolver
     internal static HashSet<TypeId> AllowedWireTypeIds(TypeId declaredKind, 
bool registerByName, bool compatible)
     {
         TypeId baseKind = NormalizeBaseKind(declaredKind);
-        TypeId expected = ResolveWireTypeId(declaredKind, registerByName, 
compatible);
-        HashSet<TypeId> allowed = [expected];
         if (baseKind == TypeId.Struct && compatible)
         {
-            allowed.Add(TypeId.CompatibleStruct);
-            allowed.Add(TypeId.NamedCompatibleStruct);
-            allowed.Add(TypeId.Struct);
-            allowed.Add(TypeId.NamedStruct);
+            return CompatibleStructAllowedWireTypes;
         }
 
-        return allowed;
+        TypeId expected = ResolveWireTypeId(declaredKind, registerByName, 
compatible);
+        return SingleAllowedWireTypes.GetOrAdd(expected, static typeId => 
[typeId]);
     }
 
     public object? ReadByUserTypeId(uint userTypeId, ReadContext context, 
TypeMeta? compatibleTypeMeta = null)
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs 
b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index e4ea145e2..5a9e4e6c2 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -662,7 +662,7 @@ public sealed class ForyRuntimeTests
     [Fact]
     public void MacroFieldOrderFollowsForyRules()
     {
-        ForyRuntime fory = ForyRuntime.Builder().Build();
+        ForyRuntime fory = 
ForyRuntime.Builder().CheckStructVersion(true).Build();
         fory.Register<FieldOrder>(300);
 
         FieldOrder value = new() { Z = "tail", A = 123_456_789, B = 17, C = 99 
};
@@ -724,10 +724,10 @@ public sealed class ForyRuntimeTests
     [Fact]
     public void SchemaVersionMismatchThrows()
     {
-        ForyRuntime writer = ForyRuntime.Builder().Compatible(false).Build();
+        ForyRuntime writer = 
ForyRuntime.Builder().Compatible(false).CheckStructVersion(true).Build();
         writer.Register<OneStringField>(200);
 
-        ForyRuntime reader = ForyRuntime.Builder().Compatible(false).Build();
+        ForyRuntime reader = 
ForyRuntime.Builder().Compatible(false).CheckStructVersion(true).Build();
         reader.Register<TwoStringField>(200);
 
         byte[] payload = writer.Serialize(new OneStringField { F1 = "hello" });
@@ -766,6 +766,18 @@ public sealed class ForyRuntimeTests
         Assert.Equal(value.Value, decoded.Value);
     }
 
+    [Fact]
+    public void EnumRoundTripPreservesUndefinedValue()
+    {
+        ForyRuntime fory = ForyRuntime.Builder().Build();
+        fory.Register<TestColor>(100);
+
+        TestColor value = (TestColor)12345;
+        TestColor decoded = fory.Deserialize<TestColor>(fory.Serialize(value));
+        Assert.Equal(value, decoded);
+        Assert.Equal(12345u, Convert.ToUInt32(decoded));
+    }
+
     [Fact]
     public void DynamicObjectSupportsObjectKeyMapAndSet()
     {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to