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:
parent
b17c2df6c2
commit
bc30a3aede
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using MessagePack;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
namespace AyCode.Core.Tests.serialization;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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<T> reference handling.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace AyCode.Core.Extensions;
|
||||
namespace AyCode.Core.Serializers.Jsons;
|
||||
|
||||
public enum AcSerializerType : byte
|
||||
{
|
||||
|
|
@ -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 { }
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
namespace AyCode.Services.Tests.SignalRs;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue