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;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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