diff --git a/AyCode.Benchmark/Program.cs b/AyCode.Benchmark/Program.cs
index 8557e54..234a02a 100644
--- a/AyCode.Benchmark/Program.cs
+++ b/AyCode.Benchmark/Program.cs
@@ -7,6 +7,9 @@ using MessagePack;
using MessagePack.Resolvers;
using BenchmarkDotNet.Configs;
using System.IO;
+using AyCode.Core.Serializers.Jsons;
+using AyCode.Core.Serializers.Binaries;
+using System.Diagnostics;
namespace AyCode.Benchmark
{
@@ -67,6 +70,12 @@ namespace AyCode.Benchmark
var config = ManualConfig.Create(DefaultConfig.Instance)
.WithArtifactsPath(benchmarkDir);
+ if (args.Length > 0 && args[0] == "--quick")
+ {
+ RunQuickBenchmark();
+ return;
+ }
+
if (args.Length > 0 && args[0] == "--test")
{
var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir);
@@ -112,6 +121,7 @@ namespace AyCode.Benchmark
}
Console.WriteLine("Usage:");
+ Console.WriteLine(" --quick Quick benchmark with tabular output (AcBinary vs MessagePack)");
Console.WriteLine(" --test Quick AcBinary test");
Console.WriteLine(" --testmsgpack Quick MessagePack test");
Console.WriteLine(" --minimal Minimal benchmark");
@@ -134,6 +144,193 @@ namespace AyCode.Benchmark
}
}
+ ///
+ /// Quick benchmark comparing AcBinary vs MessagePack with tabular output.
+ /// Tests: WithRef, NoRef, Serialize, Deserialize, Populate, Merge
+ ///
+ static void RunQuickBenchmark(int iterations = 1000)
+ {
+ Console.WriteLine();
+ Console.WriteLine("????????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine("? AcBinary vs MessagePack Quick Benchmark ?");
+ Console.WriteLine("????????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine();
+
+ // Create test data with shared references
+ TestDataFactory.ResetIdCounter();
+ var sharedTag = TestDataFactory.CreateTag("SharedTag");
+ var sharedUser = TestDataFactory.CreateUser("shareduser");
+ var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
+
+ var testOrder = TestDataFactory.CreateOrder(
+ itemCount: 3,
+ palletsPerItem: 3,
+ measurementsPerPallet: 3,
+ pointsPerMeasurement: 4,
+ sharedTag: sharedTag,
+ sharedUser: sharedUser,
+ sharedMetadata: sharedMeta);
+
+ // Options
+ var withRefOptions = new AcBinarySerializerOptions();
+ var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
+ var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
+
+ // Warm up
+ Console.WriteLine("Warming up...");
+ for (int i = 0; i < 100; i++)
+ {
+ _ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
+ _ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
+ _ = MessagePackSerializer.Serialize(testOrder, msgPackOptions);
+ }
+
+ // Pre-serialize data for deserialization tests
+ var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);
+ var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions);
+ var msgPackData = MessagePackSerializer.Serialize(testOrder, msgPackOptions);
+
+ Console.WriteLine($"Iterations: {iterations:N0}");
+ Console.WriteLine();
+
+ // Size comparison
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine("? SIZE COMPARISON ?");
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine("? Format ? Size (bytes) ? vs MessagePack ? Savings ?");
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine($"? AcBinary (WithRef) ? {acBinaryWithRef.Length,14:N0} ? {100.0 * acBinaryWithRef.Length / msgPackData.Length,13:F1}% ? {msgPackData.Length - acBinaryWithRef.Length,14:N0} ?");
+ Console.WriteLine($"? AcBinary (NoRef) ? {acBinaryNoRef.Length,14:N0} ? {100.0 * acBinaryNoRef.Length / msgPackData.Length,13:F1}% ? {msgPackData.Length - acBinaryNoRef.Length,14:N0} ?");
+ Console.WriteLine($"? MessagePack ? {msgPackData.Length,14:N0} ? {100.0,13:F1}% ? {"(baseline)",14} ?");
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine();
+
+ // Benchmark results storage
+ var results = new List<(string Operation, string Mode, double AcBinaryMs, double MsgPackMs)>();
+
+ // Serialize benchmarks
+ var sw = Stopwatch.StartNew();
+
+ // AcBinary WithRef Serialize
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ _ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
+ var acWithRefSerialize = sw.Elapsed.TotalMilliseconds;
+
+ // AcBinary NoRef Serialize
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ _ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
+ var acNoRefSerialize = sw.Elapsed.TotalMilliseconds;
+
+ // MessagePack Serialize
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ _ = MessagePackSerializer.Serialize(testOrder, msgPackOptions);
+ var msgPackSerialize = sw.Elapsed.TotalMilliseconds;
+
+ results.Add(("Serialize", "WithRef", acWithRefSerialize, msgPackSerialize));
+ results.Add(("Serialize", "NoRef", acNoRefSerialize, msgPackSerialize));
+
+ // Deserialize benchmarks
+ // AcBinary WithRef Deserialize
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ _ = AcBinaryDeserializer.Deserialize(acBinaryWithRef);
+ var acWithRefDeserialize = sw.Elapsed.TotalMilliseconds;
+
+ // AcBinary NoRef Deserialize
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ _ = AcBinaryDeserializer.Deserialize(acBinaryNoRef);
+ var acNoRefDeserialize = sw.Elapsed.TotalMilliseconds;
+
+ // MessagePack Deserialize
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ _ = MessagePackSerializer.Deserialize(msgPackData, msgPackOptions);
+ var msgPackDeserialize = sw.Elapsed.TotalMilliseconds;
+
+ results.Add(("Deserialize", "WithRef", acWithRefDeserialize, msgPackDeserialize));
+ results.Add(("Deserialize", "NoRef", acNoRefDeserialize, msgPackDeserialize));
+
+ // Populate benchmark (AcBinary only)
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ {
+ var target = CreatePopulateTarget(testOrder);
+ AcBinaryDeserializer.Populate(acBinaryNoRef, target);
+ }
+ var acPopulate = sw.Elapsed.TotalMilliseconds;
+ results.Add(("Populate", "NoRef", acPopulate, 0)); // MessagePack doesn't have Populate
+
+ // PopulateMerge benchmark (AcBinary only)
+ sw.Restart();
+ for (int i = 0; i < iterations; i++)
+ {
+ var target = CreatePopulateTarget(testOrder);
+ AcBinaryDeserializer.PopulateMerge(acBinaryNoRef.AsSpan(), target);
+ }
+ var acMerge = sw.Elapsed.TotalMilliseconds;
+ results.Add(("Merge", "NoRef", acMerge, 0));
+
+ // Round-trip
+ var acWithRefRoundTrip = acWithRefSerialize + acWithRefDeserialize;
+ var acNoRefRoundTrip = acNoRefSerialize + acNoRefDeserialize;
+ var msgPackRoundTrip = msgPackSerialize + msgPackDeserialize;
+ results.Add(("Round-trip", "WithRef", acWithRefRoundTrip, msgPackRoundTrip));
+ results.Add(("Round-trip", "NoRef", acNoRefRoundTrip, msgPackRoundTrip));
+
+ // Print performance table
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine("? PERFORMANCE COMPARISON (lower is better) ?");
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine("? Operation ? AcBinary (ms) ? MessagePack ? Ratio ?");
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+
+ foreach (var r in results)
+ {
+ var opName = $"{r.Operation} ({r.Mode})";
+ if (r.MsgPackMs > 0)
+ {
+ var ratio = r.AcBinaryMs / r.MsgPackMs;
+ var ratioStr = ratio < 1 ? $"{ratio:F2}x faster" : $"{ratio:F2}x slower";
+ Console.WriteLine($"? {opName,-24} ? {r.AcBinaryMs,14:F2} ? {r.MsgPackMs,14:F2} ? {ratioStr,14} ?");
+ }
+ else
+ {
+ Console.WriteLine($"? {opName,-24} ? {r.AcBinaryMs,14:F2} ? {"N/A",14} ? {"(unique)",14} ?");
+ }
+ }
+
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine();
+
+ // Summary
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine("? SUMMARY ?");
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ var sizeAdvantage = 100.0 - (100.0 * acBinaryNoRef.Length / msgPackData.Length);
+ Console.WriteLine($"? Size advantage: AcBinary is {sizeAdvantage:F1}% smaller than MessagePack ?");
+
+ var serializeRatio = acNoRefSerialize / msgPackSerialize;
+ var deserializeRatio = acNoRefDeserialize / msgPackDeserialize;
+ Console.WriteLine($"? Serialize (NoRef): AcBinary is {(serializeRatio < 1 ? $"{1/serializeRatio:F2}x faster" : $"{serializeRatio:F2}x slower"),-20} ?");
+ Console.WriteLine($"? Deserialize (NoRef): AcBinary is {(deserializeRatio < 1 ? $"{1/deserializeRatio:F2}x faster" : $"{deserializeRatio:F2}x slower"),-18} ?");
+ Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
+ Console.WriteLine();
+ }
+
+ static TestOrder CreatePopulateTarget(TestOrder source)
+ {
+ var target = new TestOrder { Id = source.Id };
+ foreach (var item in source.Items)
+ {
+ target.Items.Add(new TestOrderItem { Id = item.Id });
+ }
+ return target;
+ }
+
static (string InDir, string OutDir) CreateMSTestDeployDirs(string mstestBase)
{
var user = Environment.UserName ?? "Deploy";
diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs
index 0d4901c..fa3e787 100644
--- a/AyCode.Benchmark/SerializationBenchmarks.cs
+++ b/AyCode.Benchmark/SerializationBenchmarks.cs
@@ -12,6 +12,8 @@ using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using System.IO;
+using AyCode.Core.Serializers.Binaries;
+using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Benchmarks;
diff --git a/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs b/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs
index c9fb329..daf4008 100644
--- a/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs
+++ b/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs
@@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
+using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using BenchmarkDotNet.Attributes;
using MessagePack;
diff --git a/AyCode.Benchmark/Test_Benchmark_Results/.gitignore b/AyCode.Benchmark/Test_Benchmark_Results/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/AyCode.Benchmark/Test_Benchmark_Results/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/AyCode.Core.Tests/JsonExtensionTests.cs b/AyCode.Core.Tests/JsonExtensionTests.cs
index 161b08c..be5d3b7 100644
--- a/AyCode.Core.Tests/JsonExtensionTests.cs
+++ b/AyCode.Core.Tests/JsonExtensionTests.cs
@@ -3,6 +3,7 @@ using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Interfaces;
using AyCode.Core.Loggers;
+using AyCode.Core.Serializers.Jsons;
using Newtonsoft.Json;
using AyCode.Core.Tests.TestModels;
diff --git a/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs b/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs
index 0d89f4e..722d7b2 100644
--- a/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs
+++ b/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs
@@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
+using AyCode.Core.Serializers.Binaries;
namespace AyCode.Core.Tests.Serialization;
diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs
index ac2532a..8b91592 100644
--- a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs
+++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs
@@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
+using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
namespace AyCode.Core.Tests.serialization;
diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs
index 4767b73..3c29235 100644
--- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs
+++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs
@@ -1,5 +1,7 @@
-using System.Diagnostics;
+using System.Diagnostics;
using AyCode.Core.Extensions;
+using AyCode.Core.Serializers.Binaries;
+using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using MessagePack;
using MessagePack.Resolvers;
@@ -205,9 +207,9 @@ public class QuickBenchmark
var sizeDiff = msgPackData.Length - acBinaryData.Length;
if (sizeDiff > 0)
- Console.WriteLine($"? AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)");
+ Console.WriteLine($"✅ AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)");
else
- Console.WriteLine($"?? AcBinary {-sizeDiff:N0} bytes larger");
+ Console.WriteLine($"⚠️ AcBinary {-sizeDiff:N0} bytes larger");
Assert.IsNotNull(acBinaryResult);
Assert.IsNotNull(msgPackResult);
@@ -279,7 +281,7 @@ public class QuickBenchmark
Console.WriteLine();
var sizeSaving = msgPack.Length - acWithIntern.Length;
- Console.WriteLine($"? String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)");
+ Console.WriteLine($"✅ String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)");
Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller");
}
diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs
index f465c0a..3c4f9a5 100644
--- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs
+++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs
@@ -1,5 +1,6 @@
using AyCode.Core.Extensions;
using AyCode.Core.Interfaces;
+using AyCode.Core.Serializers.Jsons;
using MessagePack;
using MongoDB.Bson.Serialization.Attributes;
using Newtonsoft.Json;
diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs
deleted file mode 100644
index 5d4b892..0000000
--- a/AyCode.Core/Extensions/AcBinarySerializer.cs
+++ /dev/null
@@ -1,2001 +0,0 @@
-using System.Buffers;
-using System.Collections;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Text;
-using static AyCode.Core.Extensions.JsonUtilities;
-
-namespace AyCode.Core.Extensions;
-
-///
-/// High-performance binary serializer optimized for speed and memory efficiency.
-/// Features:
-/// - VarInt encoding for compact integers (MessagePack-style)
-/// - String interning for repeated strings
-/// - Property name table for fast lookup
-/// - Reference handling for circular/shared references
-/// - Optional metadata for schema evolution
-/// - Optimized buffer management with ArrayPool
-/// - Zero-allocation hot paths using Span and MemoryMarshal
-///
-public static class AcBinarySerializer
-{
- private static readonly ConcurrentDictionary TypeMetadataCache = new();
-
- // Pre-computed UTF8 encoder for string operations
- private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
- private static readonly Type StringType = typeof(string);
- private static readonly Type GuidType = typeof(Guid);
- private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
- private static readonly Type TimeSpanType = typeof(TimeSpan);
- private static readonly Type IntType = typeof(int);
- private static readonly Type LongType = typeof(long);
- private static readonly Type FloatType = typeof(float);
- private static readonly Type DoubleType = typeof(double);
- private static readonly Type DecimalType = typeof(decimal);
- private static readonly Type BoolType = typeof(bool);
- private static readonly Type DateTimeType = typeof(DateTime);
-
- #region Public API
-
- ///
- /// Serialize object to binary with default options.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static byte[] Serialize(T value) => Serialize(value, AcBinarySerializerOptions.Default);
-
- ///
- /// Serialize object to binary with specified options.
- ///
- public static byte[] Serialize(T value, AcBinarySerializerOptions options)
- {
- if (value == null)
- {
- return [BinaryTypeCode.Null];
- }
-
- var runtimeType = value.GetType();
- var context = SerializeCore(value, runtimeType, options);
- try
- {
- return context.ToArray();
- }
- finally
- {
- BinarySerializationContextPool.Return(context);
- }
- }
-
- ///
- /// Serialize object to an IBufferWriter for zero-copy scenarios.
- /// This avoids the final ToArray() allocation by writing directly to the caller's buffer.
- ///
- public static void Serialize(T value, IBufferWriter writer, AcBinarySerializerOptions options)
- {
- if (value == null)
- {
- var span = writer.GetSpan(1);
- span[0] = BinaryTypeCode.Null;
- writer.Advance(1);
- return;
- }
-
- var runtimeType = value.GetType();
- var context = SerializeCore(value, runtimeType, options);
- try
- {
- context.WriteTo(writer);
- }
- finally
- {
- BinarySerializationContextPool.Return(context);
- }
- }
-
- ///
- /// Get the serialized size without allocating the final array.
- /// Useful for pre-allocating buffers.
- ///
- public static int GetSerializedSize(T value, AcBinarySerializerOptions options)
- {
- if (value == null) return 1;
-
- var runtimeType = value.GetType();
- var context = SerializeCore(value, runtimeType, options);
- try
- {
- return context.Position;
- }
- finally
- {
- BinarySerializationContextPool.Return(context);
- }
- }
-
- ///
- /// Serialize object and keep the pooled buffer for zero-copy consumers.
- /// Caller must dispose the returned result to release the buffer.
- ///
- public static BinarySerializationResult SerializeToPooledBuffer(T value, AcBinarySerializerOptions options)
- {
- if (value == null)
- {
- return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]);
- }
-
- var runtimeType = value.GetType();
- var context = SerializeCore(value, runtimeType, options);
- try
- {
- return context.DetachResult();
- }
- finally
- {
- BinarySerializationContextPool.Return(context);
- }
- }
-
- private static BinarySerializationContext SerializeCore(object value, Type runtimeType, AcBinarySerializerOptions options)
- {
- var context = BinarySerializationContextPool.Get(options);
- context.WriteHeaderPlaceholder();
-
- if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType))
- {
- ScanReferences(value, context, 0);
- }
-
- if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType))
- {
- RegisterMetadataForType(runtimeType, context);
- }
-
- WriteValue(value, runtimeType, context, 0);
- context.FinalizeHeaderSections();
- return context;
- }
-
- #endregion
-
- #region Reference Scanning
-
- private static void ScanReferences(object? value, BinarySerializationContext context, int depth)
- {
- if (value == null || depth > context.MaxDepth) return;
-
- var type = value.GetType();
- if (IsPrimitiveOrStringFast(type)) return;
- if (!context.TrackForScanning(value)) return;
-
- if (value is byte[]) return; // byte arrays are value types
-
- if (value is IDictionary dictionary)
- {
- foreach (DictionaryEntry entry in dictionary)
- {
- if (entry.Value != null)
- ScanReferences(entry.Value, context, depth + 1);
- }
- return;
- }
-
- if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
- {
- foreach (var item in enumerable)
- {
- if (item != null)
- ScanReferences(item, context, depth + 1);
- }
- return;
- }
-
- var metadata = GetTypeMetadata(type);
- foreach (var prop in metadata.Properties)
- {
- if (!context.ShouldSerializeProperty(value, prop))
- {
- continue;
- }
-
- var propValue = prop.GetValue(value);
- if (propValue != null)
- ScanReferences(propValue, context, depth + 1);
- }
- }
-
- #endregion
-
- #region Property Metadata Registration
-
- private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet? visited = null)
- {
- if (IsPrimitiveOrStringFast(type)) return;
-
- visited ??= new HashSet();
- if (!visited.Add(type)) return;
-
- if (IsDictionaryType(type, out var keyType, out var valueType))
- {
- if (keyType != null) RegisterMetadataForType(keyType, context, visited);
- if (valueType != null) RegisterMetadataForType(valueType, context, visited);
- return;
- }
-
- if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
- {
- var elementType = GetCollectionElementType(type);
- if (elementType != null)
- {
- RegisterMetadataForType(elementType, context, visited);
- }
- return;
- }
-
- var metadata = GetTypeMetadata(type);
- foreach (var prop in metadata.Properties)
- {
- if (!context.ShouldIncludePropertyInMetadata(prop))
- {
- continue;
- }
-
- context.RegisterPropertyName(prop.Name);
-
- if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType))
- {
- RegisterMetadataForType(nestedType, context, visited);
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool TryResolveNestedMetadataType(Type propertyType, out Type nestedType)
- {
- nestedType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
-
- if (IsPrimitiveOrStringFast(nestedType))
- return false;
-
- if (IsDictionaryType(nestedType, out var _, out var valueType) && valueType != null)
- {
- if (!IsPrimitiveOrStringFast(valueType))
- {
- nestedType = valueType;
- return true;
- }
- return false;
- }
-
- if (typeof(IEnumerable).IsAssignableFrom(nestedType) && !ReferenceEquals(nestedType, StringType))
- {
- var elementType = GetCollectionElementType(nestedType);
- if (elementType != null && !IsPrimitiveOrStringFast(elementType))
- {
- nestedType = elementType;
- return true;
- }
- return false;
- }
-
- return true;
- }
-
- #endregion
-
- #region Value Writing
-
- private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth)
- {
- if (value == null)
- {
- context.WriteByte(BinaryTypeCode.Null);
- return;
- }
-
- // Try writing as primitive first
- if (TryWritePrimitive(value, type, context))
- return;
-
- if (depth > context.MaxDepth)
- {
- context.WriteByte(BinaryTypeCode.Null);
- return;
- }
-
- // Check for object reference
- if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
- {
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteVarInt(refId);
- return;
- }
-
- // Handle byte arrays specially
- if (value is byte[] byteArray)
- {
- WriteByteArray(byteArray, context);
- return;
- }
-
- // Handle dictionaries
- if (value is IDictionary dictionary)
- {
- WriteDictionary(dictionary, context, depth);
- return;
- }
-
- // Handle collections/arrays
- if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
- {
- WriteArray(enumerable, type, context, depth);
- return;
- }
-
- // Handle complex objects
- WriteObject(value, type, context, depth);
- }
-
- ///
- /// Optimized primitive writer using TypeCode dispatch.
- /// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context)
- {
- // Fast path: check TypeCode first (handles most primitives)
- var typeCode = Type.GetTypeCode(type);
-
- switch (typeCode)
- {
- case TypeCode.Int32:
- WriteInt32((int)value, context);
- return true;
- case TypeCode.Int64:
- WriteInt64((long)value, context);
- return true;
- case TypeCode.Boolean:
- context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False);
- return true;
- case TypeCode.Double:
- WriteFloat64Unsafe((double)value, context);
- return true;
- case TypeCode.String:
- WriteString((string)value, context);
- return true;
- case TypeCode.Single:
- WriteFloat32Unsafe((float)value, context);
- return true;
- case TypeCode.Decimal:
- WriteDecimalUnsafe((decimal)value, context);
- return true;
- case TypeCode.DateTime:
- WriteDateTimeUnsafe((DateTime)value, context);
- return true;
- case TypeCode.Byte:
- context.WriteByte(BinaryTypeCode.UInt8);
- context.WriteByte((byte)value);
- return true;
- case TypeCode.Int16:
- WriteInt16Unsafe((short)value, context);
- return true;
- case TypeCode.UInt16:
- WriteUInt16Unsafe((ushort)value, context);
- return true;
- case TypeCode.UInt32:
- WriteUInt32((uint)value, context);
- return true;
- case TypeCode.UInt64:
- WriteUInt64((ulong)value, context);
- return true;
- case TypeCode.SByte:
- context.WriteByte(BinaryTypeCode.Int8);
- context.WriteByte(unchecked((byte)(sbyte)value));
- return true;
- case TypeCode.Char:
- WriteCharUnsafe((char)value, context);
- return true;
- }
-
- // Handle nullable types
- var underlyingType = Nullable.GetUnderlyingType(type);
- if (underlyingType != null)
- {
- return TryWritePrimitive(value, underlyingType, context);
- }
-
- // Handle special types by reference comparison (faster than type equality)
- if (ReferenceEquals(type, GuidType))
- {
- WriteGuidUnsafe((Guid)value, context);
- return true;
- }
- if (ReferenceEquals(type, DateTimeOffsetType))
- {
- WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context);
- return true;
- }
- if (ReferenceEquals(type, TimeSpanType))
- {
- WriteTimeSpanUnsafe((TimeSpan)value, context);
- return true;
- }
- if (type.IsEnum)
- {
- WriteEnum(value, context);
- return true;
- }
-
- return false;
- }
-
- #endregion
-
- #region Optimized Primitive Writers using MemoryMarshal
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteInt32(int value, BinarySerializationContext context)
- {
- if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
- {
- context.WriteByte(tiny);
- return;
- }
- context.WriteByte(BinaryTypeCode.Int32);
- context.WriteVarInt(value);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteInt64(long value, BinarySerializationContext context)
- {
- if (value >= int.MinValue && value <= int.MaxValue)
- {
- WriteInt32((int)value, context);
- return;
- }
- context.WriteByte(BinaryTypeCode.Int64);
- context.WriteVarLong(value);
- }
-
- ///
- /// Optimized float64 writer using direct memory copy.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteFloat64Unsafe(double value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.Float64);
- context.WriteRaw(value);
- }
-
- ///
- /// Optimized float32 writer using direct memory copy.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteFloat32Unsafe(float value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.Float32);
- context.WriteRaw(value);
- }
-
- ///
- /// Optimized decimal writer using direct memory copy of bits.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.Decimal);
- context.WriteDecimalBits(value);
- }
-
- ///
- /// Optimized DateTime writer.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.DateTime);
- context.WriteDateTimeBits(value);
- }
-
- ///
- /// Optimized Guid writer using direct memory copy.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.Guid);
- context.WriteGuidBits(value);
- }
-
- ///
- /// Optimized DateTimeOffset writer.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.DateTimeOffset);
- context.WriteDateTimeOffsetBits(value);
- }
-
- ///
- /// Optimized TimeSpan writer.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.TimeSpan);
- context.WriteRaw(value.Ticks);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteInt16Unsafe(short value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.Int16);
- context.WriteRaw(value);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.UInt16);
- context.WriteRaw(value);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteUInt32(uint value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.UInt32);
- context.WriteVarUInt(value);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteUInt64(ulong value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.UInt64);
- context.WriteVarULong(value);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteCharUnsafe(char value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.Char);
- context.WriteRaw(value);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteEnum(object value, BinarySerializationContext context
- )
- {
- var intValue = Convert.ToInt32(value);
- if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
- {
- context.WriteByte(BinaryTypeCode.Enum);
- context.WriteByte(tiny);
- return;
- }
- context.WriteByte(BinaryTypeCode.Enum);
- context.WriteByte(BinaryTypeCode.Int32);
- context.WriteVarInt(intValue);
- }
-
- ///
- /// Optimized string writer with span-based UTF8 encoding.
- /// Uses stackalloc for small strings to avoid allocations.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteString(string value, BinarySerializationContext context)
- {
- if (value.Length == 0)
- {
- context.WriteByte(BinaryTypeCode.StringEmpty);
- return;
- }
-
- if (context.UseStringInterning && value.Length >= context.MinStringInternLength)
- {
- var index = context.RegisterInternedString(value);
- context.WriteByte(BinaryTypeCode.StringInterned);
- context.WriteVarUInt((uint)index);
- return;
- }
-
- // Első előfordulás vagy nincs interning - sima string
- context.WriteByte(BinaryTypeCode.String);
- context.WriteStringUtf8(value);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteByteArray(byte[] value, BinarySerializationContext context)
- {
- context.WriteByte(BinaryTypeCode.ByteArray);
- context.WriteVarUInt((uint)value.Length);
- context.WriteBytes(value);
- }
-
- #endregion
-
- #region Complex Type Writers
-
- private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth)
- {
- context.WriteByte(BinaryTypeCode.Object);
-
- // Register object reference if needed
- if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
- {
- context.WriteVarInt(refId);
- context.MarkAsWritten(value, refId);
- }
- else if (context.UseReferenceHandling)
- {
- context.WriteVarInt(-1); // No ref ID
- }
-
- var metadata = GetTypeMetadata(type);
- var nextDepth = depth + 1;
- var properties = metadata.Properties;
- var propCount = properties.Length;
-
- const int StackThreshold = 64;
- byte[]? rentedStates = null;
- Span propertyStates = propCount <= StackThreshold
- ? stackalloc byte[propCount]
- : (rentedStates = context.RentPropertyStateBuffer(propCount)).AsSpan(0, propCount);
- propertyStates.Clear();
- var writtenCount = 0;
-
- for (var i = 0; i < propCount; i++)
- {
- var property = properties[i];
-
- if (!context.ShouldSerializeProperty(value, property) || IsPropertyDefaultOrNull(value, property))
- {
- propertyStates[i] = 0;
- continue;
- }
-
- propertyStates[i] = 1;
- writtenCount++;
- }
-
- context.WriteVarUInt((uint)writtenCount);
-
- for (var i = 0; i < propCount; i++)
- {
- if (propertyStates[i] == 0)
- continue;
-
- var prop = properties[i];
-
- if (context.UseMetadata)
- {
- var propIndex = context.GetPropertyNameIndex(prop.Name);
- context.WriteVarUInt((uint)propIndex);
- }
- else
- {
- context.WritePreencodedPropertyName(prop.NameUtf8);
- }
-
- WritePropertyValue(value, prop, context, nextDepth);
- }
-
- if (rentedStates != null)
- {
- context.ReturnPropertyStateBuffer(rentedStates);
- }
- }
-
- ///
- /// Checks if a property value is null or default without boxing for value types.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop)
- {
- switch (prop.AccessorType)
- {
- case PropertyAccessorType.Int32:
- return prop.GetInt32(obj) == 0;
- case PropertyAccessorType.Int64:
- return prop.GetInt64(obj) == 0L;
- case PropertyAccessorType.Boolean:
- return !prop.GetBoolean(obj);
- case PropertyAccessorType.Double:
- return prop.GetDouble(obj) == 0.0;
- case PropertyAccessorType.Single:
- return prop.GetSingle(obj) == 0f;
- case PropertyAccessorType.Decimal:
- return prop.GetDecimal(obj) == 0m;
- case PropertyAccessorType.Byte:
- return prop.GetByte(obj) == 0;
- case PropertyAccessorType.Int16:
- return prop.GetInt16(obj) == 0;
- case PropertyAccessorType.UInt16:
- return prop.GetUInt16(obj) == 0;
- case PropertyAccessorType.UInt32:
- return prop.GetUInt32(obj) == 0;
- case PropertyAccessorType.UInt64:
- return prop.GetUInt64(obj) == 0;
- case PropertyAccessorType.Guid:
- return prop.GetGuid(obj) == Guid.Empty;
- case PropertyAccessorType.Enum:
- return prop.GetEnumAsInt32(obj) == 0;
- case PropertyAccessorType.DateTime:
- // DateTime default is not typically skipped
- return false;
- default:
- // Object type - use regular getter
- var value = prop.GetValue(obj);
- if (value == null) return true;
- if (prop.TypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value);
- return false;
- }
- }
-
- ///
- /// Writes a property value using typed getters to avoid boxing.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth)
- {
- switch (prop.AccessorType)
- {
- case PropertyAccessorType.Int32:
- WriteInt32(prop.GetInt32(obj), context);
- return;
- case PropertyAccessorType.Int64:
- WriteInt64(prop.GetInt64(obj), context);
- return;
- case PropertyAccessorType.Boolean:
- context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False);
- return;
- case PropertyAccessorType.Double:
- WriteFloat64Unsafe(prop.GetDouble(obj), context);
- return;
- case PropertyAccessorType.Single:
- WriteFloat32Unsafe(prop.GetSingle(obj), context);
- return;
- case PropertyAccessorType.Decimal:
- WriteDecimalUnsafe(prop.GetDecimal(obj), context);
- return;
- case PropertyAccessorType.DateTime:
- WriteDateTimeUnsafe(prop.GetDateTime(obj), context);
- return;
- case PropertyAccessorType.Byte:
- context.WriteByte(BinaryTypeCode.UInt8);
- context.WriteByte(prop.GetByte(obj));
- return;
- case PropertyAccessorType.Int16:
- WriteInt16Unsafe(prop.GetInt16(obj), context);
- return;
- case PropertyAccessorType.UInt16:
- WriteUInt16Unsafe(prop.GetUInt16(obj), context);
- return;
- case PropertyAccessorType.UInt32:
- WriteUInt32(prop.GetUInt32(obj), context);
- return;
- case PropertyAccessorType.UInt64:
- WriteUInt64(prop.GetUInt64(obj), context);
- return;
- case PropertyAccessorType.Guid:
- WriteGuidUnsafe(prop.GetGuid(obj), context);
- return;
- case PropertyAccessorType.Enum:
- var enumValue = prop.GetEnumAsInt32(obj);
- if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
- {
- context.WriteByte(BinaryTypeCode.Enum);
- context.WriteByte(tiny);
- }
- else
- {
- context.WriteByte(BinaryTypeCode.Enum);
- context.WriteByte(BinaryTypeCode.Int32);
- context.WriteVarInt(enumValue);
- }
- return;
- default:
- // Fallback to object getter for reference types
- var value = prop.GetValue(obj);
- WriteValue(value, prop.PropertyType, context, depth);
- return;
- }
- }
-
- #endregion
-
- #region Serialization Result
-
- public sealed class BinarySerializationResult : IDisposable
- {
- private readonly bool _pooled;
- private bool _disposed;
-
- internal BinarySerializationResult(byte[] buffer, int length, bool pooled)
- {
- Buffer = buffer;
- Length = length;
- _pooled = pooled;
- }
-
- public byte[] Buffer { get; }
- public int Length { get; }
- public ReadOnlySpan Span => Buffer.AsSpan(0, Length);
- public ReadOnlyMemory Memory => new(Buffer, 0, Length);
-
- public byte[] ToArray()
- {
- var result = GC.AllocateUninitializedArray(Length);
- Buffer.AsSpan(0, Length).CopyTo(result);
- return result;
- }
-
- public void Dispose()
- {
- if (_disposed) return;
- _disposed = true;
-
- if (_pooled)
- {
- ArrayPool.Shared.Return(Buffer);
- }
- }
-
- internal static BinarySerializationResult FromImmutable(byte[] buffer)
- => new(buffer, buffer.Length, pooled: false);
- }
-
- #endregion
-
- #region Specialized Array Writers
-
- ///
- /// Optimized array writer with specialized paths for primitive arrays.
- ///
- private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth)
- {
- context.WriteByte(BinaryTypeCode.Array);
- var nextDepth = depth + 1;
-
- // Optimized path for primitive arrays
- var elementType = GetCollectionElementType(type);
- if (elementType != null && type.IsArray)
- {
- if (TryWritePrimitiveArray(enumerable, elementType, context))
- return;
- }
-
- // For IList, we can write the count directly
- if (enumerable is IList list)
- {
- var count = list.Count;
- context.WriteVarUInt((uint)count);
- for (var i = 0; i < count; i++)
- {
- var item = list[i];
- var itemType = item?.GetType() ?? typeof(object);
- WriteValue(item, itemType, context, nextDepth);
- }
- return;
- }
-
- // For other IEnumerable, collect first
- var items = new List