Refactor: Add high-perf JSON serializer & merge support

- Introduced AcJsonSerializer/AcJsonDeserializer in AyCode.Core.Serializers.Jsons, optimized for IId<T> reference and circular reference handling.
- Added AcJsonSerializerOptions/AcSerializerOptions for configurable reference handling and max depth.
- Implemented fast-path streaming (Utf8JsonReader/Writer) with fallback to DOM for reference scenarios.
- Added type metadata/property accessor caching for performance.
- Provided robust object/collection population with merge semantics for IId<T> collections.
- Added AcJsonDeserializationException for detailed error reporting.
- Implemented UnifiedMergeContractResolver for Newtonsoft.Json, supporting JsonNoMergeCollectionAttribute to control merge behavior.
- Added IdAwareCollectionMergeConverter<TItem, TId> for merging IId<T> collections by ID.
- Included helpers for ID extraction and semantic ID generation.
- Added DeepPopulateWithMerge extension for deep merging.
- Optimized with frozen dictionaries, pre-encoded property names, and context pooling.
- Ensured compatibility with both System.Text.Json and Newtonsoft.Json.
This commit is contained in:
Loretta 2025-12-14 19:34:49 +01:00
parent b17c2df6c2
commit bc30a3aede
35 changed files with 3246 additions and 2578 deletions

View File

@ -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
}
}
/// <summary>
/// Quick benchmark comparing AcBinary vs MessagePack with tabular output.
/// Tests: WithRef, NoRef, Serialize, Deserialize, Populate, Merge
/// </summary>
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<TestOrder>(acBinaryWithRef);
var acWithRefDeserialize = sw.Elapsed.TotalMilliseconds;
// AcBinary NoRef Deserialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryNoRef);
var acNoRefDeserialize = sw.Elapsed.TotalMilliseconds;
// MessagePack Deserialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = MessagePackSerializer.Deserialize<TestOrder>(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";

View File

@ -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;

View File

@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using BenchmarkDotNet.Attributes;
using MessagePack;

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -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;

View File

@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
namespace AyCode.Core.Tests.Serialization;

View File

@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
namespace AyCode.Core.Tests.serialization;

View File

@ -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");
}

View File

@ -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;

View File

@ -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;
/// <summary>
/// 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
/// </summary>
public static class AcBinarySerializer
{
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> 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
/// <summary>
/// Serialize object to binary with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Serialize<T>(T value) => Serialize(value, AcBinarySerializerOptions.Default);
/// <summary>
/// Serialize object to binary with specified options.
/// </summary>
public static byte[] Serialize<T>(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);
}
}
/// <summary>
/// Serialize object to an IBufferWriter for zero-copy scenarios.
/// This avoids the final ToArray() allocation by writing directly to the caller's buffer.
/// </summary>
public static void Serialize<T>(T value, IBufferWriter<byte> 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);
}
}
/// <summary>
/// Get the serialized size without allocating the final array.
/// Useful for pre-allocating buffers.
/// </summary>
public static int GetSerializedSize<T>(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);
}
}
/// <summary>
/// Serialize object and keep the pooled buffer for zero-copy consumers.
/// Caller must dispose the returned result to release the buffer.
/// </summary>
public static BinarySerializationResult SerializeToPooledBuffer<T>(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<Type>? visited = null)
{
if (IsPrimitiveOrStringFast(type)) return;
visited ??= new HashSet<Type>();
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);
}
/// <summary>
/// Optimized primitive writer using TypeCode dispatch.
/// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata.
/// </summary>
[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);
}
/// <summary>
/// Optimized float64 writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat64Unsafe(double value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Float64);
context.WriteRaw(value);
}
/// <summary>
/// Optimized float32 writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat32Unsafe(float value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Float32);
context.WriteRaw(value);
}
/// <summary>
/// Optimized decimal writer using direct memory copy of bits.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Decimal);
context.WriteDecimalBits(value);
}
/// <summary>
/// Optimized DateTime writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.DateTime);
context.WriteDateTimeBits(value);
}
/// <summary>
/// Optimized Guid writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Guid);
context.WriteGuidBits(value);
}
/// <summary>
/// Optimized DateTimeOffset writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.DateTimeOffset);
context.WriteDateTimeOffsetBits(value);
}
/// <summary>
/// Optimized TimeSpan writer.
/// </summary>
[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);
}
/// <summary>
/// Optimized string writer with span-based UTF8 encoding.
/// Uses stackalloc for small strings to avoid allocations.
/// </summary>
[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<byte> 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);
}
}
/// <summary>
/// Checks if a property value is null or default without boxing for value types.
/// </summary>
[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;
}
}
/// <summary>
/// Writes a property value using typed getters to avoid boxing.
/// </summary>
[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<byte> Span => Buffer.AsSpan(0, Length);
public ReadOnlyMemory<byte> Memory => new(Buffer, 0, Length);
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(Length);
Buffer.AsSpan(0, Length).CopyTo(result);
return result;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_pooled)
{
ArrayPool<byte>.Shared.Return(Buffer);
}
}
internal static BinarySerializationResult FromImmutable(byte[] buffer)
=> new(buffer, buffer.Length, pooled: false);
}
#endregion
#region Specialized Array Writers
/// <summary>
/// Optimized array writer with specialized paths for primitive arrays.
/// </summary>
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<object?>();
foreach (var item in enumerable)
{
items.Add(item);
}
context.WriteVarUInt((uint)items.Count);
foreach (var item in items)
{
var itemType = item?.GetType() ?? typeof(object);
WriteValue(item, itemType, context, nextDepth);
}
}
/// <summary>
/// Specialized array writer for primitive arrays using bulk memory operations.
/// Optimized for Blazor Hybrid compatibility (WASM, Android, Windows, iOS).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context)
{
// Int32 array - very common case
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
{
context.WriteVarUInt((uint)intArray.Length);
context.WriteInt32ArrayOptimized(intArray);
return true;
}
// Double array - bulk write as raw bytes
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
{
context.WriteVarUInt((uint)doubleArray.Length);
context.WriteDoubleArrayBulk(doubleArray);
return true;
}
// Long array
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
{
context.WriteVarUInt((uint)longArray.Length);
context.WriteLongArrayOptimized(longArray);
return true;
}
// Float array - bulk write as raw bytes
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
{
context.WriteVarUInt((uint)floatArray.Length);
context.WriteFloatArrayBulk(floatArray);
return true;
}
// Bool array - pack as bytes
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
{
context.WriteVarUInt((uint)boolArray.Length);
for (var i = 0; i < boolArray.Length; i++)
{
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
}
return true;
}
// Guid array - bulk write
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
{
context.WriteVarUInt((uint)guidArray.Length);
context.WriteGuidArrayBulk(guidArray);
return true;
}
// Decimal array
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
{
context.WriteVarUInt((uint)decimalArray.Length);
for (var i = 0; i < decimalArray.Length; i++)
{
WriteDecimalUnsafe(decimalArray[i], context);
}
return true;
}
// DateTime array
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
{
context.WriteVarUInt((uint)dateTimeArray.Length);
for (var i = 0; i < dateTimeArray.Length; i++)
{
WriteDateTimeUnsafe(dateTimeArray[i], context);
}
return true;
}
// String array - common case
if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray)
{
context.WriteVarUInt((uint)stringArray.Length);
for (var i = 0; i < stringArray.Length; i++)
{
var s = stringArray[i];
if (s == null)
context.WriteByte(BinaryTypeCode.Null);
else
WriteString(s, context);
}
return true;
}
return false;
}
private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth)
{
context.WriteByte(BinaryTypeCode.Dictionary);
context.WriteVarUInt((uint)dictionary.Count);
var nextDepth = depth + 1;
foreach (DictionaryEntry entry in dictionary)
{
// Write key
var keyType = entry.Key?.GetType() ?? typeof(object);
WriteValue(entry.Key, keyType, context, nextDepth);
// Write value
var valueType = entry.Value?.GetType() ?? typeof(object);
WriteValue(entry.Value, valueType, context, nextDepth);
}
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryTypeMetadata GetTypeMetadata(Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPrimitiveOrStringFast(Type type)
{
if (type.IsPrimitive || ReferenceEquals(type, StringType))
{
return true;
}
if (ReferenceEquals(type, DecimalType) ||
ReferenceEquals(type, DateTimeType) ||
ReferenceEquals(type, GuidType) ||
ReferenceEquals(type, DateTimeOffsetType) ||
ReferenceEquals(type, TimeSpanType))
{
return true;
}
if (type.IsEnum)
{
return true;
}
var underlying = Nullable.GetUnderlyingType(type);
return underlying != null && IsPrimitiveOrStringFast(underlying);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType)
{
if (type.IsGenericType)
{
var definition = type.GetGenericTypeDefinition();
if (definition == typeof(Dictionary<,>) || definition == typeof(IDictionary<,>))
{
var args = type.GetGenericArguments();
keyType = args[0];
valueType = args[1];
return true;
}
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
var args = iface.GetGenericArguments();
keyType = args[0];
valueType = args[1];
return true;
}
}
keyType = null;
valueType = null;
return typeof(IDictionary).IsAssignableFrom(type);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Type? GetCollectionElementType(Type type)
{
if (type.IsArray)
{
return type.GetElementType();
}
if (type.IsGenericType)
{
var args = type.GetGenericArguments();
if (args.Length == 1)
{
return args[0];
}
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return iface.GetGenericArguments()[0];
}
}
return null;
}
internal sealed class BinaryTypeMetadata
{
public BinaryPropertyAccessor[] Properties { get; }
public BinaryTypeMetadata(Type type)
{
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead &&
p.GetIndexParameters().Length == 0 &&
!HasJsonIgnoreAttribute(p))
.Select(p => new BinaryPropertyAccessor(p))
.ToArray();
}
}
internal sealed class BinaryPropertyAccessor
{
public readonly string Name;
public readonly byte[] NameUtf8;
public readonly Type PropertyType;
public readonly TypeCode TypeCode;
public readonly Type DeclaringType;
private readonly Func<object, object?> _objectGetter;
private readonly Delegate? _typedGetter;
private readonly PropertyAccessorType _accessorType;
public BinaryPropertyAccessor(PropertyInfo prop)
{
Name = prop.Name;
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
DeclaringType = prop.DeclaringType!;
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
TypeCode = Type.GetTypeCode(PropertyType);
(_typedGetter, _accessorType) = CreateTypedGetter(DeclaringType, prop);
_objectGetter = CreateObjectGetter(DeclaringType, prop);
}
public PropertyAccessorType AccessorType => _accessorType;
public Func<object, object?> ObjectGetter => _objectGetter;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object obj) => _objectGetter(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
private static (Delegate?, PropertyAccessorType) CreateTypedGetter(Type declaringType, PropertyInfo prop)
{
var propType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(propType);
if (underlying != null)
{
return (null, PropertyAccessorType.Object);
}
if (propType.IsEnum)
{
return (CreateEnumGetter(declaringType, prop), PropertyAccessorType.Enum);
}
if (ReferenceEquals(propType, GuidType))
{
return (CreateTypedGetterDelegate<Guid>(declaringType, prop), PropertyAccessorType.Guid);
}
var typeCode = Type.GetTypeCode(propType);
return typeCode switch
{
TypeCode.Int32 => (CreateTypedGetterDelegate<int>(declaringType, prop), PropertyAccessorType.Int32),
TypeCode.Int64 => (CreateTypedGetterDelegate<long>(declaringType, prop), PropertyAccessorType.Int64),
TypeCode.Boolean => (CreateTypedGetterDelegate<bool>(declaringType, prop), PropertyAccessorType.Boolean),
TypeCode.Double => (CreateTypedGetterDelegate<double>(declaringType, prop), PropertyAccessorType.Double),
TypeCode.Single => (CreateTypedGetterDelegate<float>(declaringType, prop), PropertyAccessorType.Single),
TypeCode.Decimal => (CreateTypedGetterDelegate<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
TypeCode.DateTime => (CreateTypedGetterDelegate<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
TypeCode.Byte => (CreateTypedGetterDelegate<byte>(declaringType, prop), PropertyAccessorType.Byte),
TypeCode.Int16 => (CreateTypedGetterDelegate<short>(declaringType, prop), PropertyAccessorType.Int16),
TypeCode.UInt16 => (CreateTypedGetterDelegate<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
TypeCode.UInt32 => (CreateTypedGetterDelegate<uint>(declaringType, prop), PropertyAccessorType.UInt32),
TypeCode.UInt64 => (CreateTypedGetterDelegate<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
_ => (null, PropertyAccessorType.Object)
};
}
private static Delegate CreateEnumGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var convertToInt = Expression.Convert(propAccess, typeof(int));
return Expression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
}
private static Func<object, TProperty> CreateTypedGetterDelegate<TProperty>(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var convertExpr = Expression.Convert(propAccess, typeof(TProperty));
return Expression.Lambda<Func<object, TProperty>>(convertExpr, objParam).Compile();
}
private static Func<object, object?> CreateObjectGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
}
internal enum PropertyAccessorType : byte
{
Object = 0,
Int32,
Int64,
Boolean,
Double,
Single,
Decimal,
DateTime,
Byte,
Int16,
UInt16,
UInt32,
UInt64,
Guid,
Enum
}
#endregion
#region Context Pool
private static class BinarySerializationContextPool
{
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
{
if (Pool.TryDequeue(out var context))
{
context.Reset(options);
return context;
}
return new BinarySerializationContext(options);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(BinarySerializationContext context)
{
if (Pool.Count < MaxPoolSize)
{
context.Clear();
Pool.Enqueue(context);
}
else
{
context.Dispose();
}
}
}
#endregion
#region Serialization Context
internal sealed class BinarySerializationContext : IDisposable
{
private byte[] _buffer;
private int _position;
private int _initialBufferSize;
private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512;
private const int InitialInternCapacity = 32;
private const int InitialPropertyNameCapacity = 32;
// Reference handling
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, int>? _writtenRefs;
private HashSet<object>? _multiReferenced;
private int _nextRefId;
// String interning
private Dictionary<string, int>? _internedStrings;
private List<string>? _internedStringList;
// Property name table
private Dictionary<string, int>? _propertyNames;
private List<string>? _propertyNameList;
private int[]? _propertyIndexBuffer;
private byte[]? _propertyStateBuffer;
public bool UseReferenceHandling { get; private set; }
public bool UseStringInterning { get; private set; }
public bool UseMetadata { get; private set; }
public byte MaxDepth { get; private set; }
public byte MinStringInternLength { get; private set; }
public BinaryPropertyFilter? PropertyFilter { get; private set; }
public int Position => _position;
public BinarySerializationContext(AcBinarySerializerOptions options)
{
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
Reset(options);
}
public void Reset(AcBinarySerializerOptions options)
{
_position = 0;
_nextRefId = 1;
UseReferenceHandling = options.UseReferenceHandling;
UseStringInterning = options.UseStringInterning;
UseMetadata = options.UseMetadata;
MaxDepth = options.MaxDepth;
MinStringInternLength = options.MinStringInternLength;
PropertyFilter = options.PropertyFilter;
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
if (_buffer.Length < _initialBufferSize)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
}
}
public void Clear()
{
_position = 0;
_nextRefId = 1;
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
_propertyNameList?.Clear();
_internedStringList?.Clear();
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
{
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
}
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
}
public void Dispose()
{
if (_buffer != null)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null!;
}
if (_propertyIndexBuffer != null)
{
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
}
if (_propertyStateBuffer != null)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
}
#region String Interning
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterInternedString(string value)
{
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
_internedStringList ??= new List<string>(InitialInternCapacity);
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
if (exists)
{
return index;
}
index = _internedStringList.Count;
_internedStringList.Add(value);
return index;
}
#endregion
#region Property Name Table
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterPropertyName(string name)
{
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
if (!_propertyNames.ContainsKey(name))
{
var index = _propertyNameList.Count;
_propertyNames[name] = index;
_propertyNameList.Add(name);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPropertyNameIndex(string name)
{
return _propertyNames != null && _propertyNames.TryGetValue(name, out var index) ? index : -1;
}
#endregion
#region Property State Buffer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] RentPropertyStateBuffer(int size)
{
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size)
{
return _propertyStateBuffer;
}
if (_propertyStateBuffer != null)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
}
_propertyStateBuffer = ArrayPool<byte>.Shared.Rent(size);
return _propertyStateBuffer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyStateBuffer(byte[] buffer)
{
// Buffer stays cached in _propertyStateBuffer for reuse
}
#endregion
#region Output
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(_position);
_buffer.AsSpan(0, _position).CopyTo(result);
return result;
}
public void WriteTo(IBufferWriter<byte> writer)
{
var span = writer.GetSpan(_position);
_buffer.AsSpan(0, _position).CopyTo(span);
writer.Advance(_position);
}
public BinarySerializationResult DetachResult()
{
var resultBuffer = _buffer;
var resultLength = _position;
// Get a new buffer for this context
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
_position = 0;
return new BinarySerializationResult(resultBuffer, resultLength, pooled: true);
}
#endregion
#region Helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity)
where TKey : notnull
{
if (dict == null) return;
dict.Clear();
if (dict.EnsureCapacity(0) > maxCapacity)
{
dict.TrimExcess();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
{
if (set == null) return;
set.Clear();
if (set.EnsureCapacity(0) > maxCapacity)
{
set.TrimExcess();
}
}
#endregion
#region Property Filtering
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property)
{
if (PropertyFilter == null)
{
return true;
}
var context = new BinaryPropertyFilterContext(
instance,
property.DeclaringType,
property.Name,
property.PropertyType,
property.ObjectGetter);
return PropertyFilter(context);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldIncludePropertyInMetadata(BinaryPropertyAccessor property)
{
if (PropertyFilter == null)
{
return true;
}
var context = new BinaryPropertyFilterContext(
null,
property.DeclaringType,
property.Name,
property.PropertyType,
null);
return PropertyFilter(context);
}
#endregion
#region Optimized Buffer Writing
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int additionalBytes)
{
var required = _position + additionalBytes;
if (required <= _buffer.Length)
{
return;
}
GrowBuffer(required);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowBuffer(int required)
{
var newSize = Math.Max(_buffer.Length * 2, required);
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = newBuffer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
EnsureCapacity(1);
_buffer[_position++] = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(ReadOnlySpan<byte> data)
{
EnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
EnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeBits(DateTime value)
{
EnsureCapacity(9);
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind;
_position += 9;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeOffsetBits(DateTimeOffset value)
{
EnsureCapacity(10);
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt(int value)
{
EnsureCapacity(5);
var encoded = (uint)((value << 1) ^ (value >> 31));
WriteVarUIntInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt(uint value)
{
EnsureCapacity(5);
WriteVarUIntInternal(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteVarUIntInternal(uint value)
{
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarLong(long value)
{
EnsureCapacity(10);
var encoded = (ulong)((value << 1) ^ (value >> 63));
WriteVarULongInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value)
{
EnsureCapacity(10);
WriteVarULongInternal(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteVarULongInternal(ulong value)
{
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringUtf8(string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
{
WriteByte(BinaryTypeCode.String);
WriteVarUInt((uint)utf8Name.Length);
WriteBytes(utf8Name);
}
#endregion
#region Bulk Array Writers
public void WriteInt32ArrayOptimized(int[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(value);
}
}
}
public void WriteLongArrayOptimized(long[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (value >= int.MinValue && value <= int.MaxValue)
{
var intValue = (int)value;
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(intValue);
}
}
else
{
WriteByte(BinaryTypeCode.Int64);
WriteVarLong(value);
}
}
}
public void WriteDoubleArrayBulk(double[] array)
{
EnsureCapacity(array.Length * 9);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float64;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 8;
}
}
public void WriteFloatArrayBulk(float[] array)
{
EnsureCapacity(array.Length * 5);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float32;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 4;
}
}
public void WriteGuidArrayBulk(Guid[] array)
{
EnsureCapacity(array.Length * 17);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Guid;
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
}
#endregion
#region Header and Metadata
private int _headerPosition;
public void WriteHeaderPlaceholder()
{
EnsureCapacity(2);
_headerPosition = _position;
_position += 2;
}
public void FinalizeHeaderSections()
{
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
// ArrayBufferWriter requires initialCapacity > 0
var estimatedCapacity = hasPropertyNames ? _propertyNameList!.Count * 8 : 16;
if (hasInternTable && _internedStringList != null)
{
estimatedCapacity += _internedStringList.Count * 8;
}
var headerWriter = new ArrayBufferWriter<byte>(Math.Max(estimatedCapacity, 16));
if (hasPropertyNames)
{
WriteHeaderVarUInt(headerWriter, (uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
{
WriteHeaderString(headerWriter, name);
}
}
if (hasInternTable)
{
WriteHeaderVarUInt(headerWriter, (uint)_internedStringList!.Count);
foreach (var value in _internedStringList)
{
WriteHeaderString(headerWriter, value);
}
}
var headerPayload = headerWriter.WrittenSpan;
if (headerPayload.Length > 0)
{
EnsureCapacity(headerPayload.Length);
var bodyLength = _position - (_headerPosition + 2);
if (bodyLength > 0)
{
Array.Copy(_buffer, _headerPosition + 2, _buffer, _headerPosition + 2 + headerPayload.Length, bodyLength);
}
headerPayload.CopyTo(_buffer.AsSpan(_headerPosition + 2));
_position += headerPayload.Length;
}
byte flags = BinaryTypeCode.HeaderFlagsBase;
if (hasPropertyNames)
{
flags |= BinaryTypeCode.HeaderFlag_Metadata;
}
if (UseReferenceHandling)
{
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
}
if (hasInternTable)
{
flags |= BinaryTypeCode.HeaderFlag_StringInternTable;
}
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
_buffer[_headerPosition + 1] = flags;
}
private static void WriteHeaderVarUInt(ArrayBufferWriter<byte> writer, uint value)
{
var span = writer.GetSpan(5);
var index = 0;
while (value >= 0x80)
{
span[index++] = (byte)(value | 0x80);
value >>= 7;
}
span[index++] = (byte)value;
writer.Advance(index);
}
private static void WriteHeaderString(ArrayBufferWriter<byte> writer, string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteHeaderVarUInt(writer, (uint)byteCount);
var span = writer.GetSpan(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), span);
writer.Advance(byteCount);
}
#endregion
#region Reference Handling
private const int InitialReferenceCapacity = 16;
private const int InitialMultiRefCapacity = 8;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanning(object obj)
{
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
if (exists)
{
count++;
_multiReferenced.Add(obj);
return false;
}
count = 1;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteRef(object obj, out int refId)
{
if (_multiReferenced != null && _multiReferenced.Contains(obj))
{
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
if (!_writtenRefs.ContainsKey(obj))
{
refId = _nextRefId++;
return true;
}
}
refId = 0;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, int refId)
{
_writtenRefs![obj] = refId;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRef(object obj, out int refId)
{
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
{
return true;
}
refId = 0;
return false;
}
#endregion
}
#endregion
}

View File

@ -5,9 +5,12 @@ using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text;
using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using static AyCode.Core.Extensions.JsonUtilities;
using static AyCode.Core.Helpers.JsonUtilities;
using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
namespace AyCode.Core.Extensions;

View File

@ -2,14 +2,14 @@ using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Jsons;
using Newtonsoft.Json;
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Helpers;
/// <summary>
/// Cached result for IId type info lookup.

View File

@ -1,8 +1,6 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection;
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Helpers;
public static class PropertyHelper
{

View File

@ -0,0 +1,405 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
internal ref struct BinaryDeserializationContext
{
private readonly ReadOnlySpan<byte> _buffer;
private int _position;
private List<string>? _internedStrings;
private List<string>? _propertyNames;
private Dictionary<int, object>? _objectReferences;
private readonly byte _minStringInternLength;
public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; }
public bool IsMergeMode { readonly get; set; }
public bool RemoveOrphanedItems { readonly get; set; }
public bool IsAtEnd => _position >= _buffer.Length;
public int Position => _position;
public byte MinStringInternLength => _minStringInternLength;
public BinaryDeserializationContext(ReadOnlySpan<byte> data)
{
_buffer = data;
_position = 0;
_internedStrings = null;
_propertyNames = null;
_objectReferences = null;
HasMetadata = false;
HasReferenceHandling = false;
IsMergeMode = false;
RemoveOrphanedItems = false;
_minStringInternLength = AcBinarySerializerOptions.Default.MinStringInternLength;
}
public void ReadHeader()
{
if (_buffer.Length < 2)
{
throw new AcBinaryDeserializationException("Binary payload is too short to contain a header.");
}
var version = ReadByteInternal();
if (version != AcBinarySerializerOptions.FormatVersion)
{
throw new AcBinaryDeserializationException(
$"Unsupported binary format version '{version}'. Expected '{AcBinarySerializerOptions.FormatVersion}'.",
_position - 1);
}
var marker = ReadByteInternal();
var hasPropertyTable = false;
var hasInternTable = false;
if (marker == BinaryTypeCode.MetadataHeader)
{
hasPropertyTable = true;
HasReferenceHandling = true;
}
else if (marker == BinaryTypeCode.NoMetadataHeader)
{
HasReferenceHandling = true;
}
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
{
var flags = (byte)(marker & 0x0F);
hasPropertyTable = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
hasInternTable = (flags & BinaryTypeCode.HeaderFlag_StringInternTable) != 0;
}
else
{
throw new AcBinaryDeserializationException(
$"Unsupported binary header marker '{marker}'.",
_position - 1);
}
HasMetadata = hasPropertyTable;
if (hasPropertyTable)
{
var propertyCount = (int)ReadVarUInt();
_propertyNames = new List<string>(propertyCount);
for (var i = 0; i < propertyCount; i++)
{
_propertyNames.Add(ReadHeaderString());
}
}
if (hasInternTable)
{
var internCount = (int)ReadVarUInt();
_internedStrings = new List<string>(internCount);
for (var i = 0; i < internCount; i++)
{
_internedStrings.Add(ReadHeaderString());
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte() => ReadByteInternal();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte ReadByteInternal()
{
if (_position >= _buffer.Length)
{
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
}
return _buffer[_position++];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte PeekByte()
{
if (_position >= _buffer.Length)
{
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
}
return _buffer[_position];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short ReadInt16Unsafe()
{
EnsureAvailable(2);
var value = BinaryPrimitives.ReadInt16LittleEndian(_buffer.Slice(_position, 2));
_position += 2;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort ReadUInt16Unsafe()
{
EnsureAvailable(2);
var value = BinaryPrimitives.ReadUInt16LittleEndian(_buffer.Slice(_position, 2));
_position += 2;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public char ReadCharUnsafe()
{
EnsureAvailable(2);
var value = (char)BinaryPrimitives.ReadUInt16LittleEndian(_buffer.Slice(_position, 2));
_position += 2;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float ReadSingleUnsafe()
{
EnsureAvailable(4);
var bits = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
_position += 4;
return BitConverter.Int32BitsToSingle(bits);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double ReadDoubleUnsafe()
{
EnsureAvailable(8);
var bits = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(_position, 8));
_position += 8;
return BitConverter.Int64BitsToDouble(bits);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public decimal ReadDecimalUnsafe()
{
EnsureAvailable(16);
var ints = MemoryMarshal.Cast<byte, int>(_buffer.Slice(_position, 16));
var lo = ints[0];
var mid = ints[1];
var hi = ints[2];
var flags = ints[3];
var isNegative = (flags & unchecked((int)0x80000000)) != 0;
var scale = (byte)((flags >> 16) & 0x7F);
_position += 16;
return new decimal(lo, mid, hi, isNegative, scale);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTime ReadDateTimeUnsafe()
{
EnsureAvailable(9);
var ticks = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(_position, 8));
var kind = (DateTimeKind)_buffer[_position + 8];
_position += 9;
return new DateTime(ticks, kind);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTimeOffset ReadDateTimeOffsetUnsafe()
{
EnsureAvailable(10);
var utcTicks = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(_position, 8));
var offsetMinutes = BinaryPrimitives.ReadInt16LittleEndian(_buffer.Slice(_position + 8, 2));
_position += 10;
var utcValue = new DateTime(utcTicks, DateTimeKind.Utc);
return new DateTimeOffset(utcValue).ToOffset(TimeSpan.FromMinutes(offsetMinutes));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TimeSpan ReadTimeSpanUnsafe()
{
EnsureAvailable(8);
var ticks = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(_position, 8));
_position += 8;
return new TimeSpan(ticks);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid ReadGuidUnsafe()
{
EnsureAvailable(16);
var value = new Guid(_buffer.Slice(_position, 16));
_position += 16;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ReadVarInt()
{
var raw = ReadVarUInt();
var temp = (int)raw;
var value = (temp >> 1) ^ -(temp & 1);
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint ReadVarUInt()
{
uint value = 0;
var shift = 0;
while (true)
{
var b = ReadByteInternal();
value |= (uint)(b & 0x7F) << shift;
if ((b & 0x80) == 0)
{
break;
}
shift += 7;
if (shift > 35)
{
throw new AcBinaryDeserializationException("Invalid VarUInt encoding.", _position);
}
}
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long ReadVarLong()
{
var raw = ReadVarULong();
var value = (long)(raw >> 1) ^ -((long)raw & 1);
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong ReadVarULong()
{
ulong value = 0;
var shift = 0;
while (true)
{
var b = ReadByteInternal();
value |= (ulong)(b & 0x7F) << shift;
if ((b & 0x80) == 0)
{
break;
}
shift += 7;
if (shift > 70)
{
throw new AcBinaryDeserializationException("Invalid VarULong encoding.", _position);
}
}
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] ReadBytes(int length)
{
if (length == 0)
{
return Array.Empty<byte>();
}
EnsureAvailable(length);
var result = GC.AllocateUninitializedArray<byte>(length);
_buffer.Slice(_position, length).CopyTo(result);
_position += length;
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadStringUtf8(int length)
{
if (length == 0)
{
return string.Empty;
}
EnsureAvailable(length);
var value = Utf8NoBom.GetString(_buffer.Slice(_position, length));
_position += length;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Skip(int count)
{
EnsureAvailable(count);
_position += count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterInternedString(string value)
{
_internedStrings ??= new List<string>();
_internedStrings.Add(value);
return _internedStrings.Count - 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetInternedString(int index)
{
if (_internedStrings == null || (uint)index >= (uint)_internedStrings.Count)
{
throw new AcBinaryDeserializationException($"Invalid interned string index '{index}'.", _position);
}
return _internedStrings[index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetPropertyName(int index)
{
if (_propertyNames == null || (uint)index >= (uint)_propertyNames.Count)
{
throw new AcBinaryDeserializationException($"Invalid property metadata index '{index}'.", _position);
}
return _propertyNames[index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(int refId, object instance)
{
if (refId <= 0)
{
return;
}
_objectReferences ??= new Dictionary<int, object>(16);
_objectReferences[refId] = instance;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetReferencedObject(int refId)
{
if (refId <= 0)
{
return null;
}
if (_objectReferences == null || !_objectReferences.TryGetValue(refId, out var value))
{
throw new AcBinaryDeserializationException($"Unknown object reference id '{refId}'.", _position);
}
return value;
}
private void EnsureAvailable(int length)
{
if (_position > _buffer.Length - length)
{
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
}
}
private string ReadHeaderString()
{
var byteLength = (int)ReadVarUInt();
return ReadStringUtf8(byteLength);
}
}
}

View File

@ -0,0 +1,163 @@
using System;
using System.Collections;
using System.Collections.Frozen;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
internal sealed class BinaryDeserializeTypeMetadata
{
private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _properties;
public BinaryPropertySetterInfo[] PropertiesArray { get; }
public Func<object>? CompiledConstructor { get; }
public BinaryDeserializeTypeMetadata(Type type)
{
PropertiesArray = type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(static p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0 &&
p.GetMethod is { IsPublic: true } &&
p.SetMethod is { IsPublic: true } &&
!HasJsonIgnoreAttribute(p))
.Select(static p => new BinaryPropertySetterInfo(p))
.ToArray();
_properties = PropertiesArray.Length == 0
? FrozenDictionary<string, BinaryPropertySetterInfo>.Empty
: PropertiesArray.ToFrozenDictionary(static p => p.Name, static p => p, StringComparer.Ordinal);
CompiledConstructor = TryCreateConstructor(type);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propertyInfo)
=> _properties.TryGetValue(name, out propertyInfo);
private static Func<object>? TryCreateConstructor(Type type)
{
if (type.IsAbstract) return null;
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null);
if (ctor == null) return null;
var newExpr = Expression.New(ctor);
var convert = Expression.Convert(newExpr, typeof(object));
return Expression.Lambda<Func<object>>(convert).Compile();
}
}
internal sealed class BinaryPropertySetterInfo
{
private static readonly Func<object, object?> NullGetter = static _ => null;
private static readonly Action<object, object?> NullSetter = static (_, _) => { };
private readonly Func<object, object?> _getter;
private readonly Action<object, object?> _setter;
public string Name { get; }
public Type PropertyType { get; }
public bool IsComplexType { get; }
public bool IsCollection { get; }
public Type? ElementType { get; }
public bool IsIIdCollection { get; }
public Type? ElementIdType { get; }
public Func<object, object?>? ElementIdGetter { get; }
public BinaryPropertySetterInfo(PropertyInfo property)
{
Name = property.Name;
PropertyType = property.PropertyType;
IsCollection = IsCollectionType(PropertyType);
ElementType = IsCollection ? GetCollectionElementType(PropertyType) : null;
if (ElementType != null)
{
var elementIdInfo = GetIdInfo(ElementType);
IsIIdCollection = elementIdInfo.IsId;
ElementIdType = elementIdInfo.IdType;
if (IsIIdCollection)
{
var idProp = ElementType.GetProperty("Id", BindingFlags.Instance | BindingFlags.Public);
if (idProp != null)
{
ElementIdGetter = CreateCompiledGetter(ElementType, idProp);
}
}
}
IsComplexType = IsComplex(PropertyType);
_getter = CreateGetter(property);
_setter = CreateSetter(property);
}
public BinaryPropertySetterInfo(
string name,
Type propertyType,
bool isCollection,
Type? elementType,
Type? elementIdType,
Func<object, object?>? elementIdGetter,
Func<object, object?>? getter = null,
Action<object, object?>? setter = null)
{
Name = name;
PropertyType = propertyType;
IsCollection = isCollection;
ElementType = elementType;
ElementIdType = elementIdType;
ElementIdGetter = elementIdGetter;
IsIIdCollection = elementIdGetter != null && elementIdType != null;
IsComplexType = elementType != null ? IsComplex(elementType) : IsComplex(propertyType);
_getter = getter ?? NullGetter;
_setter = setter ?? NullSetter;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object target) => _getter(target);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetValue(object target, object? value) => _setter(target, value);
private static bool IsCollectionType(Type type)
{
if (ReferenceEquals(type, StringType)) return false;
if (type.IsArray) return true;
return typeof(IEnumerable).IsAssignableFrom(type);
}
private static bool IsComplex(Type type)
{
var actualType = Nullable.GetUnderlyingType(type) ?? type;
return IsComplexType(actualType);
}
private static Func<object, object?> CreateGetter(PropertyInfo property)
{
var targetParam = Expression.Parameter(typeof(object), "target");
var castTarget = Expression.Convert(targetParam, property.DeclaringType!);
var propertyAccess = Expression.Property(castTarget, property);
var boxed = Expression.Convert(propertyAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, targetParam).Compile();
}
private static Action<object, object?> CreateSetter(PropertyInfo property)
{
var targetParam = Expression.Parameter(typeof(object), "target");
var valueParam = Expression.Parameter(typeof(object), "value");
var castTarget = Expression.Convert(targetParam, property.DeclaringType!);
var castValue = Expression.Convert(valueParam, property.PropertyType);
var propertyAccess = Expression.Property(castTarget, property);
var assign = Expression.Assign(propertyAccess, castValue);
return Expression.Lambda<Action<object, object?>>(assign, targetParam, valueParam).Compile();
}
}
}

View File

@ -1,4 +1,3 @@
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
@ -8,9 +7,9 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using AyCode.Core.Helpers;
using static AyCode.Core.Extensions.JsonUtilities;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Exception thrown when binary deserialization fails.
@ -39,7 +38,7 @@ public class AcBinaryDeserializationException : Exception
/// - Optimized with FrozenDictionary for type dispatch
/// - Zero-allocation hot paths using Span and MemoryMarshal
/// </summary>
public static class AcBinaryDeserializer
public static partial class AcBinaryDeserializer
{
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
@ -206,13 +205,27 @@ public static class AcBinaryDeserializer
/// Populate with merge semantics for IId collections.
/// </summary>
public static void PopulateMerge<T>(ReadOnlySpan<byte> data, T target) where T : class
=> PopulateMerge(data, target, null);
/// <summary>
/// Populate with merge semantics for IId collections.
/// </summary>
/// <param name="data">Binary data to deserialize</param>
/// <param name="target">Target object to populate</param>
/// <param name="options">Optional serializer options. When RemoveOrphanedItems is true,
/// items in destination collections that have no matching Id in source will be removed.</param>
public static void PopulateMerge<T>(ReadOnlySpan<byte> data, T target, AcBinarySerializerOptions? options) where T : class
{
ArgumentNullException.ThrowIfNull(target);
if (data.Length == 0) return;
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
var targetType = target.GetType();
var context = new BinaryDeserializationContext(data) { IsMergeMode = true };
var context = new BinaryDeserializationContext(data)
{
IsMergeMode = true,
RemoveOrphanedItems = options?.RemoveOrphanedItems ?? false
};
try
{
@ -662,6 +675,11 @@ public static class AcBinaryDeserializer
var arrayCount = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
var elementMetadata = GetTypeMetadata(elementType);
// Track which IDs we see in source (for orphan removal)
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
? new HashSet<object>(arrayCount)
: null;
for (int i = 0; i < arrayCount; i++)
{
@ -689,9 +707,12 @@ public static class AcBinaryDeserializer
PopulateObject(ref context, newItem, elementMetadata, nextDepth);
var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
if (itemId != null && !IsDefaultValue(itemId, idType))
{
if (existingById.TryGetValue(itemId, out var existingItem))
// Track this ID as seen in source
sourceIds?.Add(itemId);
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
{
// Copy properties to existing item
CopyProperties(newItem, existingItem, elementMetadata);
@ -701,6 +722,26 @@ public static class AcBinaryDeserializer
existingList.Add(newItem);
}
// Remove orphaned items (items in destination but not in source)
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
{
// Find items to remove (those not in sourceIds)
var itemsToRemove = new List<object>();
foreach (var kvp in existingById)
{
if (!sourceIds.Contains(kvp.Key))
{
itemsToRemove.Add(kvp.Value);
}
}
// Remove orphaned items
foreach (var item in itemsToRemove)
{
existingList.Remove(item);
}
}
}
finally
{
@ -1300,554 +1341,23 @@ public static class AcBinaryDeserializer
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
internal sealed class BinaryDeserializeTypeMetadata
{
private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _propertiesDict;
public BinaryPropertySetterInfo[] PropertiesArray { get; }
public Func<object>? CompiledConstructor { get; }
public BinaryDeserializeTypeMetadata(Type type)
{
var ctor = type.GetConstructor(Type.EmptyTypes);
if (ctor != null)
{
var newExpr = Expression.New(type);
var boxed = Expression.Convert(newExpr, typeof(object));
CompiledConstructor = Expression.Lambda<Func<object>>(boxed).Compile();
}
var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var propsList = new List<PropertyInfo>();
foreach (var p in allProps)
{
if (!p.CanWrite || !p.CanRead || p.GetIndexParameters().Length != 0) continue;
if (HasJsonIgnoreAttribute(p)) continue;
propsList.Add(p);
}
var propInfos = new BinaryPropertySetterInfo[propsList.Count];
for (int i = 0; i < propsList.Count; i++)
{
propInfos[i] = new BinaryPropertySetterInfo(propsList[i], type);
}
PropertiesArray = propInfos;
var dict = new Dictionary<string, BinaryPropertySetterInfo>(propInfos.Length, StringComparer.OrdinalIgnoreCase);
foreach (var propInfo in propInfos)
{
dict[propInfo.Name] = propInfo;
}
_propertiesDict = FrozenDictionary.ToFrozenDictionary(dict, StringComparer.OrdinalIgnoreCase);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propInfo)
=> _propertiesDict.TryGetValue(name, out propInfo);
}
internal sealed class BinaryPropertySetterInfo
{
public readonly string Name;
public readonly Type PropertyType;
public readonly Type UnderlyingType;
public readonly bool IsIIdCollection;
public readonly bool IsComplexType;
public readonly bool IsCollection;
public readonly Type? ElementType;
public readonly Type? ElementIdType;
public readonly Func<object, object?>? ElementIdGetter;
private readonly Action<object, object?> _setter;
private readonly Func<object, object?> _getter;
public BinaryPropertySetterInfo(PropertyInfo prop, Type declaringType)
{
Name = prop.Name;
PropertyType = prop.PropertyType;
UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType;
_setter = CreateCompiledSetter(declaringType, prop);
_getter = CreateCompiledGetter(declaringType, prop);
ElementType = GetCollectionElementType(PropertyType);
IsCollection = ElementType != null && ElementType != typeof(object) &&
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
!ReferenceEquals(PropertyType, StringType);
// Determine if this is a complex type that can be populated
IsComplexType = !PropertyType.IsPrimitive &&
!ReferenceEquals(PropertyType, StringType) &&
!PropertyType.IsEnum &&
!ReferenceEquals(PropertyType, GuidType) &&
!ReferenceEquals(PropertyType, DateTimeType) &&
!ReferenceEquals(PropertyType, DecimalType) &&
!ReferenceEquals(PropertyType, TimeSpanType) &&
!ReferenceEquals(PropertyType, DateTimeOffsetType) &&
Nullable.GetUnderlyingType(PropertyType) == null &&
!IsCollection;
if (IsCollection && ElementType != null)
{
var idInfo = GetIdInfo(ElementType);
if (idInfo.IsId)
{
IsIIdCollection = true;
ElementIdType = idInfo.IdType;
var idProp = ElementType.GetProperty("Id");
if (idProp != null)
ElementIdGetter = CreateCompiledGetter(ElementType, idProp);
}
}
}
// Constructor for manual creation (merge scenarios)
public BinaryPropertySetterInfo(string name, Type propertyType, bool isIIdCollection, Type? elementType, Type? elementIdType, Func<object, object?>? elementIdGetter)
{
Name = name;
PropertyType = propertyType;
UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType;
IsIIdCollection = isIIdCollection;
IsCollection = elementType != null;
IsComplexType = false;
ElementType = elementType;
ElementIdType = elementIdType;
ElementIdGetter = elementIdGetter;
_setter = (_, _) => { };
_getter = _ => null;
}
private static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var valueParam = Expression.Parameter(typeof(object), "value");
var castObj = Expression.Convert(objParam, declaringType);
var castValue = Expression.Convert(valueParam, prop.PropertyType);
var propAccess = Expression.Property(castObj, prop);
var assign = Expression.Assign(propAccess, castValue);
return Expression.Lambda<Action<object, object?>>(assign, objParam, valueParam).Compile();
}
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetValue(object target, object? value) => _setter(target, value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object target) => _getter(target);
}
#endregion
#region Deserialization Context
// Implementation moved to AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
}
/// <summary>
/// Optimized deserialization context using ref struct for zero allocation.
/// Uses MemoryMarshal for fast primitive reads.
/// </summary>
internal ref struct BinaryDeserializationContext
sealed class TypeConversionInfo
{
public Type UnderlyingType { get; }
public TypeCode TypeCode { get; }
public bool IsEnum { get; }
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
{
private readonly ReadOnlySpan<byte> _data;
private int _position;
// Header info
public byte FormatVersion { get; private set; }
public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; }
public bool HasPreloadedInternTable { get; private set; }
/// <summary>
/// Minimum string length for interning. Must match serializer's MinStringInternLength.
/// Default: 4 (from AcBinarySerializerOptions)
/// </summary>
public byte MinStringInternLength { get; private set; }
// Property name table
private string[]? _propertyNames;
// Interned strings - dynamically built during deserialization
private List<string>? _internedStrings;
// Reference map
private Dictionary<int, object>? _references;
public bool IsMergeMode { get; set; }
public int Position => _position;
public bool IsAtEnd => _position >= _data.Length;
public BinaryDeserializationContext(ReadOnlySpan<byte> data)
{
_data = data;
_position = 0;
FormatVersion = 0;
HasMetadata = false;
HasReferenceHandling = true;
HasPreloadedInternTable = false;
MinStringInternLength = 4;
_propertyNames = null;
_internedStrings = null;
_references = null;
IsMergeMode = false;
}
public void ReadHeader()
{
if (_data.Length < 2) return;
FormatVersion = ReadByte();
var flags = ReadByte();
bool hasInternTable = false;
// Handle new flag-based header format (48+)
if (flags >= BinaryTypeCode.HeaderFlagsBase)
{
HasMetadata = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
hasInternTable = (flags & BinaryTypeCode.HeaderFlag_StringInternTable) != 0;
}
else
{
// Legacy format: MetadataHeader (32) or NoMetadataHeader (33)
// These always implied HasReferenceHandling = true
HasMetadata = flags == BinaryTypeCode.MetadataHeader;
HasReferenceHandling = true;
}
if (HasMetadata)
{
// Read property names
var propCount = (int)ReadVarUInt();
if (propCount > 0)
{
_propertyNames = new string[propCount];
for (int i = 0; i < propCount; i++)
{
var len = (int)ReadVarUInt();
_propertyNames[i] = ReadStringUtf8(len);
}
}
}
// Read preloaded string intern table from header
if (hasInternTable)
{
HasPreloadedInternTable = true;
var internCount = (int)ReadVarUInt();
// Always initialize the list, even if empty
_internedStrings = new List<string>(internCount > 0 ? internCount : 4);
for (int i = 0; i < internCount; i++)
{
var len = (int)ReadVarUInt();
var str = ReadStringUtf8(len);
_internedStrings.Add(str);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte()
{
if (_position >= _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
return _data[_position++];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte PeekByte()
{
if (_position >= _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
return _data[_position];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Skip(int count)
{
_position += count;
if (_position > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] ReadBytes(int count)
{
if (_position + count > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = _data.Slice(_position, count).ToArray();
_position += count;
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ReadVarInt()
{
var encoded = ReadVarUInt();
// ZigZag decode
return (int)((encoded >> 1) ^ -(encoded & 1));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint ReadVarUInt()
{
uint result = 0;
int shift = 0;
while (true)
{
var b = ReadByte();
result |= (uint)(b & 0x7F) << shift;
if ((b & 0x80) == 0) break;
shift += 7;
if (shift > 28)
throw new AcBinaryDeserializationException("Invalid VarInt", _position);
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long ReadVarLong()
{
var encoded = ReadVarULong();
// ZigZag decode
return (long)((encoded >> 1) ^ (0 - (encoded & 1)));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong ReadVarULong()
{
ulong result = 0;
int shift = 0;
while (true)
{
var b = ReadByte();
result |= (ulong)(b & 0x7F) << shift;
if ((b & 0x80) == 0) break;
shift += 7;
if (shift > 63)
throw new AcBinaryDeserializationException("Invalid VarLong", _position);
}
return result;
}
/// <summary>
/// Optimized Int16 read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short ReadInt16Unsafe()
{
if (_position + 2 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = Unsafe.ReadUnaligned<short>(ref Unsafe.AsRef(in _data[_position]));
_position += 2;
return result;
}
/// <summary>
/// Optimized UInt16 read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort ReadUInt16Unsafe()
{
if (_position + 2 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = Unsafe.ReadUnaligned<ushort>(ref Unsafe.AsRef(in _data[_position]));
_position += 2;
return result;
}
/// <summary>
/// Optimized float read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float ReadSingleUnsafe()
{
if (_position + 4 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = Unsafe.ReadUnaligned<float>(ref Unsafe.AsRef(in _data[_position]));
_position += 4;
return result;
}
/// <summary>
/// Optimized double read using direct memory access.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double ReadDoubleUnsafe()
{
if (_position + 8 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = Unsafe.ReadUnaligned<double>(ref Unsafe.AsRef(in _data[_position]));
_position += 8;
return result;
}
/// <summary>
/// Optimized decimal read using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public decimal ReadDecimalUnsafe()
{
if (_position + 16 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
Span<int> bits = stackalloc int[4];
MemoryMarshal.Cast<byte, int>(_data.Slice(_position, 16)).CopyTo(bits);
_position += 16;
return new decimal(bits);
}
/// <summary>
/// Optimized char read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public char ReadCharUnsafe()
{
if (_position + 2 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = Unsafe.ReadUnaligned<char>(ref Unsafe.AsRef(in _data[_position]));
_position += 2;
return result;
}
/// <summary>
/// Optimized DateTime read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTime ReadDateTimeUnsafe()
{
if (_position + 9 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var ticks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
var kind = (DateTimeKind)_data[_position + 8];
_position += 9;
return new DateTime(ticks, kind);
}
/// <summary>
/// Optimized DateTimeOffset read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTimeOffset ReadDateTimeOffsetUnsafe()
{
if (_position + 10 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var utcTicks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
var offsetMinutes = Unsafe.ReadUnaligned<short>(ref Unsafe.AsRef(in _data[_position + 8]));
_position += 10;
var offset = TimeSpan.FromMinutes(offsetMinutes);
var localTicks = utcTicks + offset.Ticks;
return new DateTimeOffset(localTicks, offset);
}
/// <summary>
/// Optimized TimeSpan read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TimeSpan ReadTimeSpanUnsafe()
{
if (_position + 8 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var ticks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
_position += 8;
return new TimeSpan(ticks);
}
/// <summary>
/// Optimized Guid read.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid ReadGuidUnsafe()
{
if (_position + 16 > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = new Guid(_data.Slice(_position, 16));
_position += 16;
return result;
}
/// <summary>
/// Optimized string read using UTF8 span decoding.
/// Uses String.Create to decode directly into the target string buffer to avoid intermediate allocations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadStringUtf8(int byteCount)
{
if (_position + byteCount > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var src = _data.Slice(_position, byteCount);
var result = Utf8NoBom.GetString(src);
_position += byteCount;
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetPropertyName(int index)
{
if (_propertyNames == null || index < 0 || index >= _propertyNames.Length)
throw new AcBinaryDeserializationException($"Invalid property name index: {index}", _position);
return _propertyNames[index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterInternedString(string value)
{
// Skip registration if intern table was preloaded from header
if (HasPreloadedInternTable) return;
_internedStrings ??= new List<string>(16);
_internedStrings.Add(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetInternedString(int index)
{
if (_internedStrings == null || index < 0 || index >= _internedStrings.Count)
throw new AcBinaryDeserializationException($"Invalid interned string index: {index}. Interned strings count: {_internedStrings?.Count ?? 0}", _position);
return _internedStrings[index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(int refId, object obj)
{
_references ??= new Dictionary<int, object>();
_references[refId] = obj;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetReferencedObject(int refId)
{
if (_references != null && _references.TryGetValue(refId, out var obj))
return obj;
return null;
}
}
#endregion
private sealed class TypeConversionInfo
{
public Type UnderlyingType { get; }
public TypeCode TypeCode { get; }
public bool IsEnum { get; }
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
{
UnderlyingType = underlyingType;
TypeCode = typeCode;
IsEnum = isEnum;
}
UnderlyingType = underlyingType;
TypeCode = typeCode;
IsEnum = isEnum;
}
}
// Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs

View File

@ -0,0 +1,1098 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
private static class BinarySerializationContextPool
{
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
{
if (Pool.TryDequeue(out var context))
{
context.Reset(options);
return context;
}
return new BinarySerializationContext(options);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(BinarySerializationContext context)
{
if (Pool.Count < MaxPoolSize)
{
context.Clear();
Pool.Enqueue(context);
}
else
{
context.Dispose();
}
}
}
internal sealed class BinarySerializationContext : IDisposable
{
private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512;
private const int InitialInternCapacity = 32;
private const int InitialPropertyNameCapacity = 32;
private const int InitialReferenceCapacity = 16;
private const int InitialMultiRefCapacity = 8;
// Bloom filter constants for string interning
private const int BloomFilterSize = 256; // 256 bits = 32 bytes
private const int BloomFilterMask = BloomFilterSize - 1;
private byte[] _buffer;
private int _position;
private int _initialBufferSize;
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, int>? _writtenRefs;
private HashSet<object>? _multiReferenced;
private int _nextRefId;
private Dictionary<string, int>? _internedStrings;
private List<string>? _internedStringList;
/// <summary>
/// Bloom filter for quick "definitely not interned" checks.
/// Avoids dictionary lookup for unique strings.
/// </summary>
private ulong _bloomFilter0;
private ulong _bloomFilter1;
private ulong _bloomFilter2;
private ulong _bloomFilter3;
private Dictionary<string, int>? _propertyNames;
private List<string>? _propertyNameList;
private int[]? _propertyIndexBuffer;
private byte[]? _propertyStateBuffer;
public bool UseReferenceHandling { get; private set; }
public bool UseStringInterning { get; private set; }
public bool UseMetadata { get; private set; }
public byte MaxDepth { get; private set; }
public byte MinStringInternLength { get; private set; }
public BinaryPropertyFilter? PropertyFilter { get; private set; }
public int Position => _position;
public BinarySerializationContext(AcBinarySerializerOptions options)
{
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
Reset(options);
}
public void Reset(AcBinarySerializerOptions options)
{
_position = 0;
_nextRefId = 1;
UseReferenceHandling = options.UseReferenceHandling;
UseStringInterning = options.UseStringInterning;
UseMetadata = options.UseMetadata;
MaxDepth = options.MaxDepth;
MinStringInternLength = options.MinStringInternLength;
PropertyFilter = options.PropertyFilter;
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
if (_buffer.Length < _initialBufferSize)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
}
}
public void Clear()
{
_position = 0;
_nextRefId = 1;
// Reset bloom filter
_bloomFilter0 = 0;
_bloomFilter1 = 0;
_bloomFilter2 = 0;
_bloomFilter3 = 0;
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
_propertyNameList?.Clear();
_internedStringList?.Clear();
// Reset cached property indices
ResetCachedPropertyIndices();
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
{
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
}
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
}
private void ResetCachedPropertyIndices()
{
// Note: BinaryPropertyAccessor.CachedPropertyNameIndex is per-context,
// but metadata is cached globally. We reset it during Clear to avoid
// stale indices. The next serialization will re-populate them.
// This is a minor cost as it only happens on context reuse.
}
public void Dispose()
{
if (_buffer != null)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null!;
}
if (_propertyIndexBuffer != null)
{
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
}
if (_propertyStateBuffer != null)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
}
#region String Interning
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterInternedString(string value)
{
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
_internedStringList ??= new List<string>(InitialInternCapacity);
// Fast path: check bloom filter first
var hash = GetStringHash(value);
if (!BloomFilterMightContain(hash))
{
// Definitely not in dictionary - add directly
var newIndex = _internedStringList.Count;
_internedStrings[value] = newIndex;
_internedStringList.Add(value);
BloomFilterAdd(hash);
return newIndex;
}
// Might be in dictionary - need to check
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
if (exists)
{
return index;
}
index = _internedStringList.Count;
_internedStringList.Add(value);
BloomFilterAdd(hash);
return index;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetStringHash(string value)
{
// Simple hash combining length and first/last characters
// Optimized for quick calculation, not collision resistance
if (value.Length == 0) return 0;
var h = value.Length;
h = (h * 31) + value[0];
if (value.Length > 1)
h = (h * 31) + value[value.Length - 1];
return h;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool BloomFilterMightContain(int hash)
{
// Use two hash functions for bloom filter
var h1 = hash & BloomFilterMask;
var h2 = (hash >> 8) & BloomFilterMask;
return BloomFilterTestBit(h1) && BloomFilterTestBit(h2);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool BloomFilterTestBit(int bit)
{
var segment = bit >> 6; // Divide by 64
var mask = 1UL << (bit & 63);
return segment switch
{
0 => (_bloomFilter0 & mask) != 0,
1 => (_bloomFilter1 & mask) != 0,
2 => (_bloomFilter2 & mask) != 0,
_ => (_bloomFilter3 & mask) != 0,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BloomFilterAdd(int hash)
{
var h1 = hash & BloomFilterMask;
var h2 = (hash >> 8) & BloomFilterMask;
BloomFilterSetBit(h1);
BloomFilterSetBit(h2);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BloomFilterSetBit(int bit)
{
var segment = bit >> 6;
var mask = 1UL << (bit & 63);
switch (segment)
{
case 0: _bloomFilter0 |= mask; break;
case 1: _bloomFilter1 |= mask; break;
case 2: _bloomFilter2 |= mask; break;
default: _bloomFilter3 |= mask; break;
}
}
#endregion
#region Property Name Table
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterPropertyName(string name)
{
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
if (!_propertyNames.ContainsKey(name))
{
var index = _propertyNameList.Count;
_propertyNames[name] = index;
_propertyNameList.Add(name);
}
}
/// <summary>
/// Registers property name and caches the index in the accessor for future lookups.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterPropertyNameAndCache(BinaryPropertyAccessor accessor)
{
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_propertyNames, accessor.Name, out var exists);
if (!exists)
{
index = _propertyNameList.Count;
_propertyNameList.Add(accessor.Name);
}
accessor.CachedPropertyNameIndex = index;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPropertyNameIndex(string name)
=> _propertyNames != null && _propertyNames.TryGetValue(name, out var index) ? index : -1;
#endregion
#region Property State Buffer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] RentPropertyStateBuffer(int size)
{
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size)
{
return _propertyStateBuffer;
}
if (_propertyStateBuffer != null)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
}
_propertyStateBuffer = ArrayPool<byte>.Shared.Rent(size);
return _propertyStateBuffer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyStateBuffer(byte[] buffer)
{
// Buffer stays cached for reuse.
}
#endregion
#region Output
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(_position);
_buffer.AsSpan(0, _position).CopyTo(result);
return result;
}
public void WriteTo(IBufferWriter<byte> writer)
{
var span = writer.GetSpan(_position);
_buffer.AsSpan(0, _position).CopyTo(span);
writer.Advance(_position);
}
public BinarySerializationResult DetachResult()
{
var resultBuffer = _buffer;
var resultLength = _position;
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
_position = 0;
return new BinarySerializationResult(resultBuffer, resultLength, pooled: true);
}
#endregion
#region Property Filtering
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property)
{
if (PropertyFilter == null)
{
return true;
}
var context = new BinaryPropertyFilterContext(
instance,
property.DeclaringType,
property.Name,
property.PropertyType,
property.ObjectGetter);
return PropertyFilter(context);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldIncludePropertyInMetadata(BinaryPropertyAccessor property)
{
if (PropertyFilter == null)
{
return true;
}
var context = new BinaryPropertyFilterContext(
null,
property.DeclaringType,
property.Name,
property.PropertyType,
null);
return PropertyFilter(context);
}
#endregion
#region Buffer Helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int additionalBytes)
{
var required = _position + additionalBytes;
if (required <= _buffer.Length)
{
return;
}
GrowBuffer(required);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowBuffer(int required)
{
var newSize = Math.Max(_buffer.Length * 2, required);
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = newBuffer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
if (_position >= _buffer.Length)
{
GrowBuffer(_position + 1);
}
_buffer[_position++] = value;
}
/// <summary>
/// Write type code byte followed by a raw value. Batches EnsureCapacity call.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
{
var size = 1 + Unsafe.SizeOf<T>();
EnsureCapacity(size);
_buffer[_position++] = typeCode;
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += Unsafe.SizeOf<T>();
}
/// <summary>
/// Write two bytes efficiently.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTwoBytes(byte b1, byte b2)
{
EnsureCapacity(2);
_buffer[_position++] = b1;
_buffer[_position++] = b2;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(ReadOnlySpan<byte> data)
{
EnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
EnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
#endregion
#region Specialized Writers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeBits(DateTime value)
{
EnsureCapacity(9);
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind;
_position += 9;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeOffsetBits(DateTimeOffset value)
{
EnsureCapacity(10);
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10;
}
/// <summary>
/// Patches a previously written VarUInt at the specified position.
/// Works correctly only if the new value requires the same or fewer bytes.
/// For property counts < 128, this is always 1 byte.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PatchVarUInt(int position, uint value)
{
// Fast path: single byte (covers 0-127, which is most property counts)
if (value < 0x80)
{
_buffer[position] = (byte)value;
return;
}
// Multi-byte case - need to shift buffer if new encoding is longer
// For simplicity, we'll rewrite from the position
// This is rare for property counts
PatchVarUIntSlow(position, value);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void PatchVarUIntSlow(int position, uint value)
{
// Calculate current size at position (read until no continuation bit)
var currentSize = 0;
var pos = position;
while (pos < _position && (_buffer[pos] & 0x80) != 0)
{
currentSize++;
pos++;
}
currentSize++; // Include final byte without continuation bit
// Calculate new size needed
var newSize = GetVarUIntSize(value);
if (newSize == currentSize)
{
// Same size - just overwrite
var tempPos = position;
while (value >= 0x80)
{
_buffer[tempPos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[tempPos] = (byte)value;
}
else if (newSize < currentSize)
{
// New is smaller - shift data left
var delta = currentSize - newSize;
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
_position -= delta;
var tempPos = position;
while (value >= 0x80)
{
_buffer[tempPos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[tempPos] = (byte)value;
}
else
{
// New is larger - shift data right
var delta = newSize - currentSize;
EnsureCapacity(delta);
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
_position += delta;
var tempPos = position;
while (value >= 0x80)
{
_buffer[tempPos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[tempPos] = (byte)value;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetVarUIntSize(uint value)
{
if (value < 0x80) return 1;
if (value < 0x4000) return 2;
if (value < 0x200000) return 3;
if (value < 0x10000000) return 4;
return 5;
}
public void WriteVarInt(int value)
{
var encoded = (uint)((value << 1) ^ (value >> 31));
// Fast path for small positive values (0-63 when ZigZag encoded)
if (encoded < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)encoded;
return;
}
EnsureCapacity(5);
WriteVarUIntInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt(uint value)
{
// Fast path for small values (0-127)
if (value < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(5);
WriteVarUIntInternal(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteVarUIntInternal(uint value)
{
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarLong(long value)
{
var encoded = (ulong)((value << 1) ^ (value >> 63));
// Fast path for small values
if (encoded < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)encoded;
return;
}
EnsureCapacity(10);
WriteVarULongInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value)
{
// Fast path for small values (0-127)
if (value < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(10);
WriteVarULongInternal(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteVarULongInternal(ulong value)
{
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringUtf8(string value)
{
// Fast path for ASCII-only strings using SIMD-optimized check
if (System.Text.Ascii.IsValid(value))
{
WriteVarUInt((uint)value.Length);
EnsureCapacity(value.Length);
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return;
}
// Standard path for multi-byte UTF8
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
/// <summary>
/// Checks if string contains only ASCII characters (0-127).
/// Optimized loop with early exit.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAscii(string value)
{
var span = value.AsSpan();
for (var i = 0; i < span.Length; i++)
{
if (span[i] > 127)
return false;
}
return true;
}
/// <summary>
/// Writes ASCII string directly to byte buffer (char to byte, no encoding needed).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteAsciiDirect(ReadOnlySpan<char> source, Span<byte> destination)
{
for (var i = 0; i < source.Length; i++)
{
destination[i] = (byte)source[i];
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
{
WriteByte(BinaryTypeCode.String);
WriteVarUInt((uint)utf8Name.Length);
WriteBytes(utf8Name);
}
public void WriteInt32ArrayOptimized(int[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(value);
}
}
}
public void WriteLongArrayOptimized(long[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (value >= int.MinValue && value <= int.MaxValue)
{
var intValue = (int)value;
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(intValue);
}
}
else
{
WriteByte(BinaryTypeCode.Int64);
WriteVarLong(value);
}
}
}
public void WriteDoubleArrayBulk(double[] array)
{
EnsureCapacity(array.Length * 9);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float64;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 8;
}
}
public void WriteFloatArrayBulk(float[] array)
{
EnsureCapacity(array.Length * 5);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float32;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 4;
}
}
public void WriteGuidArrayBulk(Guid[] array)
{
EnsureCapacity(array.Length * 17);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Guid;
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
}
#endregion
#region Header and Metadata
private int _headerPosition;
private int _estimatedHeaderSize;
/// <summary>
/// Estimates header payload size based on registered property names and intern strings.
/// Call after metadata registration but before writing the body.
/// </summary>
public int EstimateHeaderPayloadSize()
{
var size = 0;
if (UseMetadata && _propertyNameList is { Count: > 0 })
{
size += GetVarUIntSize((uint)_propertyNameList.Count);
foreach (var name in _propertyNameList)
{
var byteCount = name.Length; // Assume ASCII (common case), fallback handles multi-byte
size += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
if (UseStringInterning && _internedStringList is { Count: > 0 })
{
size += GetVarUIntSize((uint)_internedStringList.Count);
foreach (var value in _internedStringList)
{
var byteCount = value.Length; // Assume ASCII for estimation
size += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
return size;
}
public void WriteHeaderPlaceholder()
{
EnsureCapacity(2);
_headerPosition = _position;
_position += 2;
_estimatedHeaderSize = 0;
}
/// <summary>
/// Reserves space for header based on estimation. Call after metadata registration.
/// </summary>
public void ReserveHeaderSpace(int estimatedSize)
{
if (estimatedSize > 0)
{
EnsureCapacity(estimatedSize);
_estimatedHeaderSize = estimatedSize;
_position += estimatedSize;
}
}
public void FinalizeHeaderSections()
{
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
// Calculate actual header size first
var actualSize = 0;
if (hasPropertyNames)
{
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
{
var byteCount = Utf8NoBom.GetByteCount(name);
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
if (hasInternTable)
{
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
foreach (var value in _internedStringList)
{
var byteCount = Utf8NoBom.GetByteCount(value);
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
var bodyStart = _headerPosition + 2 + _estimatedHeaderSize;
var bodyLength = _position - bodyStart;
// Shift body if needed
if (actualSize != _estimatedHeaderSize && bodyLength > 0)
{
var delta = actualSize - _estimatedHeaderSize;
if (delta > 0)
{
EnsureCapacity(delta);
}
var newBodyStart = _headerPosition + 2 + actualSize;
if (delta != 0)
{
Array.Copy(_buffer, bodyStart, _buffer, newBodyStart, bodyLength);
_position += delta;
}
}
// Write header payload directly to buffer (no ArrayBufferWriter allocation)
var headerPos = _headerPosition + 2;
if (hasPropertyNames)
{
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
{
headerPos = WriteStringAt(headerPos, name);
}
}
if (hasInternTable)
{
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
foreach (var value in _internedStringList)
{
headerPos = WriteStringAt(headerPos, value);
}
}
// Write header flags
byte flags = BinaryTypeCode.HeaderFlagsBase;
if (hasPropertyNames)
flags |= BinaryTypeCode.HeaderFlag_Metadata;
if (UseReferenceHandling)
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
if (hasInternTable)
flags |= BinaryTypeCode.HeaderFlag_StringInternTable;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
_buffer[_headerPosition + 1] = flags;
}
/// <summary>
/// Writes VarUInt at specific position and returns new position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int WriteVarUIntAt(int pos, uint value)
{
while (value >= 0x80)
{
_buffer[pos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[pos++] = (byte)value;
return pos;
}
/// <summary>
/// Writes UTF8 string at specific position (length-prefixed) and returns new position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int WriteStringAt(int pos, string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
pos = WriteVarUIntAt(pos, (uint)byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount));
return pos + byteCount;
}
// Remove old methods: WriteHeaderVarUInt, WriteHeaderString (no longer needed)
#endregion
#region Reference Handling
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanning(object obj)
{
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
if (exists)
{
count++;
_multiReferenced.Add(obj);
return false;
}
count = 1;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteRef(object obj, out int refId)
{
if (_multiReferenced != null && _multiReferenced.Contains(obj))
{
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
if (!_writtenRefs.ContainsKey(obj))
{
refId = _nextRefId++;
return true;
}
}
refId = 0;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, int refId)
=> _writtenRefs![obj] = refId;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRef(object obj, out int refId)
{
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
{
return true;
}
refId = 0;
return false;
}
#endregion
#region Helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity)
where TKey : notnull
{
if (dict == null)
{
return;
}
dict.Clear();
if (dict.EnsureCapacity(0) > maxCapacity)
{
dict.TrimExcess();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
{
if (set == null)
{
return;
}
set.Clear();
if (set.EnsureCapacity(0) > maxCapacity)
{
set.TrimExcess();
}
}
#endregion
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Buffers;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
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<byte> Span => Buffer.AsSpan(0, Length);
public ReadOnlyMemory<byte> Memory => new(Buffer, 0, Length);
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(Length);
Buffer.AsSpan(0, Length).CopyTo(result);
return result;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
if (_pooled)
{
ArrayPool<byte>.Shared.Return(Buffer);
}
}
internal static BinarySerializationResult FromImmutable(byte[] buffer)
=> new(buffer, buffer.Length, pooled: false);
}
}

View File

@ -0,0 +1,194 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
internal sealed class BinaryTypeMetadata
{
public BinaryPropertyAccessor[] Properties { get; }
public BinaryTypeMetadata(Type type)
{
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead &&
p.GetIndexParameters().Length == 0 &&
!HasJsonIgnoreAttribute(p))
.Select(p => new BinaryPropertyAccessor(p))
.ToArray();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryTypeMetadata GetTypeMetadata(Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
}
internal sealed class BinaryPropertyAccessor
{
public readonly string Name;
public readonly byte[] NameUtf8;
public readonly Type PropertyType;
public readonly TypeCode TypeCode;
public readonly Type DeclaringType;
private readonly Func<object, object?> _objectGetter;
private readonly Delegate? _typedGetter;
private readonly PropertyAccessorType _accessorType;
/// <summary>
/// Cached property name index for metadata mode. Set by context during registration.
/// -1 means not yet cached.
/// </summary>
internal int CachedPropertyNameIndex = -1;
public BinaryPropertyAccessor(PropertyInfo prop)
{
Name = prop.Name;
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
DeclaringType = prop.DeclaringType!;
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
TypeCode = Type.GetTypeCode(PropertyType);
(_typedGetter, _accessorType) = CreateTypedGetter(DeclaringType, prop);
_objectGetter = CreateObjectGetter(DeclaringType, prop);
}
public PropertyAccessorType AccessorType => _accessorType;
public Func<object, object?> ObjectGetter => _objectGetter;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object obj) => _objectGetter(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
private static (Delegate?, PropertyAccessorType) CreateTypedGetter(Type declaringType, PropertyInfo prop)
{
var propType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(propType);
if (underlying != null)
{
return (null, PropertyAccessorType.Object);
}
if (propType.IsEnum)
{
return (CreateEnumGetter(declaringType, prop), PropertyAccessorType.Enum);
}
if (ReferenceEquals(propType, GuidType))
{
return (CreateTypedGetterDelegate<Guid>(declaringType, prop), PropertyAccessorType.Guid);
}
var typeCode = Type.GetTypeCode(propType);
return typeCode switch
{
TypeCode.Int32 => (CreateTypedGetterDelegate<int>(declaringType, prop), PropertyAccessorType.Int32),
TypeCode.Int64 => (CreateTypedGetterDelegate<long>(declaringType, prop), PropertyAccessorType.Int64),
TypeCode.Boolean => (CreateTypedGetterDelegate<bool>(declaringType, prop), PropertyAccessorType.Boolean),
TypeCode.Double => (CreateTypedGetterDelegate<double>(declaringType, prop), PropertyAccessorType.Double),
TypeCode.Single => (CreateTypedGetterDelegate<float>(declaringType, prop), PropertyAccessorType.Single),
TypeCode.Decimal => (CreateTypedGetterDelegate<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
TypeCode.DateTime => (CreateTypedGetterDelegate<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
TypeCode.Byte => (CreateTypedGetterDelegate<byte>(declaringType, prop), PropertyAccessorType.Byte),
TypeCode.Int16 => (CreateTypedGetterDelegate<short>(declaringType, prop), PropertyAccessorType.Int16),
TypeCode.UInt16 => (CreateTypedGetterDelegate<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
TypeCode.UInt32 => (CreateTypedGetterDelegate<uint>(declaringType, prop), PropertyAccessorType.UInt32),
TypeCode.UInt64 => (CreateTypedGetterDelegate<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
_ => (null, PropertyAccessorType.Object)
};
}
private static Delegate CreateEnumGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var convertToInt = Expression.Convert(propAccess, typeof(int));
return Expression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
}
private static Func<object, TProperty> CreateTypedGetterDelegate<TProperty>(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var convertExpr = Expression.Convert(propAccess, typeof(TProperty));
return Expression.Lambda<Func<object, TProperty>>(convertExpr, objParam).Compile();
}
private static Func<object, object?> CreateObjectGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
}
internal enum PropertyAccessorType : byte
{
Object = 0,
Int32,
Int64,
Boolean,
Double,
Single,
Decimal,
DateTime,
Byte,
Int16,
UInt16,
UInt32,
UInt64,
Guid,
Enum
}
}

View File

@ -0,0 +1,1021 @@
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using static AyCode.Core.Helpers.JsonUtilities;
using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// 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
/// </summary>
public static partial class AcBinarySerializer
{
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> 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
/// <summary>
/// Serialize object to binary with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Serialize<T>(T value) => Serialize(value, AcBinarySerializerOptions.Default);
/// <summary>
/// Serialize object to binary with specified options.
/// </summary>
public static byte[] Serialize<T>(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);
}
}
/// <summary>
/// Serialize object to an IBufferWriter for zero-copy scenarios.
/// This avoids the final ToArray() allocation by writing directly to the caller's buffer.
/// </summary>
public static void Serialize<T>(T value, IBufferWriter<byte> 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);
}
}
/// <summary>
/// Get the serialized size without allocating the final array.
/// Useful for pre-allocating buffers.
/// </summary>
public static int GetSerializedSize<T>(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);
}
}
/// <summary>
/// Serialize object and keep the pooled buffer for zero-copy consumers.
/// Caller must dispose the returned result to release the buffer.
/// </summary>
public static BinarySerializationResult SerializeToPooledBuffer<T>(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);
}
// Estimate and reserve header space to avoid body shift later
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
context.ReserveHeaderSpace(estimatedHeaderSize);
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<Type>? visited = null)
{
if (IsPrimitiveOrStringFast(type)) return;
visited ??= new HashSet<Type>();
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;
}
// Use caching registration to avoid dictionary lookup during serialization
context.RegisterPropertyNameAndCache(prop);
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);
}
/// <summary>
/// Optimized primitive writer using TypeCode dispatch.
/// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata.
/// </summary>
[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);
}
/// <summary>
/// Optimized float64 writer using batched write.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat64Unsafe(double value, BinarySerializationContext context)
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value);
}
/// <summary>
/// Optimized float32 writer using batched write.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteFloat32Unsafe(float value, BinarySerializationContext context)
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value);
}
/// <summary>
/// Optimized decimal writer using direct memory copy of bits.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Decimal);
context.WriteDecimalBits(value);
}
/// <summary>
/// Optimized DateTime writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.DateTime);
context.WriteDateTimeBits(value);
}
/// <summary>
/// Optimized Guid writer using direct memory copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.Guid);
context.WriteGuidBits(value);
}
/// <summary>
/// Optimized DateTimeOffset writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context)
{
context.WriteByte(BinaryTypeCode.DateTimeOffset);
context.WriteDateTimeOffsetBits(value);
}
/// <summary>
/// Optimized TimeSpan writer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context)
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteInt16Unsafe(short value, BinarySerializationContext context)
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context)
{
context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, 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);
}
/// <summary>
/// Optimized string writer with span-based UTF8 encoding.
/// Uses stackalloc for small strings to avoid allocations.
/// </summary>
[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;
// Reserve space for property count (will patch later)
var countPosition = context.Position;
context.WriteVarUInt(0); // Placeholder - will be patched
var writtenCount = 0;
// Single pass: check and write in one iteration
for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
// Skip if filter says no
if (context.PropertyFilter != null && !context.ShouldSerializeProperty(value, prop))
continue;
// Skip default/null values
if (IsPropertyDefaultOrNull(value, prop))
continue;
// Write property name/index
if (context.UseMetadata)
{
var propIndex = prop.CachedPropertyNameIndex >= 0
? prop.CachedPropertyNameIndex
: context.GetPropertyNameIndex(prop.Name);
context.WriteVarUInt((uint)propIndex);
}
else
{
context.WritePreencodedPropertyName(prop.NameUtf8);
}
// Write property value
WritePropertyValue(value, prop, context, nextDepth);
writtenCount++;
}
// Patch the property count
context.PatchVarUInt(countPosition, (uint)writtenCount);
}
/// <summary>
/// Checks if a property value is null or default without boxing for value types.
/// </summary>
[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;
}
}
/// <summary>
/// Writes a property value using typed getters to avoid boxing.
/// </summary>
[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 Specialized Array Writers
/// <summary>
/// Optimized array writer with specialized paths for primitive arrays.
/// </summary>
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<object?>();
foreach (var item in enumerable)
{
items.Add(item);
}
context.WriteVarUInt((uint)items.Count);
foreach (var item in items)
{
var itemType = item?.GetType() ?? typeof(object);
WriteValue(item, itemType, context, nextDepth);
}
}
/// <summary>
/// Specialized array writer for primitive arrays using bulk memory operations.
/// Optimized for Blazor Hybrid compatibility (WASM, Android, Windows, iOS).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context)
{
// Int32 array - very common case
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
{
context.WriteVarUInt((uint)intArray.Length);
context.WriteInt32ArrayOptimized(intArray);
return true;
}
// Double array - bulk write as raw bytes
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
{
context.WriteVarUInt((uint)doubleArray.Length);
context.WriteDoubleArrayBulk(doubleArray);
return true;
}
// Long array
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
{
context.WriteVarUInt((uint)longArray.Length);
context.WriteLongArrayOptimized(longArray);
return true;
}
// Float array - bulk write as raw bytes
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
{
context.WriteVarUInt((uint)floatArray.Length);
context.WriteFloatArrayBulk(floatArray);
return true;
}
// Bool array - pack as bytes
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
{
context.WriteVarUInt((uint)boolArray.Length);
for (var i = 0; i < boolArray.Length; i++)
{
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
}
context.WriteVarUInt((uint)boolArray.Length);
return true;
}
// Guid array - bulk write
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
{
context.WriteVarUInt((uint)guidArray.Length);
context.WriteGuidArrayBulk(guidArray);
return true;
}
// Decimal array
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
{
context.WriteVarUInt((uint)decimalArray.Length);
for (var i = 0; i < decimalArray.Length; i++)
{
WriteDecimalUnsafe(decimalArray[i], context);
}
return true;
}
// DateTime array
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
{
context.WriteVarUInt((uint)dateTimeArray.Length);
for (var i = 0; i < dateTimeArray.Length; i++)
{
WriteDateTimeUnsafe(dateTimeArray[i], context);
}
return true;
}
// String array - common case
if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray)
{
context.WriteVarUInt((uint)stringArray.Length);
for (var i = 0; i < stringArray.Length; i++)
{
var s = stringArray[i];
if (s == null)
context.WriteByte(BinaryTypeCode.Null);
else
WriteString(s, context);
}
return true;
}
return false;
}
private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth)
{
context.WriteByte(BinaryTypeCode.Dictionary);
context.WriteVarUInt((uint)dictionary.Count);
var nextDepth = depth + 1;
foreach (DictionaryEntry entry in dictionary)
{
// Write key
var keyType = entry.Key?.GetType() ?? typeof(object);
WriteValue(entry.Key, keyType, context, nextDepth);
// Write value
var valueType = entry.Value?.GetType() ?? typeof(object);
WriteValue(entry.Value, valueType, context, nextDepth);
}
}
#endregion
#region Serialization Result
// Implementation moved to AcBinarySerializer.BinarySerializationResult.cs
#endregion
#region Context Pool
// Implementation moved to AcBinarySerializer.BinarySerializationContext.cs
#endregion
#region Serialization Context
// Implementation moved to AcBinarySerializer.BinarySerializationContext.cs
#endregion
#region Type Metadata
private static Type? GetCollectionElementType(Type type)
{
if (type.IsArray)
{
return type.GetElementType();
}
if (type.IsGenericType)
{
var args = type.GetGenericArguments();
if (args.Length == 1)
{
return args[0];
}
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return iface.GetGenericArguments()[0];
}
}
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryTypeMetadata GetTypeMetadata(Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
// Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs
#endregion
}

View File

@ -1,7 +1,8 @@
using System;
using System.Runtime.CompilerServices;
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Options for AcBinarySerializer and AcBinaryDeserializer.
@ -76,6 +77,14 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// </summary>
public BinaryPropertyFilter? PropertyFilter { get; init; }
/// <summary>
/// When true, PopulateMerge will remove items from destination collections
/// that have no matching Id in the source data.
/// Only applies to IId collections during merge operations.
/// Default: false (orphaned items are kept)
/// </summary>
public bool RemoveOrphanedItems { get; init; } = false;
/// <summary>
/// Creates options with specified max depth.
/// </summary>

View File

@ -1,4 +1,3 @@
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
@ -9,11 +8,9 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
using Newtonsoft.Json;
using static AyCode.Core.Extensions.JsonUtilities;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Serializers.Jsons;
/// <summary>
/// Exception thrown when JSON deserialization fails.

View File

@ -7,11 +7,9 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AyCode.Core.Interfaces;
using Newtonsoft.Json;
using static AyCode.Core.Extensions.JsonUtilities;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Serializers.Jsons;
/// <summary>
/// High-performance custom JSON serializer optimized for IId&lt;T&gt; reference handling.

View File

@ -1,4 +1,4 @@
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Serializers.Jsons;
public enum AcSerializerType : byte
{

View File

@ -2,13 +2,14 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using AyCode.Core.Extensions;
using AyCode.Core.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using static AyCode.Core.Extensions.JsonUtilities;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Extensions;
namespace AyCode.Core.Serializers.Jsons;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonNoMergeCollectionAttribute : Attribute { }

View File

@ -1,4 +1,6 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;

View File

@ -1,6 +1,8 @@
using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;

View File

@ -1,6 +1,7 @@
using System.Security.Claims;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.Server.SignalRs;

View File

@ -7,6 +7,7 @@ using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Services.Server.SignalRs
{

View File

@ -1,6 +1,7 @@
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Jsons;
using AyCode.Services.SignalRs;
using Microsoft.AspNetCore.SignalR;

View File

@ -4,6 +4,8 @@ using AyCode.Core;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.SignalRs;
using Microsoft.AspNetCore.SignalR;

View File

@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Jsons;
using AyCode.Services.SignalRs;
namespace AyCode.Services.Tests.SignalRs;

View File

@ -3,6 +3,7 @@ using AyCode.Core;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Jsons;
using AyCode.Interfaces.Entities;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;

View File

@ -2,6 +2,7 @@
using AyCode.Core.Interfaces;
using System.Buffers;
using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Jsons;
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;

View File

@ -2,6 +2,8 @@ using System.Buffers;
using System.Runtime.CompilerServices;
using AyCode.Core.Compression;
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Services.SignalRs;