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 MessagePack.Resolvers;
|
||||||
using BenchmarkDotNet.Configs;
|
using BenchmarkDotNet.Configs;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace AyCode.Benchmark
|
namespace AyCode.Benchmark
|
||||||
{
|
{
|
||||||
|
|
@ -67,6 +70,12 @@ namespace AyCode.Benchmark
|
||||||
var config = ManualConfig.Create(DefaultConfig.Instance)
|
var config = ManualConfig.Create(DefaultConfig.Instance)
|
||||||
.WithArtifactsPath(benchmarkDir);
|
.WithArtifactsPath(benchmarkDir);
|
||||||
|
|
||||||
|
if (args.Length > 0 && args[0] == "--quick")
|
||||||
|
{
|
||||||
|
RunQuickBenchmark();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Length > 0 && args[0] == "--test")
|
if (args.Length > 0 && args[0] == "--test")
|
||||||
{
|
{
|
||||||
var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir);
|
var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir);
|
||||||
|
|
@ -112,6 +121,7 @@ namespace AyCode.Benchmark
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("Usage:");
|
Console.WriteLine("Usage:");
|
||||||
|
Console.WriteLine(" --quick Quick benchmark with tabular output (AcBinary vs MessagePack)");
|
||||||
Console.WriteLine(" --test Quick AcBinary test");
|
Console.WriteLine(" --test Quick AcBinary test");
|
||||||
Console.WriteLine(" --testmsgpack Quick MessagePack test");
|
Console.WriteLine(" --testmsgpack Quick MessagePack test");
|
||||||
Console.WriteLine(" --minimal Minimal benchmark");
|
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)
|
static (string InDir, string OutDir) CreateMSTestDeployDirs(string mstestBase)
|
||||||
{
|
{
|
||||||
var user = Environment.UserName ?? "Deploy";
|
var user = Environment.UserName ?? "Deploy";
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ using MongoDB.Bson;
|
||||||
using MongoDB.Bson.IO;
|
using MongoDB.Bson.IO;
|
||||||
using MongoDB.Bson.Serialization;
|
using MongoDB.Bson.Serialization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
namespace AyCode.Core.Benchmarks;
|
namespace AyCode.Core.Benchmarks;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -3,6 +3,7 @@ using AyCode.Core.Enums;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
using AyCode.Core.Loggers;
|
using AyCode.Core.Loggers;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
namespace AyCode.Core.Tests.Serialization;
|
namespace AyCode.Core.Tests.Serialization;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
|
|
||||||
namespace AyCode.Core.Tests.serialization;
|
namespace AyCode.Core.Tests.serialization;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using MessagePack.Resolvers;
|
using MessagePack.Resolvers;
|
||||||
|
|
@ -205,9 +207,9 @@ public class QuickBenchmark
|
||||||
|
|
||||||
var sizeDiff = msgPackData.Length - acBinaryData.Length;
|
var sizeDiff = msgPackData.Length - acBinaryData.Length;
|
||||||
if (sizeDiff > 0)
|
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
|
else
|
||||||
Console.WriteLine($"?? AcBinary {-sizeDiff:N0} bytes larger");
|
Console.WriteLine($"⚠️ AcBinary {-sizeDiff:N0} bytes larger");
|
||||||
|
|
||||||
Assert.IsNotNull(acBinaryResult);
|
Assert.IsNotNull(acBinaryResult);
|
||||||
Assert.IsNotNull(msgPackResult);
|
Assert.IsNotNull(msgPackResult);
|
||||||
|
|
@ -279,7 +281,7 @@ public class QuickBenchmark
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
var sizeSaving = msgPack.Length - acWithIntern.Length;
|
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");
|
Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
using Newtonsoft.Json;
|
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.Runtime.Serialization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
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;
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ using System.Buffers;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using System.Globalization;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Helpers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cached result for IId type info lookup.
|
/// Cached result for IId type info lookup.
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
using System.Collections;
|
using System.Reflection;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Helpers;
|
||||||
|
|
||||||
public static class PropertyHelper
|
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;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
|
|
@ -8,9 +7,9 @@ using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using AyCode.Core.Helpers;
|
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>
|
/// <summary>
|
||||||
/// Exception thrown when binary deserialization fails.
|
/// Exception thrown when binary deserialization fails.
|
||||||
|
|
@ -39,7 +38,7 @@ public class AcBinaryDeserializationException : Exception
|
||||||
/// - Optimized with FrozenDictionary for type dispatch
|
/// - Optimized with FrozenDictionary for type dispatch
|
||||||
/// - Zero-allocation hot paths using Span and MemoryMarshal
|
/// - Zero-allocation hot paths using Span and MemoryMarshal
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AcBinaryDeserializer
|
public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
||||||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||||||
|
|
@ -206,13 +205,27 @@ public static class AcBinaryDeserializer
|
||||||
/// Populate with merge semantics for IId collections.
|
/// Populate with merge semantics for IId collections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void PopulateMerge<T>(ReadOnlySpan<byte> data, T target) where T : class
|
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);
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
if (data.Length == 0) return;
|
if (data.Length == 0) return;
|
||||||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
|
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
|
||||||
|
|
||||||
var targetType = target.GetType();
|
var targetType = target.GetType();
|
||||||
var context = new BinaryDeserializationContext(data) { IsMergeMode = true };
|
var context = new BinaryDeserializationContext(data)
|
||||||
|
{
|
||||||
|
IsMergeMode = true,
|
||||||
|
RemoveOrphanedItems = options?.RemoveOrphanedItems ?? false
|
||||||
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -663,6 +676,11 @@ public static class AcBinaryDeserializer
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
var elementMetadata = GetTypeMetadata(elementType);
|
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++)
|
for (int i = 0; i < arrayCount; i++)
|
||||||
{
|
{
|
||||||
var itemCode = context.PeekByte();
|
var itemCode = context.PeekByte();
|
||||||
|
|
@ -689,9 +707,12 @@ public static class AcBinaryDeserializer
|
||||||
PopulateObject(ref context, newItem, elementMetadata, nextDepth);
|
PopulateObject(ref context, newItem, elementMetadata, nextDepth);
|
||||||
|
|
||||||
var itemId = idGetter(newItem);
|
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
|
// Copy properties to existing item
|
||||||
CopyProperties(newItem, existingItem, elementMetadata);
|
CopyProperties(newItem, existingItem, elementMetadata);
|
||||||
|
|
@ -701,6 +722,26 @@ public static class AcBinaryDeserializer
|
||||||
|
|
||||||
existingList.Add(newItem);
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -1300,554 +1341,23 @@ public static class AcBinaryDeserializer
|
||||||
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
|
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
|
#endregion
|
||||||
|
|
||||||
#region Deserialization Context
|
// Implementation moved to AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
sealed class TypeConversionInfo
|
||||||
/// Optimized deserialization context using ref struct for zero allocation.
|
{
|
||||||
/// Uses MemoryMarshal for fast primitive reads.
|
public Type UnderlyingType { get; }
|
||||||
/// </summary>
|
public TypeCode TypeCode { get; }
|
||||||
internal ref struct BinaryDeserializationContext
|
public bool IsEnum { get; }
|
||||||
|
|
||||||
|
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
|
||||||
{
|
{
|
||||||
private readonly ReadOnlySpan<byte> _data;
|
UnderlyingType = underlyingType;
|
||||||
private int _position;
|
TypeCode = typeCode;
|
||||||
|
IsEnum = isEnum;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 System.Runtime.CompilerServices;
|
||||||
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for AcBinarySerializer and AcBinaryDeserializer.
|
/// Options for AcBinarySerializer and AcBinaryDeserializer.
|
||||||
|
|
@ -76,6 +77,14 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BinaryPropertyFilter? PropertyFilter { get; init; }
|
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>
|
/// <summary>
|
||||||
/// Creates options with specified max depth.
|
/// Creates options with specified max depth.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
|
|
@ -9,11 +8,9 @@ using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
using AyCode.Core.Interfaces;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using static AyCode.Core.Extensions.JsonUtilities;
|
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when JSON deserialization fails.
|
/// Exception thrown when JSON deserialization fails.
|
||||||
|
|
@ -7,11 +7,9 @@ using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AyCode.Core.Interfaces;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using static AyCode.Core.Extensions.JsonUtilities;
|
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// High-performance custom JSON serializer optimized for IId<T> reference handling.
|
/// 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
|
public enum AcSerializerType : byte
|
||||||
{
|
{
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Newtonsoft.Json.Serialization;
|
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)]
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||||
public sealed class JsonNoMergeCollectionAttribute : Attribute { }
|
public sealed class JsonNoMergeCollectionAttribute : Attribute { }
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using AyCode.Services.Server.SignalRs;
|
using AyCode.Services.Server.SignalRs;
|
||||||
using AyCode.Services.SignalRs;
|
using AyCode.Services.SignalRs;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using AyCode.Core.Enums;
|
using AyCode.Core.Enums;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using AyCode.Services.Server.SignalRs;
|
using AyCode.Services.Server.SignalRs;
|
||||||
using AyCode.Services.SignalRs;
|
using AyCode.Services.SignalRs;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using AyCode.Models.Server.DynamicMethods;
|
using AyCode.Models.Server.DynamicMethods;
|
||||||
using AyCode.Services.Server.SignalRs;
|
using AyCode.Services.Server.SignalRs;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Collections;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
namespace AyCode.Services.Server.SignalRs
|
namespace AyCode.Services.Server.SignalRs
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
using AyCode.Core.Loggers;
|
using AyCode.Core.Loggers;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Services.SignalRs;
|
using AyCode.Services.SignalRs;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ using AyCode.Core;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
using AyCode.Core.Loggers;
|
using AyCode.Core.Loggers;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Models.Server.DynamicMethods;
|
using AyCode.Models.Server.DynamicMethods;
|
||||||
using AyCode.Services.SignalRs;
|
using AyCode.Services.SignalRs;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Services.SignalRs;
|
using AyCode.Services.SignalRs;
|
||||||
|
|
||||||
namespace AyCode.Services.Tests.SignalRs;
|
namespace AyCode.Services.Tests.SignalRs;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AyCode.Core;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
using AyCode.Core.Loggers;
|
using AyCode.Core.Loggers;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Interfaces.Entities;
|
using AyCode.Interfaces.Entities;
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
|
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
|
||||||
using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using AyCode.Core.Compression;
|
using AyCode.Core.Compression;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
namespace AyCode.Services.SignalRs;
|
namespace AyCode.Services.SignalRs;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue