From bc30a3aedebd124259213b4124a72ffb9ad90d5f Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 14 Dec 2025 19:34:49 +0100 Subject: [PATCH] Refactor: Add high-perf JSON serializer & merge support - Introduced AcJsonSerializer/AcJsonDeserializer in AyCode.Core.Serializers.Jsons, optimized for IId 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 collections. - Added AcJsonDeserializationException for detailed error reporting. - Implemented UnifiedMergeContractResolver for Newtonsoft.Json, supporting JsonNoMergeCollectionAttribute to control merge behavior. - Added IdAwareCollectionMergeConverter for merging IId 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. --- AyCode.Benchmark/Program.cs | 197 ++ AyCode.Benchmark/SerializationBenchmarks.cs | 2 + .../SignalRCommunicationBenchmarks.cs | 1 + .../Test_Benchmark_Results/.gitignore | 2 + AyCode.Core.Tests/JsonExtensionTests.cs | 1 + .../AcBinaryDateTimeSerializationTests.cs | 1 + .../Serialization/AcBinarySerializerTests.cs | 1 + .../Serialization/QuickBenchmark.cs | 10 +- .../TestModels/SharedTestModels.cs | 1 + AyCode.Core/Extensions/AcBinarySerializer.cs | 2001 ----------------- .../Extensions/SerializeObjectExtensions.cs | 5 +- .../{Extensions => Helpers}/JsonUtilities.cs | 4 +- .../{Extensions => Helpers}/PropertyHelper.cs | 6 +- ...serializer.BinaryDeserializationContext.cs | 405 ++++ ...erializer.BinaryDeserializeTypeMetadata.cs | 163 ++ .../Binaries}/AcBinaryDeserializer.cs | 614 +---- ...rySerializer.BinarySerializationContext.cs | 1098 +++++++++ ...arySerializer.BinarySerializationResult.cs | 50 + .../AcBinarySerializer.BinaryTypeMetadata.cs | 194 ++ .../Binaries/AcBinarySerializer.cs | 1021 +++++++++ .../Binaries}/AcBinarySerializerOptions.cs | 13 +- .../Jsons}/AcJsonDeserializer.cs | 7 +- .../Jsons}/AcJsonSerializer.cs | 6 +- .../Jsons}/AcJsonSerializerOptions.cs | 2 +- .../Jsons}/MergeContractResolver.cs | 5 +- .../SignalRs/SignalRClientToHubTest.cs | 2 + .../SignalRs/SignalRDataSourceTests.cs | 2 + .../SignalRs/TestableSignalRHub2.cs | 1 + .../SignalRs/AcSignalRDataSource.cs | 1 + .../SignalRs/AcSignalRSendToClientService.cs | 1 + .../SignalRs/AcWebSignalRHubBase.cs | 2 + .../SignalRs/PostJsonDataMessageTests.cs | 1 + .../SignalRs/AcSignalRClientBase.cs | 1 + .../SignalRs/IAcSignalRHubClient.cs | 1 + .../SignalRs/SignalRSerializationHelper.cs | 2 + 35 files changed, 3246 insertions(+), 2578 deletions(-) create mode 100644 AyCode.Benchmark/Test_Benchmark_Results/.gitignore delete mode 100644 AyCode.Core/Extensions/AcBinarySerializer.cs rename AyCode.Core/{Extensions => Helpers}/JsonUtilities.cs (99%) rename AyCode.Core/{Extensions => Helpers}/PropertyHelper.cs (86%) create mode 100644 AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs create mode 100644 AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs rename AyCode.Core/{Extensions => Serializers/Binaries}/AcBinaryDeserializer.cs (68%) create mode 100644 AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs create mode 100644 AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationResult.cs create mode 100644 AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs create mode 100644 AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs rename AyCode.Core/{Extensions => Serializers/Binaries}/AcBinarySerializerOptions.cs (95%) rename AyCode.Core/{Extensions => Serializers/Jsons}/AcJsonDeserializer.cs (99%) rename AyCode.Core/{Extensions => Serializers/Jsons}/AcJsonSerializer.cs (99%) rename AyCode.Core/{Extensions => Serializers/Jsons}/AcJsonSerializerOptions.cs (97%) rename AyCode.Core/{Extensions => Serializers/Jsons}/MergeContractResolver.cs (99%) diff --git a/AyCode.Benchmark/Program.cs b/AyCode.Benchmark/Program.cs index 8557e54..234a02a 100644 --- a/AyCode.Benchmark/Program.cs +++ b/AyCode.Benchmark/Program.cs @@ -7,6 +7,9 @@ using MessagePack; using MessagePack.Resolvers; using BenchmarkDotNet.Configs; using System.IO; +using AyCode.Core.Serializers.Jsons; +using AyCode.Core.Serializers.Binaries; +using System.Diagnostics; namespace AyCode.Benchmark { @@ -67,6 +70,12 @@ namespace AyCode.Benchmark var config = ManualConfig.Create(DefaultConfig.Instance) .WithArtifactsPath(benchmarkDir); + if (args.Length > 0 && args[0] == "--quick") + { + RunQuickBenchmark(); + return; + } + if (args.Length > 0 && args[0] == "--test") { var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir); @@ -112,6 +121,7 @@ namespace AyCode.Benchmark } Console.WriteLine("Usage:"); + Console.WriteLine(" --quick Quick benchmark with tabular output (AcBinary vs MessagePack)"); Console.WriteLine(" --test Quick AcBinary test"); Console.WriteLine(" --testmsgpack Quick MessagePack test"); Console.WriteLine(" --minimal Minimal benchmark"); @@ -134,6 +144,193 @@ namespace AyCode.Benchmark } } + /// + /// Quick benchmark comparing AcBinary vs MessagePack with tabular output. + /// Tests: WithRef, NoRef, Serialize, Deserialize, Populate, Merge + /// + static void RunQuickBenchmark(int iterations = 1000) + { + Console.WriteLine(); + Console.WriteLine("????????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine("? AcBinary vs MessagePack Quick Benchmark ?"); + Console.WriteLine("????????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine(); + + // Create test data with shared references + TestDataFactory.ResetIdCounter(); + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); + + var testOrder = TestDataFactory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedMetadata: sharedMeta); + + // Options + var withRefOptions = new AcBinarySerializerOptions(); + var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + + // Warm up + Console.WriteLine("Warming up..."); + for (int i = 0; i < 100; i++) + { + _ = AcBinarySerializer.Serialize(testOrder, withRefOptions); + _ = AcBinarySerializer.Serialize(testOrder, noRefOptions); + _ = MessagePackSerializer.Serialize(testOrder, msgPackOptions); + } + + // Pre-serialize data for deserialization tests + var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions); + var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions); + var msgPackData = MessagePackSerializer.Serialize(testOrder, msgPackOptions); + + Console.WriteLine($"Iterations: {iterations:N0}"); + Console.WriteLine(); + + // Size comparison + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine("? SIZE COMPARISON ?"); + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine("? Format ? Size (bytes) ? vs MessagePack ? Savings ?"); + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine($"? AcBinary (WithRef) ? {acBinaryWithRef.Length,14:N0} ? {100.0 * acBinaryWithRef.Length / msgPackData.Length,13:F1}% ? {msgPackData.Length - acBinaryWithRef.Length,14:N0} ?"); + Console.WriteLine($"? AcBinary (NoRef) ? {acBinaryNoRef.Length,14:N0} ? {100.0 * acBinaryNoRef.Length / msgPackData.Length,13:F1}% ? {msgPackData.Length - acBinaryNoRef.Length,14:N0} ?"); + Console.WriteLine($"? MessagePack ? {msgPackData.Length,14:N0} ? {100.0,13:F1}% ? {"(baseline)",14} ?"); + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine(); + + // Benchmark results storage + var results = new List<(string Operation, string Mode, double AcBinaryMs, double MsgPackMs)>(); + + // Serialize benchmarks + var sw = Stopwatch.StartNew(); + + // AcBinary WithRef Serialize + sw.Restart(); + for (int i = 0; i < iterations; i++) + _ = AcBinarySerializer.Serialize(testOrder, withRefOptions); + var acWithRefSerialize = sw.Elapsed.TotalMilliseconds; + + // AcBinary NoRef Serialize + sw.Restart(); + for (int i = 0; i < iterations; i++) + _ = AcBinarySerializer.Serialize(testOrder, noRefOptions); + var acNoRefSerialize = sw.Elapsed.TotalMilliseconds; + + // MessagePack Serialize + sw.Restart(); + for (int i = 0; i < iterations; i++) + _ = MessagePackSerializer.Serialize(testOrder, msgPackOptions); + var msgPackSerialize = sw.Elapsed.TotalMilliseconds; + + results.Add(("Serialize", "WithRef", acWithRefSerialize, msgPackSerialize)); + results.Add(("Serialize", "NoRef", acNoRefSerialize, msgPackSerialize)); + + // Deserialize benchmarks + // AcBinary WithRef Deserialize + sw.Restart(); + for (int i = 0; i < iterations; i++) + _ = AcBinaryDeserializer.Deserialize(acBinaryWithRef); + var acWithRefDeserialize = sw.Elapsed.TotalMilliseconds; + + // AcBinary NoRef Deserialize + sw.Restart(); + for (int i = 0; i < iterations; i++) + _ = AcBinaryDeserializer.Deserialize(acBinaryNoRef); + var acNoRefDeserialize = sw.Elapsed.TotalMilliseconds; + + // MessagePack Deserialize + sw.Restart(); + for (int i = 0; i < iterations; i++) + _ = MessagePackSerializer.Deserialize(msgPackData, msgPackOptions); + var msgPackDeserialize = sw.Elapsed.TotalMilliseconds; + + results.Add(("Deserialize", "WithRef", acWithRefDeserialize, msgPackDeserialize)); + results.Add(("Deserialize", "NoRef", acNoRefDeserialize, msgPackDeserialize)); + + // Populate benchmark (AcBinary only) + sw.Restart(); + for (int i = 0; i < iterations; i++) + { + var target = CreatePopulateTarget(testOrder); + AcBinaryDeserializer.Populate(acBinaryNoRef, target); + } + var acPopulate = sw.Elapsed.TotalMilliseconds; + results.Add(("Populate", "NoRef", acPopulate, 0)); // MessagePack doesn't have Populate + + // PopulateMerge benchmark (AcBinary only) + sw.Restart(); + for (int i = 0; i < iterations; i++) + { + var target = CreatePopulateTarget(testOrder); + AcBinaryDeserializer.PopulateMerge(acBinaryNoRef.AsSpan(), target); + } + var acMerge = sw.Elapsed.TotalMilliseconds; + results.Add(("Merge", "NoRef", acMerge, 0)); + + // Round-trip + var acWithRefRoundTrip = acWithRefSerialize + acWithRefDeserialize; + var acNoRefRoundTrip = acNoRefSerialize + acNoRefDeserialize; + var msgPackRoundTrip = msgPackSerialize + msgPackDeserialize; + results.Add(("Round-trip", "WithRef", acWithRefRoundTrip, msgPackRoundTrip)); + results.Add(("Round-trip", "NoRef", acNoRefRoundTrip, msgPackRoundTrip)); + + // Print performance table + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine("? PERFORMANCE COMPARISON (lower is better) ?"); + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine("? Operation ? AcBinary (ms) ? MessagePack ? Ratio ?"); + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + + foreach (var r in results) + { + var opName = $"{r.Operation} ({r.Mode})"; + if (r.MsgPackMs > 0) + { + var ratio = r.AcBinaryMs / r.MsgPackMs; + var ratioStr = ratio < 1 ? $"{ratio:F2}x faster" : $"{ratio:F2}x slower"; + Console.WriteLine($"? {opName,-24} ? {r.AcBinaryMs,14:F2} ? {r.MsgPackMs,14:F2} ? {ratioStr,14} ?"); + } + else + { + Console.WriteLine($"? {opName,-24} ? {r.AcBinaryMs,14:F2} ? {"N/A",14} ? {"(unique)",14} ?"); + } + } + + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine(); + + // Summary + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine("? SUMMARY ?"); + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + var sizeAdvantage = 100.0 - (100.0 * acBinaryNoRef.Length / msgPackData.Length); + Console.WriteLine($"? Size advantage: AcBinary is {sizeAdvantage:F1}% smaller than MessagePack ?"); + + var serializeRatio = acNoRefSerialize / msgPackSerialize; + var deserializeRatio = acNoRefDeserialize / msgPackDeserialize; + Console.WriteLine($"? Serialize (NoRef): AcBinary is {(serializeRatio < 1 ? $"{1/serializeRatio:F2}x faster" : $"{serializeRatio:F2}x slower"),-20} ?"); + Console.WriteLine($"? Deserialize (NoRef): AcBinary is {(deserializeRatio < 1 ? $"{1/deserializeRatio:F2}x faster" : $"{deserializeRatio:F2}x slower"),-18} ?"); + Console.WriteLine("???????????????????????????????????????????????????????????????????????????????"); + Console.WriteLine(); + } + + static TestOrder CreatePopulateTarget(TestOrder source) + { + var target = new TestOrder { Id = source.Id }; + foreach (var item in source.Items) + { + target.Items.Add(new TestOrderItem { Id = item.Id }); + } + return target; + } + static (string InDir, string OutDir) CreateMSTestDeployDirs(string mstestBase) { var user = Environment.UserName ?? "Deploy"; diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs index 0d4901c..fa3e787 100644 --- a/AyCode.Benchmark/SerializationBenchmarks.cs +++ b/AyCode.Benchmark/SerializationBenchmarks.cs @@ -12,6 +12,8 @@ using MongoDB.Bson; using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; using System.IO; +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Jsons; namespace AyCode.Core.Benchmarks; diff --git a/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs b/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs index c9fb329..daf4008 100644 --- a/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs +++ b/AyCode.Benchmark/SignalRCommunicationBenchmarks.cs @@ -1,4 +1,5 @@ using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using BenchmarkDotNet.Attributes; using MessagePack; diff --git a/AyCode.Benchmark/Test_Benchmark_Results/.gitignore b/AyCode.Benchmark/Test_Benchmark_Results/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/AyCode.Benchmark/Test_Benchmark_Results/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/AyCode.Core.Tests/JsonExtensionTests.cs b/AyCode.Core.Tests/JsonExtensionTests.cs index 161b08c..be5d3b7 100644 --- a/AyCode.Core.Tests/JsonExtensionTests.cs +++ b/AyCode.Core.Tests/JsonExtensionTests.cs @@ -3,6 +3,7 @@ using AyCode.Core.Enums; using AyCode.Core.Extensions; using AyCode.Core.Interfaces; using AyCode.Core.Loggers; +using AyCode.Core.Serializers.Jsons; using Newtonsoft.Json; using AyCode.Core.Tests.TestModels; diff --git a/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs b/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs index 0d89f4e..722d7b2 100644 --- a/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinaryDateTimeSerializationTests.cs @@ -1,4 +1,5 @@ using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Tests.Serialization; diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs index ac2532a..8b91592 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs @@ -1,4 +1,5 @@ using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; namespace AyCode.Core.Tests.serialization; diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs index 4767b73..3c29235 100644 --- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -1,5 +1,7 @@ -using System.Diagnostics; +using System.Diagnostics; using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using MessagePack; using MessagePack.Resolvers; @@ -205,9 +207,9 @@ public class QuickBenchmark var sizeDiff = msgPackData.Length - acBinaryData.Length; if (sizeDiff > 0) - Console.WriteLine($"? AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)"); + Console.WriteLine($"✅ AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)"); else - Console.WriteLine($"?? AcBinary {-sizeDiff:N0} bytes larger"); + Console.WriteLine($"⚠️ AcBinary {-sizeDiff:N0} bytes larger"); Assert.IsNotNull(acBinaryResult); Assert.IsNotNull(msgPackResult); @@ -279,7 +281,7 @@ public class QuickBenchmark Console.WriteLine(); var sizeSaving = msgPack.Length - acWithIntern.Length; - Console.WriteLine($"? String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)"); + Console.WriteLine($"✅ String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)"); Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller"); } diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index f465c0a..3c4f9a5 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -1,5 +1,6 @@ using AyCode.Core.Extensions; using AyCode.Core.Interfaces; +using AyCode.Core.Serializers.Jsons; using MessagePack; using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json; diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs deleted file mode 100644 index 5d4b892..0000000 --- a/AyCode.Core/Extensions/AcBinarySerializer.cs +++ /dev/null @@ -1,2001 +0,0 @@ -using System.Buffers; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using static AyCode.Core.Extensions.JsonUtilities; - -namespace AyCode.Core.Extensions; - -/// -/// High-performance binary serializer optimized for speed and memory efficiency. -/// Features: -/// - VarInt encoding for compact integers (MessagePack-style) -/// - String interning for repeated strings -/// - Property name table for fast lookup -/// - Reference handling for circular/shared references -/// - Optional metadata for schema evolution -/// - Optimized buffer management with ArrayPool -/// - Zero-allocation hot paths using Span and MemoryMarshal -/// -public static class AcBinarySerializer -{ - private static readonly ConcurrentDictionary TypeMetadataCache = new(); - - // Pre-computed UTF8 encoder for string operations - private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - private static readonly Type StringType = typeof(string); - private static readonly Type GuidType = typeof(Guid); - private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset); - private static readonly Type TimeSpanType = typeof(TimeSpan); - private static readonly Type IntType = typeof(int); - private static readonly Type LongType = typeof(long); - private static readonly Type FloatType = typeof(float); - private static readonly Type DoubleType = typeof(double); - private static readonly Type DecimalType = typeof(decimal); - private static readonly Type BoolType = typeof(bool); - private static readonly Type DateTimeType = typeof(DateTime); - - #region Public API - - /// - /// Serialize object to binary with default options. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte[] Serialize(T value) => Serialize(value, AcBinarySerializerOptions.Default); - - /// - /// Serialize object to binary with specified options. - /// - public static byte[] Serialize(T value, AcBinarySerializerOptions options) - { - if (value == null) - { - return [BinaryTypeCode.Null]; - } - - var runtimeType = value.GetType(); - var context = SerializeCore(value, runtimeType, options); - try - { - return context.ToArray(); - } - finally - { - BinarySerializationContextPool.Return(context); - } - } - - /// - /// Serialize object to an IBufferWriter for zero-copy scenarios. - /// This avoids the final ToArray() allocation by writing directly to the caller's buffer. - /// - public static void Serialize(T value, IBufferWriter writer, AcBinarySerializerOptions options) - { - if (value == null) - { - var span = writer.GetSpan(1); - span[0] = BinaryTypeCode.Null; - writer.Advance(1); - return; - } - - var runtimeType = value.GetType(); - var context = SerializeCore(value, runtimeType, options); - try - { - context.WriteTo(writer); - } - finally - { - BinarySerializationContextPool.Return(context); - } - } - - /// - /// Get the serialized size without allocating the final array. - /// Useful for pre-allocating buffers. - /// - public static int GetSerializedSize(T value, AcBinarySerializerOptions options) - { - if (value == null) return 1; - - var runtimeType = value.GetType(); - var context = SerializeCore(value, runtimeType, options); - try - { - return context.Position; - } - finally - { - BinarySerializationContextPool.Return(context); - } - } - - /// - /// Serialize object and keep the pooled buffer for zero-copy consumers. - /// Caller must dispose the returned result to release the buffer. - /// - public static BinarySerializationResult SerializeToPooledBuffer(T value, AcBinarySerializerOptions options) - { - if (value == null) - { - return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]); - } - - var runtimeType = value.GetType(); - var context = SerializeCore(value, runtimeType, options); - try - { - return context.DetachResult(); - } - finally - { - BinarySerializationContextPool.Return(context); - } - } - - private static BinarySerializationContext SerializeCore(object value, Type runtimeType, AcBinarySerializerOptions options) - { - var context = BinarySerializationContextPool.Get(options); - context.WriteHeaderPlaceholder(); - - if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType)) - { - ScanReferences(value, context, 0); - } - - if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType)) - { - RegisterMetadataForType(runtimeType, context); - } - - WriteValue(value, runtimeType, context, 0); - context.FinalizeHeaderSections(); - return context; - } - - #endregion - - #region Reference Scanning - - private static void ScanReferences(object? value, BinarySerializationContext context, int depth) - { - if (value == null || depth > context.MaxDepth) return; - - var type = value.GetType(); - if (IsPrimitiveOrStringFast(type)) return; - if (!context.TrackForScanning(value)) return; - - if (value is byte[]) return; // byte arrays are value types - - if (value is IDictionary dictionary) - { - foreach (DictionaryEntry entry in dictionary) - { - if (entry.Value != null) - ScanReferences(entry.Value, context, depth + 1); - } - return; - } - - if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) - { - foreach (var item in enumerable) - { - if (item != null) - ScanReferences(item, context, depth + 1); - } - return; - } - - var metadata = GetTypeMetadata(type); - foreach (var prop in metadata.Properties) - { - if (!context.ShouldSerializeProperty(value, prop)) - { - continue; - } - - var propValue = prop.GetValue(value); - if (propValue != null) - ScanReferences(propValue, context, depth + 1); - } - } - - #endregion - - #region Property Metadata Registration - - private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet? visited = null) - { - if (IsPrimitiveOrStringFast(type)) return; - - visited ??= new HashSet(); - if (!visited.Add(type)) return; - - if (IsDictionaryType(type, out var keyType, out var valueType)) - { - if (keyType != null) RegisterMetadataForType(keyType, context, visited); - if (valueType != null) RegisterMetadataForType(valueType, context, visited); - return; - } - - if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType)) - { - var elementType = GetCollectionElementType(type); - if (elementType != null) - { - RegisterMetadataForType(elementType, context, visited); - } - return; - } - - var metadata = GetTypeMetadata(type); - foreach (var prop in metadata.Properties) - { - if (!context.ShouldIncludePropertyInMetadata(prop)) - { - continue; - } - - context.RegisterPropertyName(prop.Name); - - if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType)) - { - RegisterMetadataForType(nestedType, context, visited); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryResolveNestedMetadataType(Type propertyType, out Type nestedType) - { - nestedType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; - - if (IsPrimitiveOrStringFast(nestedType)) - return false; - - if (IsDictionaryType(nestedType, out var _, out var valueType) && valueType != null) - { - if (!IsPrimitiveOrStringFast(valueType)) - { - nestedType = valueType; - return true; - } - return false; - } - - if (typeof(IEnumerable).IsAssignableFrom(nestedType) && !ReferenceEquals(nestedType, StringType)) - { - var elementType = GetCollectionElementType(nestedType); - if (elementType != null && !IsPrimitiveOrStringFast(elementType)) - { - nestedType = elementType; - return true; - } - return false; - } - - return true; - } - - #endregion - - #region Value Writing - - private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth) - { - if (value == null) - { - context.WriteByte(BinaryTypeCode.Null); - return; - } - - // Try writing as primitive first - if (TryWritePrimitive(value, type, context)) - return; - - if (depth > context.MaxDepth) - { - context.WriteByte(BinaryTypeCode.Null); - return; - } - - // Check for object reference - if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId)) - { - context.WriteByte(BinaryTypeCode.ObjectRef); - context.WriteVarInt(refId); - return; - } - - // Handle byte arrays specially - if (value is byte[] byteArray) - { - WriteByteArray(byteArray, context); - return; - } - - // Handle dictionaries - if (value is IDictionary dictionary) - { - WriteDictionary(dictionary, context, depth); - return; - } - - // Handle collections/arrays - if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) - { - WriteArray(enumerable, type, context, depth); - return; - } - - // Handle complex objects - WriteObject(value, type, context, depth); - } - - /// - /// Optimized primitive writer using TypeCode dispatch. - /// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context) - { - // Fast path: check TypeCode first (handles most primitives) - var typeCode = Type.GetTypeCode(type); - - switch (typeCode) - { - case TypeCode.Int32: - WriteInt32((int)value, context); - return true; - case TypeCode.Int64: - WriteInt64((long)value, context); - return true; - case TypeCode.Boolean: - context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False); - return true; - case TypeCode.Double: - WriteFloat64Unsafe((double)value, context); - return true; - case TypeCode.String: - WriteString((string)value, context); - return true; - case TypeCode.Single: - WriteFloat32Unsafe((float)value, context); - return true; - case TypeCode.Decimal: - WriteDecimalUnsafe((decimal)value, context); - return true; - case TypeCode.DateTime: - WriteDateTimeUnsafe((DateTime)value, context); - return true; - case TypeCode.Byte: - context.WriteByte(BinaryTypeCode.UInt8); - context.WriteByte((byte)value); - return true; - case TypeCode.Int16: - WriteInt16Unsafe((short)value, context); - return true; - case TypeCode.UInt16: - WriteUInt16Unsafe((ushort)value, context); - return true; - case TypeCode.UInt32: - WriteUInt32((uint)value, context); - return true; - case TypeCode.UInt64: - WriteUInt64((ulong)value, context); - return true; - case TypeCode.SByte: - context.WriteByte(BinaryTypeCode.Int8); - context.WriteByte(unchecked((byte)(sbyte)value)); - return true; - case TypeCode.Char: - WriteCharUnsafe((char)value, context); - return true; - } - - // Handle nullable types - var underlyingType = Nullable.GetUnderlyingType(type); - if (underlyingType != null) - { - return TryWritePrimitive(value, underlyingType, context); - } - - // Handle special types by reference comparison (faster than type equality) - if (ReferenceEquals(type, GuidType)) - { - WriteGuidUnsafe((Guid)value, context); - return true; - } - if (ReferenceEquals(type, DateTimeOffsetType)) - { - WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context); - return true; - } - if (ReferenceEquals(type, TimeSpanType)) - { - WriteTimeSpanUnsafe((TimeSpan)value, context); - return true; - } - if (type.IsEnum) - { - WriteEnum(value, context); - return true; - } - - return false; - } - - #endregion - - #region Optimized Primitive Writers using MemoryMarshal - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteInt32(int value, BinarySerializationContext context) - { - if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) - { - context.WriteByte(tiny); - return; - } - context.WriteByte(BinaryTypeCode.Int32); - context.WriteVarInt(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteInt64(long value, BinarySerializationContext context) - { - if (value >= int.MinValue && value <= int.MaxValue) - { - WriteInt32((int)value, context); - return; - } - context.WriteByte(BinaryTypeCode.Int64); - context.WriteVarLong(value); - } - - /// - /// Optimized float64 writer using direct memory copy. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteFloat64Unsafe(double value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.Float64); - context.WriteRaw(value); - } - - /// - /// Optimized float32 writer using direct memory copy. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteFloat32Unsafe(float value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.Float32); - context.WriteRaw(value); - } - - /// - /// Optimized decimal writer using direct memory copy of bits. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.Decimal); - context.WriteDecimalBits(value); - } - - /// - /// Optimized DateTime writer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.DateTime); - context.WriteDateTimeBits(value); - } - - /// - /// Optimized Guid writer using direct memory copy. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.Guid); - context.WriteGuidBits(value); - } - - /// - /// Optimized DateTimeOffset writer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.DateTimeOffset); - context.WriteDateTimeOffsetBits(value); - } - - /// - /// Optimized TimeSpan writer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.TimeSpan); - context.WriteRaw(value.Ticks); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteInt16Unsafe(short value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.Int16); - context.WriteRaw(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.UInt16); - context.WriteRaw(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt32(uint value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.UInt32); - context.WriteVarUInt(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt64(ulong value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.UInt64); - context.WriteVarULong(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteCharUnsafe(char value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.Char); - context.WriteRaw(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteEnum(object value, BinarySerializationContext context - ) - { - var intValue = Convert.ToInt32(value); - if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) - { - context.WriteByte(BinaryTypeCode.Enum); - context.WriteByte(tiny); - return; - } - context.WriteByte(BinaryTypeCode.Enum); - context.WriteByte(BinaryTypeCode.Int32); - context.WriteVarInt(intValue); - } - - /// - /// Optimized string writer with span-based UTF8 encoding. - /// Uses stackalloc for small strings to avoid allocations. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteString(string value, BinarySerializationContext context) - { - if (value.Length == 0) - { - context.WriteByte(BinaryTypeCode.StringEmpty); - return; - } - - if (context.UseStringInterning && value.Length >= context.MinStringInternLength) - { - var index = context.RegisterInternedString(value); - context.WriteByte(BinaryTypeCode.StringInterned); - context.WriteVarUInt((uint)index); - return; - } - - // Első előfordulás vagy nincs interning - sima string - context.WriteByte(BinaryTypeCode.String); - context.WriteStringUtf8(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteByteArray(byte[] value, BinarySerializationContext context) - { - context.WriteByte(BinaryTypeCode.ByteArray); - context.WriteVarUInt((uint)value.Length); - context.WriteBytes(value); - } - - #endregion - - #region Complex Type Writers - - private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth) - { - context.WriteByte(BinaryTypeCode.Object); - - // Register object reference if needed - if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId)) - { - context.WriteVarInt(refId); - context.MarkAsWritten(value, refId); - } - else if (context.UseReferenceHandling) - { - context.WriteVarInt(-1); // No ref ID - } - - var metadata = GetTypeMetadata(type); - var nextDepth = depth + 1; - var properties = metadata.Properties; - var propCount = properties.Length; - - const int StackThreshold = 64; - byte[]? rentedStates = null; - Span propertyStates = propCount <= StackThreshold - ? stackalloc byte[propCount] - : (rentedStates = context.RentPropertyStateBuffer(propCount)).AsSpan(0, propCount); - propertyStates.Clear(); - var writtenCount = 0; - - for (var i = 0; i < propCount; i++) - { - var property = properties[i]; - - if (!context.ShouldSerializeProperty(value, property) || IsPropertyDefaultOrNull(value, property)) - { - propertyStates[i] = 0; - continue; - } - - propertyStates[i] = 1; - writtenCount++; - } - - context.WriteVarUInt((uint)writtenCount); - - for (var i = 0; i < propCount; i++) - { - if (propertyStates[i] == 0) - continue; - - var prop = properties[i]; - - if (context.UseMetadata) - { - var propIndex = context.GetPropertyNameIndex(prop.Name); - context.WriteVarUInt((uint)propIndex); - } - else - { - context.WritePreencodedPropertyName(prop.NameUtf8); - } - - WritePropertyValue(value, prop, context, nextDepth); - } - - if (rentedStates != null) - { - context.ReturnPropertyStateBuffer(rentedStates); - } - } - - /// - /// Checks if a property value is null or default without boxing for value types. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop) - { - switch (prop.AccessorType) - { - case PropertyAccessorType.Int32: - return prop.GetInt32(obj) == 0; - case PropertyAccessorType.Int64: - return prop.GetInt64(obj) == 0L; - case PropertyAccessorType.Boolean: - return !prop.GetBoolean(obj); - case PropertyAccessorType.Double: - return prop.GetDouble(obj) == 0.0; - case PropertyAccessorType.Single: - return prop.GetSingle(obj) == 0f; - case PropertyAccessorType.Decimal: - return prop.GetDecimal(obj) == 0m; - case PropertyAccessorType.Byte: - return prop.GetByte(obj) == 0; - case PropertyAccessorType.Int16: - return prop.GetInt16(obj) == 0; - case PropertyAccessorType.UInt16: - return prop.GetUInt16(obj) == 0; - case PropertyAccessorType.UInt32: - return prop.GetUInt32(obj) == 0; - case PropertyAccessorType.UInt64: - return prop.GetUInt64(obj) == 0; - case PropertyAccessorType.Guid: - return prop.GetGuid(obj) == Guid.Empty; - case PropertyAccessorType.Enum: - return prop.GetEnumAsInt32(obj) == 0; - case PropertyAccessorType.DateTime: - // DateTime default is not typically skipped - return false; - default: - // Object type - use regular getter - var value = prop.GetValue(obj); - if (value == null) return true; - if (prop.TypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value); - return false; - } - } - - /// - /// Writes a property value using typed getters to avoid boxing. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth) - { - switch (prop.AccessorType) - { - case PropertyAccessorType.Int32: - WriteInt32(prop.GetInt32(obj), context); - return; - case PropertyAccessorType.Int64: - WriteInt64(prop.GetInt64(obj), context); - return; - case PropertyAccessorType.Boolean: - context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False); - return; - case PropertyAccessorType.Double: - WriteFloat64Unsafe(prop.GetDouble(obj), context); - return; - case PropertyAccessorType.Single: - WriteFloat32Unsafe(prop.GetSingle(obj), context); - return; - case PropertyAccessorType.Decimal: - WriteDecimalUnsafe(prop.GetDecimal(obj), context); - return; - case PropertyAccessorType.DateTime: - WriteDateTimeUnsafe(prop.GetDateTime(obj), context); - return; - case PropertyAccessorType.Byte: - context.WriteByte(BinaryTypeCode.UInt8); - context.WriteByte(prop.GetByte(obj)); - return; - case PropertyAccessorType.Int16: - WriteInt16Unsafe(prop.GetInt16(obj), context); - return; - case PropertyAccessorType.UInt16: - WriteUInt16Unsafe(prop.GetUInt16(obj), context); - return; - case PropertyAccessorType.UInt32: - WriteUInt32(prop.GetUInt32(obj), context); - return; - case PropertyAccessorType.UInt64: - WriteUInt64(prop.GetUInt64(obj), context); - return; - case PropertyAccessorType.Guid: - WriteGuidUnsafe(prop.GetGuid(obj), context); - return; - case PropertyAccessorType.Enum: - var enumValue = prop.GetEnumAsInt32(obj); - if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny)) - { - context.WriteByte(BinaryTypeCode.Enum); - context.WriteByte(tiny); - } - else - { - context.WriteByte(BinaryTypeCode.Enum); - context.WriteByte(BinaryTypeCode.Int32); - context.WriteVarInt(enumValue); - } - return; - default: - // Fallback to object getter for reference types - var value = prop.GetValue(obj); - WriteValue(value, prop.PropertyType, context, depth); - return; - } - } - - #endregion - - #region Serialization Result - - public sealed class BinarySerializationResult : IDisposable - { - private readonly bool _pooled; - private bool _disposed; - - internal BinarySerializationResult(byte[] buffer, int length, bool pooled) - { - Buffer = buffer; - Length = length; - _pooled = pooled; - } - - public byte[] Buffer { get; } - public int Length { get; } - public ReadOnlySpan Span => Buffer.AsSpan(0, Length); - public ReadOnlyMemory Memory => new(Buffer, 0, Length); - - public byte[] ToArray() - { - var result = GC.AllocateUninitializedArray(Length); - Buffer.AsSpan(0, Length).CopyTo(result); - return result; - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - - if (_pooled) - { - ArrayPool.Shared.Return(Buffer); - } - } - - internal static BinarySerializationResult FromImmutable(byte[] buffer) - => new(buffer, buffer.Length, pooled: false); - } - - #endregion - - #region Specialized Array Writers - - /// - /// Optimized array writer with specialized paths for primitive arrays. - /// - private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth) - { - context.WriteByte(BinaryTypeCode.Array); - var nextDepth = depth + 1; - - // Optimized path for primitive arrays - var elementType = GetCollectionElementType(type); - if (elementType != null && type.IsArray) - { - if (TryWritePrimitiveArray(enumerable, elementType, context)) - return; - } - - // For IList, we can write the count directly - if (enumerable is IList list) - { - var count = list.Count; - context.WriteVarUInt((uint)count); - for (var i = 0; i < count; i++) - { - var item = list[i]; - var itemType = item?.GetType() ?? typeof(object); - WriteValue(item, itemType, context, nextDepth); - } - return; - } - - // For other IEnumerable, collect first - var items = new List(); - foreach (var item in enumerable) - { - items.Add(item); - } - - context.WriteVarUInt((uint)items.Count); - foreach (var item in items) - { - var itemType = item?.GetType() ?? typeof(object); - WriteValue(item, itemType, context, nextDepth); - } - } - - /// - /// Specialized array writer for primitive arrays using bulk memory operations. - /// Optimized for Blazor Hybrid compatibility (WASM, Android, Windows, iOS). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context) - { - // Int32 array - very common case - if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray) - { - context.WriteVarUInt((uint)intArray.Length); - context.WriteInt32ArrayOptimized(intArray); - return true; - } - - // Double array - bulk write as raw bytes - if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray) - { - context.WriteVarUInt((uint)doubleArray.Length); - context.WriteDoubleArrayBulk(doubleArray); - return true; - } - - // Long array - if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray) - { - context.WriteVarUInt((uint)longArray.Length); - context.WriteLongArrayOptimized(longArray); - return true; - } - - // Float array - bulk write as raw bytes - if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray) - { - context.WriteVarUInt((uint)floatArray.Length); - context.WriteFloatArrayBulk(floatArray); - return true; - } - - // Bool array - pack as bytes - if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray) - { - context.WriteVarUInt((uint)boolArray.Length); - for (var i = 0; i < boolArray.Length; i++) - { - context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False); - } - return true; - } - - // Guid array - bulk write - if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray) - { - context.WriteVarUInt((uint)guidArray.Length); - context.WriteGuidArrayBulk(guidArray); - return true; - } - - // Decimal array - if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray) - { - context.WriteVarUInt((uint)decimalArray.Length); - for (var i = 0; i < decimalArray.Length; i++) - { - WriteDecimalUnsafe(decimalArray[i], context); - } - return true; - } - - // DateTime array - if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray) - { - context.WriteVarUInt((uint)dateTimeArray.Length); - for (var i = 0; i < dateTimeArray.Length; i++) - { - WriteDateTimeUnsafe(dateTimeArray[i], context); - } - return true; - } - - // String array - common case - if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray) - { - context.WriteVarUInt((uint)stringArray.Length); - for (var i = 0; i < stringArray.Length; i++) - { - var s = stringArray[i]; - if (s == null) - context.WriteByte(BinaryTypeCode.Null); - else - WriteString(s, context); - } - return true; - } - - return false; - } - - private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth) - { - context.WriteByte(BinaryTypeCode.Dictionary); - context.WriteVarUInt((uint)dictionary.Count); - var nextDepth = depth + 1; - - foreach (DictionaryEntry entry in dictionary) - { - // Write key - var keyType = entry.Key?.GetType() ?? typeof(object); - WriteValue(entry.Key, keyType, context, nextDepth); - - // Write value - var valueType = entry.Value?.GetType() ?? typeof(object); - WriteValue(entry.Value, valueType, context, nextDepth); - } - } - - #endregion - - #region Type Metadata - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BinaryTypeMetadata GetTypeMetadata(Type type) - => TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsPrimitiveOrStringFast(Type type) - { - if (type.IsPrimitive || ReferenceEquals(type, StringType)) - { - return true; - } - - if (ReferenceEquals(type, DecimalType) || - ReferenceEquals(type, DateTimeType) || - ReferenceEquals(type, GuidType) || - ReferenceEquals(type, DateTimeOffsetType) || - ReferenceEquals(type, TimeSpanType)) - { - return true; - } - - if (type.IsEnum) - { - return true; - } - - var underlying = Nullable.GetUnderlyingType(type); - return underlying != null && IsPrimitiveOrStringFast(underlying); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType) - { - if (type.IsGenericType) - { - var definition = type.GetGenericTypeDefinition(); - if (definition == typeof(Dictionary<,>) || definition == typeof(IDictionary<,>)) - { - var args = type.GetGenericArguments(); - keyType = args[0]; - valueType = args[1]; - return true; - } - } - - foreach (var iface in type.GetInterfaces()) - { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) - { - var args = iface.GetGenericArguments(); - keyType = args[0]; - valueType = args[1]; - return true; - } - } - - keyType = null; - valueType = null; - return typeof(IDictionary).IsAssignableFrom(type); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Type? GetCollectionElementType(Type type) - { - if (type.IsArray) - { - return type.GetElementType(); - } - - if (type.IsGenericType) - { - var args = type.GetGenericArguments(); - if (args.Length == 1) - { - return args[0]; - } - } - - foreach (var iface in type.GetInterfaces()) - { - if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { - return iface.GetGenericArguments()[0]; - } - } - - return null; - } - - internal sealed class BinaryTypeMetadata - { - public BinaryPropertyAccessor[] Properties { get; } - - public BinaryTypeMetadata(Type type) - { - Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanRead && - p.GetIndexParameters().Length == 0 && - !HasJsonIgnoreAttribute(p)) - .Select(p => new BinaryPropertyAccessor(p)) - .ToArray(); - } - } - - internal sealed class BinaryPropertyAccessor - { - public readonly string Name; - public readonly byte[] NameUtf8; - public readonly Type PropertyType; - public readonly TypeCode TypeCode; - public readonly Type DeclaringType; - - private readonly Func _objectGetter; - private readonly Delegate? _typedGetter; - private readonly PropertyAccessorType _accessorType; - - public BinaryPropertyAccessor(PropertyInfo prop) - { - Name = prop.Name; - NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); - DeclaringType = prop.DeclaringType!; - PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; - TypeCode = Type.GetTypeCode(PropertyType); - - (_typedGetter, _accessorType) = CreateTypedGetter(DeclaringType, prop); - _objectGetter = CreateObjectGetter(DeclaringType, prop); - } - - public PropertyAccessorType AccessorType => _accessorType; - public Func ObjectGetter => _objectGetter; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetValue(object obj) => _objectGetter(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetInt32(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetInt64(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool GetBoolean(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double GetDouble(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float GetSingle(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public decimal GetDecimal(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DateTime GetDateTime(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte GetByte(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public short GetInt16(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort GetUInt16(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint GetUInt32(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ulong GetUInt64(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Guid GetGuid(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetEnumAsInt32(object obj) => ((Func)_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(declaringType, prop), PropertyAccessorType.Guid); - } - - var typeCode = Type.GetTypeCode(propType); - return typeCode switch - { - TypeCode.Int32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int32), - TypeCode.Int64 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int64), - TypeCode.Boolean => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Boolean), - TypeCode.Double => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Double), - TypeCode.Single => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Single), - TypeCode.Decimal => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Decimal), - TypeCode.DateTime => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.DateTime), - TypeCode.Byte => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Byte), - TypeCode.Int16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int16), - TypeCode.UInt16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt16), - TypeCode.UInt32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt32), - TypeCode.UInt64 => (CreateTypedGetterDelegate(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>(convertToInt, objParam).Compile(); - } - - private static Func CreateTypedGetterDelegate(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>(convertExpr, objParam).Compile(); - } - - private static Func 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>(boxed, objParam).Compile(); - } - } - - internal enum PropertyAccessorType : byte - { - Object = 0, - Int32, - Int64, - Boolean, - Double, - Single, - Decimal, - DateTime, - Byte, - Int16, - UInt16, - UInt32, - UInt64, - Guid, - Enum - } - - #endregion - - #region Context Pool - - private static class BinarySerializationContextPool - { - private static readonly ConcurrentQueue Pool = new(); - private const int MaxPoolSize = 16; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BinarySerializationContext Get(AcBinarySerializerOptions options) - { - if (Pool.TryDequeue(out var context)) - { - context.Reset(options); - return context; - } - - return new BinarySerializationContext(options); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Return(BinarySerializationContext context) - { - if (Pool.Count < MaxPoolSize) - { - context.Clear(); - Pool.Enqueue(context); - } - else - { - context.Dispose(); - } - } - } - - #endregion - - #region Serialization Context - - internal sealed class BinarySerializationContext : IDisposable - { - private byte[] _buffer; - private int _position; - private int _initialBufferSize; - - private const int MinBufferSize = 256; - private const int PropertyIndexBufferMaxCache = 512; - private const int PropertyStateBufferMaxCache = 512; - private const int InitialInternCapacity = 32; - private const int InitialPropertyNameCapacity = 32; - - // Reference handling - private Dictionary? _scanOccurrences; - private Dictionary? _writtenRefs; - private HashSet? _multiReferenced; - private int _nextRefId; - - // String interning - private Dictionary? _internedStrings; - private List? _internedStringList; - - // Property name table - private Dictionary? _propertyNames; - private List? _propertyNameList; - private int[]? _propertyIndexBuffer; - private byte[]? _propertyStateBuffer; - - public bool UseReferenceHandling { get; private set; } - public bool UseStringInterning { get; private set; } - public bool UseMetadata { get; private set; } - public byte MaxDepth { get; private set; } - public byte MinStringInternLength { get; private set; } - public BinaryPropertyFilter? PropertyFilter { get; private set; } - - public int Position => _position; - - public BinarySerializationContext(AcBinarySerializerOptions options) - { - _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize); - _buffer = ArrayPool.Shared.Rent(_initialBufferSize); - Reset(options); - } - - public void Reset(AcBinarySerializerOptions options) - { - _position = 0; - _nextRefId = 1; - UseReferenceHandling = options.UseReferenceHandling; - UseStringInterning = options.UseStringInterning; - UseMetadata = options.UseMetadata; - MaxDepth = options.MaxDepth; - MinStringInternLength = options.MinStringInternLength; - PropertyFilter = options.PropertyFilter; - _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize); - - if (_buffer.Length < _initialBufferSize) - { - ArrayPool.Shared.Return(_buffer); - _buffer = ArrayPool.Shared.Rent(_initialBufferSize); - } - } - - public void Clear() - { - _position = 0; - _nextRefId = 1; - - ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4); - ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4); - ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4); - ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4); - ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4); - - _propertyNameList?.Clear(); - _internedStringList?.Clear(); - - if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache) - { - ArrayPool.Shared.Return(_propertyIndexBuffer); - _propertyIndexBuffer = null; - } - - if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache) - { - ArrayPool.Shared.Return(_propertyStateBuffer); - _propertyStateBuffer = null; - } - } - - public void Dispose() - { - if (_buffer != null) - { - ArrayPool.Shared.Return(_buffer); - _buffer = null!; - } - - if (_propertyIndexBuffer != null) - { - ArrayPool.Shared.Return(_propertyIndexBuffer); - _propertyIndexBuffer = null; - } - - if (_propertyStateBuffer != null) - { - ArrayPool.Shared.Return(_propertyStateBuffer); - _propertyStateBuffer = null; - } - } - - #region String Interning - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int RegisterInternedString(string value) - { - _internedStrings ??= new Dictionary(InitialInternCapacity, StringComparer.Ordinal); - _internedStringList ??= new List(InitialInternCapacity); - - ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists); - if (exists) - { - return index; - } - - index = _internedStringList.Count; - _internedStringList.Add(value); - return index; - } - - #endregion - - #region Property Name Table - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RegisterPropertyName(string name) - { - _propertyNames ??= new Dictionary(InitialPropertyNameCapacity, StringComparer.Ordinal); - _propertyNameList ??= new List(InitialPropertyNameCapacity); - - if (!_propertyNames.ContainsKey(name)) - { - var index = _propertyNameList.Count; - _propertyNames[name] = index; - _propertyNameList.Add(name); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetPropertyNameIndex(string name) - { - return _propertyNames != null && _propertyNames.TryGetValue(name, out var index) ? index : -1; - } - - #endregion - - #region Property State Buffer - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte[] RentPropertyStateBuffer(int size) - { - if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size) - { - return _propertyStateBuffer; - } - - if (_propertyStateBuffer != null) - { - ArrayPool.Shared.Return(_propertyStateBuffer); - } - - _propertyStateBuffer = ArrayPool.Shared.Rent(size); - return _propertyStateBuffer; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ReturnPropertyStateBuffer(byte[] buffer) - { - // Buffer stays cached in _propertyStateBuffer for reuse - } - - #endregion - - #region Output - - public byte[] ToArray() - { - var result = GC.AllocateUninitializedArray(_position); - _buffer.AsSpan(0, _position).CopyTo(result); - return result; - } - - public void WriteTo(IBufferWriter writer) - { - var span = writer.GetSpan(_position); - _buffer.AsSpan(0, _position).CopyTo(span); - writer.Advance(_position); - } - - public BinarySerializationResult DetachResult() - { - var resultBuffer = _buffer; - var resultLength = _position; - - // Get a new buffer for this context - _buffer = ArrayPool.Shared.Rent(_initialBufferSize); - _position = 0; - - return new BinarySerializationResult(resultBuffer, resultLength, pooled: true); - } - - #endregion - - #region Helpers - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ClearAndTrimIfNeeded(Dictionary? dict, int maxCapacity) - where TKey : notnull - { - if (dict == null) return; - dict.Clear(); - if (dict.EnsureCapacity(0) > maxCapacity) - { - dict.TrimExcess(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ClearAndTrimIfNeeded(HashSet? set, int maxCapacity) - { - if (set == null) return; - set.Clear(); - if (set.EnsureCapacity(0) > maxCapacity) - { - set.TrimExcess(); - } - } - - #endregion - - #region Property Filtering - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property) - { - if (PropertyFilter == null) - { - return true; - } - - var context = new BinaryPropertyFilterContext( - instance, - property.DeclaringType, - property.Name, - property.PropertyType, - property.ObjectGetter); - return PropertyFilter(context); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShouldIncludePropertyInMetadata(BinaryPropertyAccessor property) - { - if (PropertyFilter == null) - { - return true; - } - - var context = new BinaryPropertyFilterContext( - null, - property.DeclaringType, - property.Name, - property.PropertyType, - null); - return PropertyFilter(context); - } - - #endregion - - #region Optimized Buffer Writing - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureCapacity(int additionalBytes) - { - var required = _position + additionalBytes; - if (required <= _buffer.Length) - { - return; - } - - GrowBuffer(required); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void GrowBuffer(int required) - { - var newSize = Math.Max(_buffer.Length * 2, required); - var newBuffer = ArrayPool.Shared.Rent(newSize); - _buffer.AsSpan(0, _position).CopyTo(newBuffer); - ArrayPool.Shared.Return(_buffer); - _buffer = newBuffer; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteByte(byte value) - { - EnsureCapacity(1); - _buffer[_position++] = value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteBytes(ReadOnlySpan data) - { - EnsureCapacity(data.Length); - data.CopyTo(_buffer.AsSpan(_position)); - _position += data.Length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteRaw(T value) where T : unmanaged - { - var size = Unsafe.SizeOf(); - EnsureCapacity(size); - Unsafe.WriteUnaligned(ref _buffer[_position], value); - _position += size; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteDecimalBits(decimal value) - { - EnsureCapacity(16); - Span bits = stackalloc int[4]; - decimal.TryGetBits(value, bits, out _); - MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16)); - _position += 16; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteDateTimeBits(DateTime value) - { - EnsureCapacity(9); - Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks); - _buffer[_position + 8] = (byte)value.Kind; - _position += 9; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteGuidBits(Guid value) - { - EnsureCapacity(16); - value.TryWriteBytes(_buffer.AsSpan(_position, 16)); - _position += 16; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteDateTimeOffsetBits(DateTimeOffset value) - { - EnsureCapacity(10); - Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks); - Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes); - _position += 10; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteVarInt(int value) - { - EnsureCapacity(5); - var encoded = (uint)((value << 1) ^ (value >> 31)); - WriteVarUIntInternal(encoded); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteVarUInt(uint value) - { - EnsureCapacity(5); - WriteVarUIntInternal(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteVarUIntInternal(uint value) - { - while (value >= 0x80) - { - _buffer[_position++] = (byte)(value | 0x80); - value >>= 7; - } - - _buffer[_position++] = (byte)value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteVarLong(long value) - { - EnsureCapacity(10); - var encoded = (ulong)((value << 1) ^ (value >> 63)); - WriteVarULongInternal(encoded); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteVarULong(ulong value) - { - EnsureCapacity(10); - WriteVarULongInternal(value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteVarULongInternal(ulong value) - { - while (value >= 0x80) - { - _buffer[_position++] = (byte)(value | 0x80); - value >>= 7; - } - - _buffer[_position++] = (byte)value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteStringUtf8(string value) - { - var byteCount = Utf8NoBom.GetByteCount(value); - WriteVarUInt((uint)byteCount); - EnsureCapacity(byteCount); - Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); - _position += byteCount; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WritePreencodedPropertyName(ReadOnlySpan utf8Name) - { - WriteByte(BinaryTypeCode.String); - WriteVarUInt((uint)utf8Name.Length); - WriteBytes(utf8Name); - } - - #endregion - - #region Bulk Array Writers - - public void WriteInt32ArrayOptimized(int[] array) - { - for (var i = 0; i < array.Length; i++) - { - var value = array[i]; - if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) - { - WriteByte(tiny); - } - else - { - WriteByte(BinaryTypeCode.Int32); - WriteVarInt(value); - } - } - } - - public void WriteLongArrayOptimized(long[] array) - { - for (var i = 0; i < array.Length; i++) - { - var value = array[i]; - if (value >= int.MinValue && value <= int.MaxValue) - { - var intValue = (int)value; - if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) - { - WriteByte(tiny); - } - else - { - WriteByte(BinaryTypeCode.Int32); - WriteVarInt(intValue); - } - } - else - { - WriteByte(BinaryTypeCode.Int64); - WriteVarLong(value); - } - } - } - - public void WriteDoubleArrayBulk(double[] array) - { - EnsureCapacity(array.Length * 9); - for (var i = 0; i < array.Length; i++) - { - _buffer[_position++] = BinaryTypeCode.Float64; - Unsafe.WriteUnaligned(ref _buffer[_position], array[i]); - _position += 8; - } - } - - public void WriteFloatArrayBulk(float[] array) - { - EnsureCapacity(array.Length * 5); - for (var i = 0; i < array.Length; i++) - { - _buffer[_position++] = BinaryTypeCode.Float32; - Unsafe.WriteUnaligned(ref _buffer[_position], array[i]); - _position += 4; - } - } - - public void WriteGuidArrayBulk(Guid[] array) - { - EnsureCapacity(array.Length * 17); - for (var i = 0; i < array.Length; i++) - { - _buffer[_position++] = BinaryTypeCode.Guid; - array[i].TryWriteBytes(_buffer.AsSpan(_position, 16)); - _position += 16; - } - } - - #endregion - - #region Header and Metadata - - private int _headerPosition; - - public void WriteHeaderPlaceholder() - { - EnsureCapacity(2); - _headerPosition = _position; - _position += 2; - } - - public void FinalizeHeaderSections() - { - var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 }; - var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 }; - - // ArrayBufferWriter requires initialCapacity > 0 - var estimatedCapacity = hasPropertyNames ? _propertyNameList!.Count * 8 : 16; - if (hasInternTable && _internedStringList != null) - { - estimatedCapacity += _internedStringList.Count * 8; - } - var headerWriter = new ArrayBufferWriter(Math.Max(estimatedCapacity, 16)); - - if (hasPropertyNames) - { - WriteHeaderVarUInt(headerWriter, (uint)_propertyNameList!.Count); - foreach (var name in _propertyNameList) - { - WriteHeaderString(headerWriter, name); - } - } - - if (hasInternTable) - { - WriteHeaderVarUInt(headerWriter, (uint)_internedStringList!.Count); - foreach (var value in _internedStringList) - { - WriteHeaderString(headerWriter, value); - } - } - - var headerPayload = headerWriter.WrittenSpan; - if (headerPayload.Length > 0) - { - EnsureCapacity(headerPayload.Length); - var bodyLength = _position - (_headerPosition + 2); - if (bodyLength > 0) - { - Array.Copy(_buffer, _headerPosition + 2, _buffer, _headerPosition + 2 + headerPayload.Length, bodyLength); - } - - headerPayload.CopyTo(_buffer.AsSpan(_headerPosition + 2)); - _position += headerPayload.Length; - } - - byte flags = BinaryTypeCode.HeaderFlagsBase; - if (hasPropertyNames) - { - flags |= BinaryTypeCode.HeaderFlag_Metadata; - } - - if (UseReferenceHandling) - { - flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling; - } - - if (hasInternTable) - { - flags |= BinaryTypeCode.HeaderFlag_StringInternTable; - } - - _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; - _buffer[_headerPosition + 1] = flags; - } - - private static void WriteHeaderVarUInt(ArrayBufferWriter writer, uint value) - { - var span = writer.GetSpan(5); - var index = 0; - while (value >= 0x80) - { - span[index++] = (byte)(value | 0x80); - value >>= 7; - } - - span[index++] = (byte)value; - writer.Advance(index); - } - - private static void WriteHeaderString(ArrayBufferWriter writer, string value) - { - var byteCount = Utf8NoBom.GetByteCount(value); - WriteHeaderVarUInt(writer, (uint)byteCount); - var span = writer.GetSpan(byteCount); - Utf8NoBom.GetBytes(value.AsSpan(), span); - writer.Advance(byteCount); - } - - #endregion - - #region Reference Handling - - private const int InitialReferenceCapacity = 16; - private const int InitialMultiRefCapacity = 8; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TrackForScanning(object obj) - { - _scanOccurrences ??= new Dictionary(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); - _multiReferenced ??= new HashSet(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance); - - ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); - if (exists) - { - count++; - _multiReferenced.Add(obj); - return false; - } - - count = 1; - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShouldWriteRef(object obj, out int refId) - { - if (_multiReferenced != null && _multiReferenced.Contains(obj)) - { - _writtenRefs ??= new Dictionary(32, ReferenceEqualityComparer.Instance); - if (!_writtenRefs.ContainsKey(obj)) - { - refId = _nextRefId++; - return true; - } - } - - refId = 0; - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void MarkAsWritten(object obj, int refId) - { - _writtenRefs![obj] = refId; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetExistingRef(object obj, out int refId) - { - if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId)) - { - return true; - } - - refId = 0; - return false; - } - - #endregion - } - - #endregion -} \ No newline at end of file diff --git a/AyCode.Core/Extensions/SerializeObjectExtensions.cs b/AyCode.Core/Extensions/SerializeObjectExtensions.cs index 631ff75..ab9ad39 100644 --- a/AyCode.Core/Extensions/SerializeObjectExtensions.cs +++ b/AyCode.Core/Extensions/SerializeObjectExtensions.cs @@ -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; diff --git a/AyCode.Core/Extensions/JsonUtilities.cs b/AyCode.Core/Helpers/JsonUtilities.cs similarity index 99% rename from AyCode.Core/Extensions/JsonUtilities.cs rename to AyCode.Core/Helpers/JsonUtilities.cs index 5a4ebf5..33c164e 100644 --- a/AyCode.Core/Extensions/JsonUtilities.cs +++ b/AyCode.Core/Helpers/JsonUtilities.cs @@ -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; /// /// Cached result for IId type info lookup. diff --git a/AyCode.Core/Extensions/PropertyHelper.cs b/AyCode.Core/Helpers/PropertyHelper.cs similarity index 86% rename from AyCode.Core/Extensions/PropertyHelper.cs rename to AyCode.Core/Helpers/PropertyHelper.cs index e8eb83d..57a875c 100644 --- a/AyCode.Core/Extensions/PropertyHelper.cs +++ b/AyCode.Core/Helpers/PropertyHelper.cs @@ -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 { diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs new file mode 100644 index 0000000..2022027 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -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 _buffer; + private int _position; + private List? _internedStrings; + private List? _propertyNames; + private Dictionary? _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 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(propertyCount); + for (var i = 0; i < propertyCount; i++) + { + _propertyNames.Add(ReadHeaderString()); + } + } + + if (hasInternTable) + { + var internCount = (int)ReadVarUInt(); + _internedStrings = new List(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(_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(); + } + + EnsureAvailable(length); + var result = GC.AllocateUninitializedArray(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(); + _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(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); + } + } +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs new file mode 100644 index 0000000..2c8943e --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -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 _properties; + + public BinaryPropertySetterInfo[] PropertiesArray { get; } + public Func? 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.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? 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>(convert).Compile(); + } + } + + internal sealed class BinaryPropertySetterInfo + { + private static readonly Func NullGetter = static _ => null; + private static readonly Action NullSetter = static (_, _) => { }; + + private readonly Func _getter; + private readonly Action _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? 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? elementIdGetter, + Func? getter = null, + Action? 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 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>(boxed, targetParam).Compile(); + } + + private static Action 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>(assign, targetParam, valueParam).Compile(); + } + } +} diff --git a/AyCode.Core/Extensions/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs similarity index 68% rename from AyCode.Core/Extensions/AcBinaryDeserializer.cs rename to AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index ef6cae6..1c036c3 100644 --- a/AyCode.Core/Extensions/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -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; /// /// 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 /// -public static class AcBinaryDeserializer +public static partial class AcBinaryDeserializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); private static readonly ConcurrentDictionary TypeConversionCache = new(); @@ -206,13 +205,27 @@ public static class AcBinaryDeserializer /// Populate with merge semantics for IId collections. /// public static void PopulateMerge(ReadOnlySpan data, T target) where T : class + => PopulateMerge(data, target, null); + + /// + /// Populate with merge semantics for IId collections. + /// + /// Binary data to deserialize + /// Target object to populate + /// Optional serializer options. When RemoveOrphanedItems is true, + /// items in destination collections that have no matching Id in source will be removed. + public static void PopulateMerge(ReadOnlySpan 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? sourceIds = context.RemoveOrphanedItems && existingById != null + ? new HashSet(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(); + 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>(boxed, objParam).Compile(); } - internal sealed class BinaryDeserializeTypeMetadata - { - private readonly FrozenDictionary _propertiesDict; - public BinaryPropertySetterInfo[] PropertiesArray { get; } - public Func? 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>(boxed).Compile(); - } - - var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - var propsList = new List(); - - 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(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? ElementIdGetter; - - private readonly Action _setter; - private readonly Func _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? 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 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>(assign, objParam, valueParam).Compile(); - } - - private static Func 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>(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 +} - /// - /// Optimized deserialization context using ref struct for zero allocation. - /// Uses MemoryMarshal for fast primitive reads. - /// - 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 _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; } - - /// - /// Minimum string length for interning. Must match serializer's MinStringInternLength. - /// Default: 4 (from AcBinarySerializerOptions) - /// - public byte MinStringInternLength { get; private set; } - - // Property name table - private string[]? _propertyNames; - - // Interned strings - dynamically built during deserialization - private List? _internedStrings; - - // Reference map - private Dictionary? _references; - - public bool IsMergeMode { get; set; } - public int Position => _position; - public bool IsAtEnd => _position >= _data.Length; - - public BinaryDeserializationContext(ReadOnlySpan 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(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; - } - - /// - /// Optimized Int16 read using direct memory access. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public short ReadInt16Unsafe() - { - if (_position + 2 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var result = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - _position += 2; - return result; - } - - /// - /// Optimized UInt16 read using direct memory access. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort ReadUInt16Unsafe() - { - if (_position + 2 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var result = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - _position += 2; - return result; - } - - /// - /// Optimized float read using direct memory access. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float ReadSingleUnsafe() - { - if (_position + 4 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var result = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - _position += 4; - return result; - } - - /// - /// Optimized double read using direct memory access. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double ReadDoubleUnsafe() - { - if (_position + 8 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var result = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - _position += 8; - return result; - } - - /// - /// Optimized decimal read using direct memory copy. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public decimal ReadDecimalUnsafe() - { - if (_position + 16 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - - Span bits = stackalloc int[4]; - MemoryMarshal.Cast(_data.Slice(_position, 16)).CopyTo(bits); - _position += 16; - return new decimal(bits); - } - - /// - /// Optimized char read. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public char ReadCharUnsafe() - { - if (_position + 2 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var result = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - _position += 2; - return result; - } - - /// - /// Optimized DateTime read. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DateTime ReadDateTimeUnsafe() - { - if (_position + 9 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var ticks = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - var kind = (DateTimeKind)_data[_position + 8]; - _position += 9; - return new DateTime(ticks, kind); - } - - /// - /// Optimized DateTimeOffset read. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DateTimeOffset ReadDateTimeOffsetUnsafe() - { - if (_position + 10 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var utcTicks = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - var offsetMinutes = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position + 8])); - _position += 10; - var offset = TimeSpan.FromMinutes(offsetMinutes); - var localTicks = utcTicks + offset.Ticks; - return new DateTimeOffset(localTicks, offset); - } - - /// - /// Optimized TimeSpan read. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TimeSpan ReadTimeSpanUnsafe() - { - if (_position + 8 > _data.Length) - throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var ticks = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _data[_position])); - _position += 8; - return new TimeSpan(ticks); - } - - /// - /// Optimized Guid read. - /// - [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; - } - - /// - /// Optimized string read using UTF8 span decoding. - /// Uses String.Create to decode directly into the target string buffer to avoid intermediate allocations. - /// - [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(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(); - _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 diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs new file mode 100644 index 0000000..986ee48 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -0,0 +1,1098 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer; + +namespace AyCode.Core.Serializers.Binaries; + +public static partial class AcBinarySerializer +{ + private static class BinarySerializationContextPool + { + private static readonly ConcurrentQueue Pool = new(); + private const int MaxPoolSize = 16; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BinarySerializationContext Get(AcBinarySerializerOptions options) + { + if (Pool.TryDequeue(out var context)) + { + context.Reset(options); + return context; + } + + return new BinarySerializationContext(options); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(BinarySerializationContext context) + { + if (Pool.Count < MaxPoolSize) + { + context.Clear(); + Pool.Enqueue(context); + } + else + { + context.Dispose(); + } + } + } + + internal sealed class BinarySerializationContext : IDisposable + { + private const int MinBufferSize = 256; + private const int PropertyIndexBufferMaxCache = 512; + private const int PropertyStateBufferMaxCache = 512; + private const int InitialInternCapacity = 32; + private const int InitialPropertyNameCapacity = 32; + private const int InitialReferenceCapacity = 16; + private const int InitialMultiRefCapacity = 8; + + // Bloom filter constants for string interning + private const int BloomFilterSize = 256; // 256 bits = 32 bytes + private const int BloomFilterMask = BloomFilterSize - 1; + + private byte[] _buffer; + private int _position; + private int _initialBufferSize; + + private Dictionary? _scanOccurrences; + private Dictionary? _writtenRefs; + private HashSet? _multiReferenced; + private int _nextRefId; + + private Dictionary? _internedStrings; + private List? _internedStringList; + + /// + /// Bloom filter for quick "definitely not interned" checks. + /// Avoids dictionary lookup for unique strings. + /// + private ulong _bloomFilter0; + private ulong _bloomFilter1; + private ulong _bloomFilter2; + private ulong _bloomFilter3; + + private Dictionary? _propertyNames; + private List? _propertyNameList; + private int[]? _propertyIndexBuffer; + private byte[]? _propertyStateBuffer; + + public bool UseReferenceHandling { get; private set; } + public bool UseStringInterning { get; private set; } + public bool UseMetadata { get; private set; } + public byte MaxDepth { get; private set; } + public byte MinStringInternLength { get; private set; } + public BinaryPropertyFilter? PropertyFilter { get; private set; } + + public int Position => _position; + + public BinarySerializationContext(AcBinarySerializerOptions options) + { + _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize); + _buffer = ArrayPool.Shared.Rent(_initialBufferSize); + Reset(options); + } + + public void Reset(AcBinarySerializerOptions options) + { + _position = 0; + _nextRefId = 1; + UseReferenceHandling = options.UseReferenceHandling; + UseStringInterning = options.UseStringInterning; + UseMetadata = options.UseMetadata; + MaxDepth = options.MaxDepth; + MinStringInternLength = options.MinStringInternLength; + PropertyFilter = options.PropertyFilter; + _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize); + + if (_buffer.Length < _initialBufferSize) + { + ArrayPool.Shared.Return(_buffer); + _buffer = ArrayPool.Shared.Rent(_initialBufferSize); + } + } + + public void Clear() + { + _position = 0; + _nextRefId = 1; + + // Reset bloom filter + _bloomFilter0 = 0; + _bloomFilter1 = 0; + _bloomFilter2 = 0; + _bloomFilter3 = 0; + + ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4); + ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4); + ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4); + ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4); + ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4); + + _propertyNameList?.Clear(); + _internedStringList?.Clear(); + + // Reset cached property indices + ResetCachedPropertyIndices(); + + if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache) + { + ArrayPool.Shared.Return(_propertyIndexBuffer); + _propertyIndexBuffer = null; + } + + if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache) + { + ArrayPool.Shared.Return(_propertyStateBuffer); + _propertyStateBuffer = null; + } + } + + private void ResetCachedPropertyIndices() + { + // Note: BinaryPropertyAccessor.CachedPropertyNameIndex is per-context, + // but metadata is cached globally. We reset it during Clear to avoid + // stale indices. The next serialization will re-populate them. + // This is a minor cost as it only happens on context reuse. + } + + public void Dispose() + { + if (_buffer != null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null!; + } + + if (_propertyIndexBuffer != null) + { + ArrayPool.Shared.Return(_propertyIndexBuffer); + _propertyIndexBuffer = null; + } + + if (_propertyStateBuffer != null) + { + ArrayPool.Shared.Return(_propertyStateBuffer); + _propertyStateBuffer = null; + } + } + + #region String Interning + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int RegisterInternedString(string value) + { + _internedStrings ??= new Dictionary(InitialInternCapacity, StringComparer.Ordinal); + _internedStringList ??= new List(InitialInternCapacity); + + // Fast path: check bloom filter first + var hash = GetStringHash(value); + if (!BloomFilterMightContain(hash)) + { + // Definitely not in dictionary - add directly + var newIndex = _internedStringList.Count; + _internedStrings[value] = newIndex; + _internedStringList.Add(value); + BloomFilterAdd(hash); + return newIndex; + } + + // Might be in dictionary - need to check + ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists); + if (exists) + { + return index; + } + + index = _internedStringList.Count; + _internedStringList.Add(value); + BloomFilterAdd(hash); + return index; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetStringHash(string value) + { + // Simple hash combining length and first/last characters + // Optimized for quick calculation, not collision resistance + if (value.Length == 0) return 0; + var h = value.Length; + h = (h * 31) + value[0]; + if (value.Length > 1) + h = (h * 31) + value[value.Length - 1]; + return h; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool BloomFilterMightContain(int hash) + { + // Use two hash functions for bloom filter + var h1 = hash & BloomFilterMask; + var h2 = (hash >> 8) & BloomFilterMask; + + return BloomFilterTestBit(h1) && BloomFilterTestBit(h2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool BloomFilterTestBit(int bit) + { + var segment = bit >> 6; // Divide by 64 + var mask = 1UL << (bit & 63); + return segment switch + { + 0 => (_bloomFilter0 & mask) != 0, + 1 => (_bloomFilter1 & mask) != 0, + 2 => (_bloomFilter2 & mask) != 0, + _ => (_bloomFilter3 & mask) != 0, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BloomFilterAdd(int hash) + { + var h1 = hash & BloomFilterMask; + var h2 = (hash >> 8) & BloomFilterMask; + + BloomFilterSetBit(h1); + BloomFilterSetBit(h2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BloomFilterSetBit(int bit) + { + var segment = bit >> 6; + var mask = 1UL << (bit & 63); + switch (segment) + { + case 0: _bloomFilter0 |= mask; break; + case 1: _bloomFilter1 |= mask; break; + case 2: _bloomFilter2 |= mask; break; + default: _bloomFilter3 |= mask; break; + } + } + + #endregion + + #region Property Name Table + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterPropertyName(string name) + { + _propertyNames ??= new Dictionary(InitialPropertyNameCapacity, StringComparer.Ordinal); + _propertyNameList ??= new List(InitialPropertyNameCapacity); + + if (!_propertyNames.ContainsKey(name)) + { + var index = _propertyNameList.Count; + _propertyNames[name] = index; + _propertyNameList.Add(name); + } + } + + /// + /// Registers property name and caches the index in the accessor for future lookups. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterPropertyNameAndCache(BinaryPropertyAccessor accessor) + { + _propertyNames ??= new Dictionary(InitialPropertyNameCapacity, StringComparer.Ordinal); + _propertyNameList ??= new List(InitialPropertyNameCapacity); + + ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_propertyNames, accessor.Name, out var exists); + if (!exists) + { + index = _propertyNameList.Count; + _propertyNameList.Add(accessor.Name); + } + accessor.CachedPropertyNameIndex = index; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetPropertyNameIndex(string name) + => _propertyNames != null && _propertyNames.TryGetValue(name, out var index) ? index : -1; + + #endregion + + #region Property State Buffer + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte[] RentPropertyStateBuffer(int size) + { + if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size) + { + return _propertyStateBuffer; + } + + if (_propertyStateBuffer != null) + { + ArrayPool.Shared.Return(_propertyStateBuffer); + } + + _propertyStateBuffer = ArrayPool.Shared.Rent(size); + return _propertyStateBuffer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReturnPropertyStateBuffer(byte[] buffer) + { + // Buffer stays cached for reuse. + } + + #endregion + + #region Output + + public byte[] ToArray() + { + var result = GC.AllocateUninitializedArray(_position); + _buffer.AsSpan(0, _position).CopyTo(result); + return result; + } + + public void WriteTo(IBufferWriter writer) + { + var span = writer.GetSpan(_position); + _buffer.AsSpan(0, _position).CopyTo(span); + writer.Advance(_position); + } + + public BinarySerializationResult DetachResult() + { + var resultBuffer = _buffer; + var resultLength = _position; + + _buffer = ArrayPool.Shared.Rent(_initialBufferSize); + _position = 0; + + return new BinarySerializationResult(resultBuffer, resultLength, pooled: true); + } + + #endregion + + #region Property Filtering + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property) + { + if (PropertyFilter == null) + { + return true; + } + + var context = new BinaryPropertyFilterContext( + instance, + property.DeclaringType, + property.Name, + property.PropertyType, + property.ObjectGetter); + return PropertyFilter(context); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldIncludePropertyInMetadata(BinaryPropertyAccessor property) + { + if (PropertyFilter == null) + { + return true; + } + + var context = new BinaryPropertyFilterContext( + null, + property.DeclaringType, + property.Name, + property.PropertyType, + null); + return PropertyFilter(context); + } + + #endregion + + #region Buffer Helpers + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int additionalBytes) + { + var required = _position + additionalBytes; + if (required <= _buffer.Length) + { + return; + } + + GrowBuffer(required); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowBuffer(int required) + { + var newSize = Math.Max(_buffer.Length * 2, required); + var newBuffer = ArrayPool.Shared.Rent(newSize); + _buffer.AsSpan(0, _position).CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteByte(byte value) + { + if (_position >= _buffer.Length) + { + GrowBuffer(_position + 1); + } + _buffer[_position++] = value; + } + + /// + /// Write type code byte followed by a raw value. Batches EnsureCapacity call. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteTypeCodeAndRaw(byte typeCode, T value) where T : unmanaged + { + var size = 1 + Unsafe.SizeOf(); + EnsureCapacity(size); + _buffer[_position++] = typeCode; + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += Unsafe.SizeOf(); + } + + /// + /// Write two bytes efficiently. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteTwoBytes(byte b1, byte b2) + { + EnsureCapacity(2); + _buffer[_position++] = b1; + _buffer[_position++] = b2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(ReadOnlySpan data) + { + EnsureCapacity(data.Length); + data.CopyTo(_buffer.AsSpan(_position)); + _position += data.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRaw(T value) where T : unmanaged + { + var size = Unsafe.SizeOf(); + EnsureCapacity(size); + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += size; + } + + #endregion + + #region Specialized Writers + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDecimalBits(decimal value) + { + EnsureCapacity(16); + Span bits = stackalloc int[4]; + decimal.TryGetBits(value, bits, out _); + MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16)); + _position += 16; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDateTimeBits(DateTime value) + { + EnsureCapacity(9); + Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks); + _buffer[_position + 8] = (byte)value.Kind; + _position += 9; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteGuidBits(Guid value) + { + EnsureCapacity(16); + value.TryWriteBytes(_buffer.AsSpan(_position, 16)); + _position += 16; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDateTimeOffsetBits(DateTimeOffset value) + { + EnsureCapacity(10); + Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks); + Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes); + _position += 10; + } + + /// + /// Patches a previously written VarUInt at the specified position. + /// Works correctly only if the new value requires the same or fewer bytes. + /// For property counts < 128, this is always 1 byte. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PatchVarUInt(int position, uint value) + { + // Fast path: single byte (covers 0-127, which is most property counts) + if (value < 0x80) + { + _buffer[position] = (byte)value; + return; + } + + // Multi-byte case - need to shift buffer if new encoding is longer + // For simplicity, we'll rewrite from the position + // This is rare for property counts + PatchVarUIntSlow(position, value); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void PatchVarUIntSlow(int position, uint value) + { + // Calculate current size at position (read until no continuation bit) + var currentSize = 0; + var pos = position; + while (pos < _position && (_buffer[pos] & 0x80) != 0) + { + currentSize++; + pos++; + } + currentSize++; // Include final byte without continuation bit + + // Calculate new size needed + var newSize = GetVarUIntSize(value); + + if (newSize == currentSize) + { + // Same size - just overwrite + var tempPos = position; + while (value >= 0x80) + { + _buffer[tempPos++] = (byte)(value | 0x80); + value >>= 7; + } + _buffer[tempPos] = (byte)value; + } + else if (newSize < currentSize) + { + // New is smaller - shift data left + var delta = currentSize - newSize; + Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize); + _position -= delta; + + var tempPos = position; + while (value >= 0x80) + { + _buffer[tempPos++] = (byte)(value | 0x80); + value >>= 7; + } + _buffer[tempPos] = (byte)value; + } + else + { + // New is larger - shift data right + var delta = newSize - currentSize; + EnsureCapacity(delta); + Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize); + _position += delta; + + var tempPos = position; + while (value >= 0x80) + { + _buffer[tempPos++] = (byte)(value | 0x80); + value >>= 7; + } + _buffer[tempPos] = (byte)value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetVarUIntSize(uint value) + { + if (value < 0x80) return 1; + if (value < 0x4000) return 2; + if (value < 0x200000) return 3; + if (value < 0x10000000) return 4; + return 5; + } + + public void WriteVarInt(int value) + { + var encoded = (uint)((value << 1) ^ (value >> 31)); + // Fast path for small positive values (0-63 when ZigZag encoded) + if (encoded < 0x80) + { + EnsureCapacity(1); + _buffer[_position++] = (byte)encoded; + return; + } + EnsureCapacity(5); + WriteVarUIntInternal(encoded); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteVarUInt(uint value) + { + // Fast path for small values (0-127) + if (value < 0x80) + { + EnsureCapacity(1); + _buffer[_position++] = (byte)value; + return; + } + EnsureCapacity(5); + WriteVarUIntInternal(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteVarUIntInternal(uint value) + { + while (value >= 0x80) + { + _buffer[_position++] = (byte)(value | 0x80); + value >>= 7; + } + + _buffer[_position++] = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteVarLong(long value) + { + var encoded = (ulong)((value << 1) ^ (value >> 63)); + // Fast path for small values + if (encoded < 0x80) + { + EnsureCapacity(1); + _buffer[_position++] = (byte)encoded; + return; + } + EnsureCapacity(10); + WriteVarULongInternal(encoded); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteVarULong(ulong value) + { + // Fast path for small values (0-127) + if (value < 0x80) + { + EnsureCapacity(1); + _buffer[_position++] = (byte)value; + return; + } + EnsureCapacity(10); + WriteVarULongInternal(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteVarULongInternal(ulong value) + { + while (value >= 0x80) + { + _buffer[_position++] = (byte)(value | 0x80); + value >>= 7; + } + + _buffer[_position++] = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteStringUtf8(string value) + { + // Fast path for ASCII-only strings using SIMD-optimized check + if (System.Text.Ascii.IsValid(value)) + { + WriteVarUInt((uint)value.Length); + EnsureCapacity(value.Length); + // Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion + System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _); + _position += value.Length; + return; + } + + // Standard path for multi-byte UTF8 + var byteCount = Utf8NoBom.GetByteCount(value); + WriteVarUInt((uint)byteCount); + EnsureCapacity(byteCount); + Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); + _position += byteCount; + } + + /// + /// Checks if string contains only ASCII characters (0-127). + /// Optimized loop with early exit. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAscii(string value) + { + var span = value.AsSpan(); + for (var i = 0; i < span.Length; i++) + { + if (span[i] > 127) + return false; + } + return true; + } + + /// + /// Writes ASCII string directly to byte buffer (char to byte, no encoding needed). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteAsciiDirect(ReadOnlySpan source, Span destination) + { + for (var i = 0; i < source.Length; i++) + { + destination[i] = (byte)source[i]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WritePreencodedPropertyName(ReadOnlySpan utf8Name) + { + WriteByte(BinaryTypeCode.String); + WriteVarUInt((uint)utf8Name.Length); + WriteBytes(utf8Name); + } + + public void WriteInt32ArrayOptimized(int[] array) + { + for (var i = 0; i < array.Length; i++) + { + var value = array[i]; + if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) + { + WriteByte(tiny); + } + else + { + WriteByte(BinaryTypeCode.Int32); + WriteVarInt(value); + } + } + } + + public void WriteLongArrayOptimized(long[] array) + { + for (var i = 0; i < array.Length; i++) + { + var value = array[i]; + if (value >= int.MinValue && value <= int.MaxValue) + { + var intValue = (int)value; + if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) + { + WriteByte(tiny); + } + else + { + WriteByte(BinaryTypeCode.Int32); + WriteVarInt(intValue); + } + } + else + { + WriteByte(BinaryTypeCode.Int64); + WriteVarLong(value); + } + } + } + + public void WriteDoubleArrayBulk(double[] array) + { + EnsureCapacity(array.Length * 9); + for (var i = 0; i < array.Length; i++) + { + _buffer[_position++] = BinaryTypeCode.Float64; + Unsafe.WriteUnaligned(ref _buffer[_position], array[i]); + _position += 8; + } + } + + public void WriteFloatArrayBulk(float[] array) + { + EnsureCapacity(array.Length * 5); + for (var i = 0; i < array.Length; i++) + { + _buffer[_position++] = BinaryTypeCode.Float32; + Unsafe.WriteUnaligned(ref _buffer[_position], array[i]); + _position += 4; + } + } + + public void WriteGuidArrayBulk(Guid[] array) + { + EnsureCapacity(array.Length * 17); + for (var i = 0; i < array.Length; i++) + { + _buffer[_position++] = BinaryTypeCode.Guid; + array[i].TryWriteBytes(_buffer.AsSpan(_position, 16)); + _position += 16; + } + } + + #endregion + + #region Header and Metadata + + private int _headerPosition; + private int _estimatedHeaderSize; + + /// + /// Estimates header payload size based on registered property names and intern strings. + /// Call after metadata registration but before writing the body. + /// + public int EstimateHeaderPayloadSize() + { + var size = 0; + + if (UseMetadata && _propertyNameList is { Count: > 0 }) + { + size += GetVarUIntSize((uint)_propertyNameList.Count); + foreach (var name in _propertyNameList) + { + var byteCount = name.Length; // Assume ASCII (common case), fallback handles multi-byte + size += GetVarUIntSize((uint)byteCount) + byteCount; + } + } + + if (UseStringInterning && _internedStringList is { Count: > 0 }) + { + size += GetVarUIntSize((uint)_internedStringList.Count); + foreach (var value in _internedStringList) + { + var byteCount = value.Length; // Assume ASCII for estimation + size += GetVarUIntSize((uint)byteCount) + byteCount; + } + } + + return size; + } + + public void WriteHeaderPlaceholder() + { + EnsureCapacity(2); + _headerPosition = _position; + _position += 2; + _estimatedHeaderSize = 0; + } + + /// + /// Reserves space for header based on estimation. Call after metadata registration. + /// + public void ReserveHeaderSpace(int estimatedSize) + { + if (estimatedSize > 0) + { + EnsureCapacity(estimatedSize); + _estimatedHeaderSize = estimatedSize; + _position += estimatedSize; + } + } + + public void FinalizeHeaderSections() + { + var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 }; + var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 }; + + // Calculate actual header size first + var actualSize = 0; + if (hasPropertyNames) + { + actualSize += GetVarUIntSize((uint)_propertyNameList!.Count); + foreach (var name in _propertyNameList) + { + var byteCount = Utf8NoBom.GetByteCount(name); + actualSize += GetVarUIntSize((uint)byteCount) + byteCount; + } + } + + if (hasInternTable) + { + actualSize += GetVarUIntSize((uint)_internedStringList!.Count); + foreach (var value in _internedStringList) + { + var byteCount = Utf8NoBom.GetByteCount(value); + actualSize += GetVarUIntSize((uint)byteCount) + byteCount; + } + } + + var bodyStart = _headerPosition + 2 + _estimatedHeaderSize; + var bodyLength = _position - bodyStart; + + // Shift body if needed + if (actualSize != _estimatedHeaderSize && bodyLength > 0) + { + var delta = actualSize - _estimatedHeaderSize; + if (delta > 0) + { + EnsureCapacity(delta); + } + + var newBodyStart = _headerPosition + 2 + actualSize; + if (delta != 0) + { + Array.Copy(_buffer, bodyStart, _buffer, newBodyStart, bodyLength); + _position += delta; + } + } + + // Write header payload directly to buffer (no ArrayBufferWriter allocation) + var headerPos = _headerPosition + 2; + + if (hasPropertyNames) + { + headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count); + foreach (var name in _propertyNameList) + { + headerPos = WriteStringAt(headerPos, name); + } + } + + if (hasInternTable) + { + headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count); + foreach (var value in _internedStringList) + { + headerPos = WriteStringAt(headerPos, value); + } + } + + // Write header flags + byte flags = BinaryTypeCode.HeaderFlagsBase; + if (hasPropertyNames) + flags |= BinaryTypeCode.HeaderFlag_Metadata; + if (UseReferenceHandling) + flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling; + if (hasInternTable) + flags |= BinaryTypeCode.HeaderFlag_StringInternTable; + + _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; + _buffer[_headerPosition + 1] = flags; + } + + /// + /// Writes VarUInt at specific position and returns new position. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int WriteVarUIntAt(int pos, uint value) + { + while (value >= 0x80) + { + _buffer[pos++] = (byte)(value | 0x80); + value >>= 7; + } + _buffer[pos++] = (byte)value; + return pos; + } + + /// + /// Writes UTF8 string at specific position (length-prefixed) and returns new position. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int WriteStringAt(int pos, string value) + { + var byteCount = Utf8NoBom.GetByteCount(value); + pos = WriteVarUIntAt(pos, (uint)byteCount); + Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount)); + return pos + byteCount; + } + + // Remove old methods: WriteHeaderVarUInt, WriteHeaderString (no longer needed) + + #endregion + + #region Reference Handling + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrackForScanning(object obj) + { + _scanOccurrences ??= new Dictionary(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); + _multiReferenced ??= new HashSet(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance); + + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); + if (exists) + { + count++; + _multiReferenced.Add(obj); + return false; + } + + count = 1; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldWriteRef(object obj, out int refId) + { + if (_multiReferenced != null && _multiReferenced.Contains(obj)) + { + _writtenRefs ??= new Dictionary(32, ReferenceEqualityComparer.Instance); + if (!_writtenRefs.ContainsKey(obj)) + { + refId = _nextRefId++; + return true; + } + } + + refId = 0; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MarkAsWritten(object obj, int refId) + => _writtenRefs![obj] = refId; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetExistingRef(object obj, out int refId) + { + if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId)) + { + return true; + } + + refId = 0; + return false; + } + + #endregion + + #region Helpers + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClearAndTrimIfNeeded(Dictionary? dict, int maxCapacity) + where TKey : notnull + { + if (dict == null) + { + return; + } + + dict.Clear(); + if (dict.EnsureCapacity(0) > maxCapacity) + { + dict.TrimExcess(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClearAndTrimIfNeeded(HashSet? set, int maxCapacity) + { + if (set == null) + { + return; + } + + set.Clear(); + if (set.EnsureCapacity(0) > maxCapacity) + { + set.TrimExcess(); + } + } + + #endregion + } +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationResult.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationResult.cs new file mode 100644 index 0000000..b9e2acf --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationResult.cs @@ -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 Span => Buffer.AsSpan(0, Length); + public ReadOnlyMemory Memory => new(Buffer, 0, Length); + + public byte[] ToArray() + { + var result = GC.AllocateUninitializedArray(Length); + Buffer.AsSpan(0, Length).CopyTo(result); + return result; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (_pooled) + { + ArrayPool.Shared.Return(Buffer); + } + } + + internal static BinarySerializationResult FromImmutable(byte[] buffer) + => new(buffer, buffer.Length, pooled: false); + } +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs new file mode 100644 index 0000000..976eb1b --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs @@ -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 _objectGetter; + private readonly Delegate? _typedGetter; + private readonly PropertyAccessorType _accessorType; + + /// + /// Cached property name index for metadata mode. Set by context during registration. + /// -1 means not yet cached. + /// + 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 ObjectGetter => _objectGetter; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object? GetValue(object obj) => _objectGetter(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetInt32(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetInt64(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetBoolean(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetDouble(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetSingle(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public decimal GetDecimal(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTime GetDateTime(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte GetByte(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short GetInt16(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort GetUInt16(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint GetUInt32(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong GetUInt64(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Guid GetGuid(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetEnumAsInt32(object obj) => ((Func)_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(declaringType, prop), PropertyAccessorType.Guid); + } + + var typeCode = Type.GetTypeCode(propType); + return typeCode switch + { + TypeCode.Int32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int32), + TypeCode.Int64 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int64), + TypeCode.Boolean => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Boolean), + TypeCode.Double => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Double), + TypeCode.Single => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Single), + TypeCode.Decimal => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Decimal), + TypeCode.DateTime => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.DateTime), + TypeCode.Byte => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Byte), + TypeCode.Int16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int16), + TypeCode.UInt16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt16), + TypeCode.UInt32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt32), + TypeCode.UInt64 => (CreateTypedGetterDelegate(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>(convertToInt, objParam).Compile(); + } + + private static Func CreateTypedGetterDelegate(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>(convertExpr, objParam).Compile(); + } + + private static Func 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>(boxed, objParam).Compile(); + } + } + + internal enum PropertyAccessorType : byte + { + Object = 0, + Int32, + Int64, + Boolean, + Double, + Single, + Decimal, + DateTime, + Byte, + Int16, + UInt16, + UInt32, + UInt64, + Guid, + Enum + } +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs new file mode 100644 index 0000000..f7dead6 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -0,0 +1,1021 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using static AyCode.Core.Helpers.JsonUtilities; +using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// High-performance binary serializer optimized for speed and memory efficiency. +/// Features: +/// - VarInt encoding for compact integers (MessagePack-style) +/// - String interning for repeated strings +/// - Property name table for fast lookup +/// - Reference handling for circular/shared references +/// - Optional metadata for schema evolution +/// - Optimized buffer management with ArrayPool +/// - Zero-allocation hot paths using Span and MemoryMarshal +/// +public static partial class AcBinarySerializer +{ + private static readonly ConcurrentDictionary TypeMetadataCache = new(); + + // Pre-computed UTF8 encoder for string operations + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + private static readonly Type StringType = typeof(string); + private static readonly Type GuidType = typeof(Guid); + private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset); + private static readonly Type TimeSpanType = typeof(TimeSpan); + private static readonly Type IntType = typeof(int); + private static readonly Type LongType = typeof(long); + private static readonly Type FloatType = typeof(float); + private static readonly Type DoubleType = typeof(double); + private static readonly Type DecimalType = typeof(decimal); + private static readonly Type BoolType = typeof(bool); + private static readonly Type DateTimeType = typeof(DateTime); + + #region Public API + + /// + /// Serialize object to binary with default options. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] Serialize(T value) => Serialize(value, AcBinarySerializerOptions.Default); + + /// + /// Serialize object to binary with specified options. + /// + public static byte[] Serialize(T value, AcBinarySerializerOptions options) + { + if (value == null) + { + return [BinaryTypeCode.Null]; + } + + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); + try + { + return context.ToArray(); + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + + /// + /// Serialize object to an IBufferWriter for zero-copy scenarios. + /// This avoids the final ToArray() allocation by writing directly to the caller's buffer. + /// + public static void Serialize(T value, IBufferWriter writer, AcBinarySerializerOptions options) + { + if (value == null) + { + var span = writer.GetSpan(1); + span[0] = BinaryTypeCode.Null; + writer.Advance(1); + return; + } + + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); + try + { + context.WriteTo(writer); + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + + /// + /// Get the serialized size without allocating the final array. + /// Useful for pre-allocating buffers. + /// + public static int GetSerializedSize(T value, AcBinarySerializerOptions options) + { + if (value == null) return 1; + + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); + try + { + return context.Position; + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + + /// + /// Serialize object and keep the pooled buffer for zero-copy consumers. + /// Caller must dispose the returned result to release the buffer. + /// + public static BinarySerializationResult SerializeToPooledBuffer(T value, AcBinarySerializerOptions options) + { + if (value == null) + { + return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]); + } + + var runtimeType = value.GetType(); + var context = SerializeCore(value, runtimeType, options); + try + { + return context.DetachResult(); + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + + private static BinarySerializationContext SerializeCore(object value, Type runtimeType, AcBinarySerializerOptions options) + { + var context = BinarySerializationContextPool.Get(options); + context.WriteHeaderPlaceholder(); + + if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType)) + { + ScanReferences(value, context, 0); + } + + if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType)) + { + RegisterMetadataForType(runtimeType, context); + } + + // Estimate and reserve header space to avoid body shift later + var estimatedHeaderSize = context.EstimateHeaderPayloadSize(); + context.ReserveHeaderSpace(estimatedHeaderSize); + + WriteValue(value, runtimeType, context, 0); + context.FinalizeHeaderSections(); + return context; + } + + #endregion + + #region Reference Scanning + + private static void ScanReferences(object? value, BinarySerializationContext context, int depth) + { + if (value == null || depth > context.MaxDepth) return; + + var type = value.GetType(); + if (IsPrimitiveOrStringFast(type)) return; + if (!context.TrackForScanning(value)) return; + + if (value is byte[]) return; // byte arrays are value types + + if (value is IDictionary dictionary) + { + foreach (DictionaryEntry entry in dictionary) + { + if (entry.Value != null) + ScanReferences(entry.Value, context, depth + 1); + } + return; + } + + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) + { + foreach (var item in enumerable) + { + if (item != null) + ScanReferences(item, context, depth + 1); + } + return; + } + + var metadata = GetTypeMetadata(type); + foreach (var prop in metadata.Properties) + { + if (!context.ShouldSerializeProperty(value, prop)) + { + continue; + } + + var propValue = prop.GetValue(value); + if (propValue != null) + ScanReferences(propValue, context, depth + 1); + } + } + + #endregion + + #region Property Metadata Registration + + private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet? visited = null) + { + if (IsPrimitiveOrStringFast(type)) return; + + visited ??= new HashSet(); + if (!visited.Add(type)) return; + + if (IsDictionaryType(type, out var keyType, out var valueType)) + { + if (keyType != null) RegisterMetadataForType(keyType, context, visited); + if (valueType != null) RegisterMetadataForType(valueType, context, visited); + return; + } + + if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType)) + { + var elementType = GetCollectionElementType(type); + if (elementType != null) + { + RegisterMetadataForType(elementType, context, visited); + } + return; + } + + var metadata = GetTypeMetadata(type); + foreach (var prop in metadata.Properties) + { + if (!context.ShouldIncludePropertyInMetadata(prop)) + { + continue; + } + + // Use caching registration to avoid dictionary lookup during serialization + context.RegisterPropertyNameAndCache(prop); + + if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType)) + { + RegisterMetadataForType(nestedType, context, visited); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryResolveNestedMetadataType(Type propertyType, out Type nestedType) + { + nestedType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + if (IsPrimitiveOrStringFast(nestedType)) + return false; + + if (IsDictionaryType(nestedType, out var _, out var valueType) && valueType != null) + { + if (!IsPrimitiveOrStringFast(valueType)) + { + nestedType = valueType; + return true; + } + return false; + } + + if (typeof(IEnumerable).IsAssignableFrom(nestedType) && !ReferenceEquals(nestedType, StringType)) + { + var elementType = GetCollectionElementType(nestedType); + if (elementType != null && !IsPrimitiveOrStringFast(elementType)) + { + nestedType = elementType; + return true; + } + return false; + } + + return true; + } + + #endregion + + #region Value Writing + + private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth) + { + if (value == null) + { + context.WriteByte(BinaryTypeCode.Null); + return; + } + + // Try writing as primitive first + if (TryWritePrimitive(value, type, context)) + return; + + if (depth > context.MaxDepth) + { + context.WriteByte(BinaryTypeCode.Null); + return; + } + + // Check for object reference + if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId)) + { + context.WriteByte(BinaryTypeCode.ObjectRef); + context.WriteVarInt(refId); + return; + } + + // Handle byte arrays specially + if (value is byte[] byteArray) + { + WriteByteArray(byteArray, context); + return; + } + + // Handle dictionaries + if (value is IDictionary dictionary) + { + WriteDictionary(dictionary, context, depth); + return; + } + + // Handle collections/arrays + if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) + { + WriteArray(enumerable, type, context, depth); + return; + } + + // Handle complex objects + WriteObject(value, type, context, depth); + } + + /// + /// Optimized primitive writer using TypeCode dispatch. + /// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context) + { + // Fast path: check TypeCode first (handles most primitives) + var typeCode = Type.GetTypeCode(type); + + switch (typeCode) + { + case TypeCode.Int32: + WriteInt32((int)value, context); + return true; + case TypeCode.Int64: + WriteInt64((long)value, context); + return true; + case TypeCode.Boolean: + context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False); + return true; + case TypeCode.Double: + WriteFloat64Unsafe((double)value, context); + return true; + case TypeCode.String: + WriteString((string)value, context); + return true; + case TypeCode.Single: + WriteFloat32Unsafe((float)value, context); + return true; + case TypeCode.Decimal: + WriteDecimalUnsafe((decimal)value, context); + return true; + case TypeCode.DateTime: + WriteDateTimeUnsafe((DateTime)value, context); + return true; + case TypeCode.Byte: + context.WriteByte(BinaryTypeCode.UInt8); + context.WriteByte((byte)value); + return true; + case TypeCode.Int16: + WriteInt16Unsafe((short)value, context); + return true; + case TypeCode.UInt16: + WriteUInt16Unsafe((ushort)value, context); + return true; + case TypeCode.UInt32: + WriteUInt32((uint)value, context); + return true; + case TypeCode.UInt64: + WriteUInt64((ulong)value, context); + return true; + case TypeCode.SByte: + context.WriteByte(BinaryTypeCode.Int8); + context.WriteByte(unchecked((byte)(sbyte)value)); + return true; + case TypeCode.Char: + WriteCharUnsafe((char)value, context); + return true; + } + + // Handle nullable types + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType != null) + { + return TryWritePrimitive(value, underlyingType, context); + } + + // Handle special types by reference comparison (faster than type equality) + if (ReferenceEquals(type, GuidType)) + { + WriteGuidUnsafe((Guid)value, context); + return true; + } + if (ReferenceEquals(type, DateTimeOffsetType)) + { + WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context); + return true; + } + if (ReferenceEquals(type, TimeSpanType)) + { + WriteTimeSpanUnsafe((TimeSpan)value, context); + return true; + } + if (type.IsEnum) + { + WriteEnum(value, context); + return true; + } + + return false; + } + + #endregion + + #region Optimized Primitive Writers using MemoryMarshal + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteInt32(int value, BinarySerializationContext context) + { + if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) + { + context.WriteByte(tiny); + return; + } + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteInt64(long value, BinarySerializationContext context) + { + if (value >= int.MinValue && value <= int.MaxValue) + { + WriteInt32((int)value, context); + return; + } + context.WriteByte(BinaryTypeCode.Int64); + context.WriteVarLong(value); + } + + /// + /// Optimized float64 writer using batched write. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteFloat64Unsafe(double value, BinarySerializationContext context) + { + context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value); + } + + /// + /// Optimized float32 writer using batched write. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteFloat32Unsafe(float value, BinarySerializationContext context) + { + context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value); + } + + /// + /// Optimized decimal writer using direct memory copy of bits. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Decimal); + context.WriteDecimalBits(value); + } + + /// + /// Optimized DateTime writer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.DateTime); + context.WriteDateTimeBits(value); + } + + /// + /// Optimized Guid writer using direct memory copy. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Guid); + context.WriteGuidBits(value); + } + + /// + /// Optimized DateTimeOffset writer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.DateTimeOffset); + context.WriteDateTimeOffsetBits(value); + } + + /// + /// Optimized TimeSpan writer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context) + { + context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteInt16Unsafe(short value, BinarySerializationContext context) + { + context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context) + { + context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt32(uint value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.UInt32); + context.WriteVarUInt(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt64(ulong value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.UInt64); + context.WriteVarULong(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteCharUnsafe(char value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.Char); + context.WriteRaw(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteEnum(object value, BinarySerializationContext context + ) + { + var intValue = Convert.ToInt32(value); + if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) + { + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(tiny); + return; + } + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(intValue); + } + + /// + /// Optimized string writer with span-based UTF8 encoding. + /// Uses stackalloc for small strings to avoid allocations. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteString(string value, BinarySerializationContext context) + { + if (value.Length == 0) + { + context.WriteByte(BinaryTypeCode.StringEmpty); + return; + } + + if (context.UseStringInterning && value.Length >= context.MinStringInternLength) + { + var index = context.RegisterInternedString(value); + context.WriteByte(BinaryTypeCode.StringInterned); + context.WriteVarUInt((uint)index); + return; + } + + // Első előfordulás vagy nincs interning - sima string + context.WriteByte(BinaryTypeCode.String); + context.WriteStringUtf8(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteByteArray(byte[] value, BinarySerializationContext context) + { + context.WriteByte(BinaryTypeCode.ByteArray); + context.WriteVarUInt((uint)value.Length); + context.WriteBytes(value); + } + + #endregion + + #region Complex Type Writers + + private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Object); + + // Register object reference if needed + if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId)) + { + context.WriteVarInt(refId); + context.MarkAsWritten(value, refId); + } + else if (context.UseReferenceHandling) + { + context.WriteVarInt(-1); // No ref ID + } + + var metadata = GetTypeMetadata(type); + var nextDepth = depth + 1; + var properties = metadata.Properties; + var propCount = properties.Length; + + // Reserve space for property count (will patch later) + var countPosition = context.Position; + context.WriteVarUInt(0); // Placeholder - will be patched + + var writtenCount = 0; + + // Single pass: check and write in one iteration + for (var i = 0; i < propCount; i++) + { + var prop = properties[i]; + + // Skip if filter says no + if (context.PropertyFilter != null && !context.ShouldSerializeProperty(value, prop)) + continue; + + // Skip default/null values + if (IsPropertyDefaultOrNull(value, prop)) + continue; + + // Write property name/index + if (context.UseMetadata) + { + var propIndex = prop.CachedPropertyNameIndex >= 0 + ? prop.CachedPropertyNameIndex + : context.GetPropertyNameIndex(prop.Name); + context.WriteVarUInt((uint)propIndex); + } + else + { + context.WritePreencodedPropertyName(prop.NameUtf8); + } + + // Write property value + WritePropertyValue(value, prop, context, nextDepth); + writtenCount++; + } + + // Patch the property count + context.PatchVarUInt(countPosition, (uint)writtenCount); + } + + /// + /// Checks if a property value is null or default without boxing for value types. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop) + { + switch (prop.AccessorType) + { + case PropertyAccessorType.Int32: + return prop.GetInt32(obj) == 0; + case PropertyAccessorType.Int64: + return prop.GetInt64(obj) == 0L; + case PropertyAccessorType.Boolean: + return !prop.GetBoolean(obj); + case PropertyAccessorType.Double: + return prop.GetDouble(obj) == 0.0; + case PropertyAccessorType.Single: + return prop.GetSingle(obj) == 0f; + case PropertyAccessorType.Decimal: + return prop.GetDecimal(obj) == 0m; + case PropertyAccessorType.Byte: + return prop.GetByte(obj) == 0; + case PropertyAccessorType.Int16: + return prop.GetInt16(obj) == 0; + case PropertyAccessorType.UInt16: + return prop.GetUInt16(obj) == 0; + case PropertyAccessorType.UInt32: + return prop.GetUInt32(obj) == 0; + case PropertyAccessorType.UInt64: + return prop.GetUInt64(obj) == 0; + case PropertyAccessorType.Guid: + return prop.GetGuid(obj) == Guid.Empty; + case PropertyAccessorType.Enum: + return prop.GetEnumAsInt32(obj) == 0; + case PropertyAccessorType.DateTime: + // DateTime default is not typically skipped + return false; + default: + // Object type - use regular getter + var value = prop.GetValue(obj); + if (value == null) return true; + if (prop.TypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value); + return false; + } + } + + /// + /// Writes a property value using typed getters to avoid boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth) + { + switch (prop.AccessorType) + { + case PropertyAccessorType.Int32: + WriteInt32(prop.GetInt32(obj), context); + return; + case PropertyAccessorType.Int64: + WriteInt64(prop.GetInt64(obj), context); + return; + case PropertyAccessorType.Boolean: + context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False); + return; + case PropertyAccessorType.Double: + WriteFloat64Unsafe(prop.GetDouble(obj), context); + return; + case PropertyAccessorType.Single: + WriteFloat32Unsafe(prop.GetSingle(obj), context); + return; + case PropertyAccessorType.Decimal: + WriteDecimalUnsafe(prop.GetDecimal(obj), context); + return; + case PropertyAccessorType.DateTime: + WriteDateTimeUnsafe(prop.GetDateTime(obj), context); + return; + case PropertyAccessorType.Byte: + context.WriteByte(BinaryTypeCode.UInt8); + context.WriteByte(prop.GetByte(obj)); + return; + case PropertyAccessorType.Int16: + WriteInt16Unsafe(prop.GetInt16(obj), context); + return; + case PropertyAccessorType.UInt16: + WriteUInt16Unsafe(prop.GetUInt16(obj), context); + return; + case PropertyAccessorType.UInt32: + WriteUInt32(prop.GetUInt32(obj), context); + return; + case PropertyAccessorType.UInt64: + WriteUInt64(prop.GetUInt64(obj), context); + return; + case PropertyAccessorType.Guid: + WriteGuidUnsafe(prop.GetGuid(obj), context); + return; + case PropertyAccessorType.Enum: + var enumValue = prop.GetEnumAsInt32(obj); + if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny)) + { + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(tiny); + } + else + { + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(enumValue); + } + return; + default: + // Fallback to object getter for reference types + var value = prop.GetValue(obj); + WriteValue(value, prop.PropertyType, context, depth); + return; + } + } + + #endregion + + #region Specialized Array Writers + + /// + /// Optimized array writer with specialized paths for primitive arrays. + /// + private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Array); + var nextDepth = depth + 1; + + // Optimized path for primitive arrays + var elementType = GetCollectionElementType(type); + if (elementType != null && type.IsArray) + { + if (TryWritePrimitiveArray(enumerable, elementType, context)) + return; + } + + // For IList, we can write the count directly + if (enumerable is IList list) + { + var count = list.Count; + context.WriteVarUInt((uint)count); + for (var i = 0; i < count; i++) + { + var item = list[i]; + var itemType = item?.GetType() ?? typeof(object); + WriteValue(item, itemType, context, nextDepth); + } + return; + } + + // For other IEnumerable, collect first + var items = new List(); + foreach (var item in enumerable) + { + items.Add(item); + } + + context.WriteVarUInt((uint)items.Count); + foreach (var item in items) + { + var itemType = item?.GetType() ?? typeof(object); + WriteValue(item, itemType, context, nextDepth); + } + } + + /// + /// Specialized array writer for primitive arrays using bulk memory operations. + /// Optimized for Blazor Hybrid compatibility (WASM, Android, Windows, iOS). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context) + { + // Int32 array - very common case + if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray) + { + context.WriteVarUInt((uint)intArray.Length); + context.WriteInt32ArrayOptimized(intArray); + return true; + } + + // Double array - bulk write as raw bytes + if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray) + { + context.WriteVarUInt((uint)doubleArray.Length); + context.WriteDoubleArrayBulk(doubleArray); + return true; + } + + // Long array + if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray) + { + context.WriteVarUInt((uint)longArray.Length); + context.WriteLongArrayOptimized(longArray); + return true; + } + + // Float array - bulk write as raw bytes + if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray) + { + context.WriteVarUInt((uint)floatArray.Length); + context.WriteFloatArrayBulk(floatArray); + return true; + } + + // Bool array - pack as bytes + if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray) + { + context.WriteVarUInt((uint)boolArray.Length); + for (var i = 0; i < boolArray.Length; i++) + { + context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False); + } + context.WriteVarUInt((uint)boolArray.Length); + return true; + } + + // Guid array - bulk write + if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray) + { + context.WriteVarUInt((uint)guidArray.Length); + context.WriteGuidArrayBulk(guidArray); + return true; + } + + // Decimal array + if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray) + { + context.WriteVarUInt((uint)decimalArray.Length); + for (var i = 0; i < decimalArray.Length; i++) + { + WriteDecimalUnsafe(decimalArray[i], context); + } + return true; + } + + // DateTime array + if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray) + { + context.WriteVarUInt((uint)dateTimeArray.Length); + for (var i = 0; i < dateTimeArray.Length; i++) + { + WriteDateTimeUnsafe(dateTimeArray[i], context); + } + return true; + } + + // String array - common case + if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray) + { + context.WriteVarUInt((uint)stringArray.Length); + for (var i = 0; i < stringArray.Length; i++) + { + var s = stringArray[i]; + if (s == null) + context.WriteByte(BinaryTypeCode.Null); + else + WriteString(s, context); + } + return true; + } + + return false; + } + + private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Dictionary); + context.WriteVarUInt((uint)dictionary.Count); + var nextDepth = depth + 1; + + foreach (DictionaryEntry entry in dictionary) + { + // Write key + var keyType = entry.Key?.GetType() ?? typeof(object); + WriteValue(entry.Key, keyType, context, nextDepth); + + // Write value + var valueType = entry.Value?.GetType() ?? typeof(object); + WriteValue(entry.Value, valueType, context, nextDepth); + } + } + + #endregion + + #region Serialization Result + + // Implementation moved to AcBinarySerializer.BinarySerializationResult.cs + + #endregion + + #region Context Pool + + // Implementation moved to AcBinarySerializer.BinarySerializationContext.cs + + #endregion + + #region Serialization Context + + // Implementation moved to AcBinarySerializer.BinarySerializationContext.cs + + #endregion + + #region Type Metadata + + private static Type? GetCollectionElementType(Type type) + { + if (type.IsArray) + { + return type.GetElementType(); + } + + if (type.IsGenericType) + { + var args = type.GetGenericArguments(); + if (args.Length == 1) + { + return args[0]; + } + } + + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return iface.GetGenericArguments()[0]; + } + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BinaryTypeMetadata GetTypeMetadata(Type type) + => TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t)); + + // Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs + + #endregion +} \ No newline at end of file diff --git a/AyCode.Core/Extensions/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs similarity index 95% rename from AyCode.Core/Extensions/AcBinarySerializerOptions.cs rename to AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 4ff28b4..0112bfa 100644 --- a/AyCode.Core/Extensions/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -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; /// /// Options for AcBinarySerializer and AcBinaryDeserializer. @@ -76,6 +77,14 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// public BinaryPropertyFilter? PropertyFilter { get; init; } + /// + /// 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) + /// + public bool RemoveOrphanedItems { get; init; } = false; + /// /// Creates options with specified max depth. /// diff --git a/AyCode.Core/Extensions/AcJsonDeserializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs similarity index 99% rename from AyCode.Core/Extensions/AcJsonDeserializer.cs rename to AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs index 327ebe2..b5de4f2 100644 --- a/AyCode.Core/Extensions/AcJsonDeserializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs @@ -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; /// /// Exception thrown when JSON deserialization fails. diff --git a/AyCode.Core/Extensions/AcJsonSerializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs similarity index 99% rename from AyCode.Core/Extensions/AcJsonSerializer.cs rename to AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs index b6fa408..e3b3efa 100644 --- a/AyCode.Core/Extensions/AcJsonSerializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs @@ -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; /// /// High-performance custom JSON serializer optimized for IId<T> reference handling. diff --git a/AyCode.Core/Extensions/AcJsonSerializerOptions.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs similarity index 97% rename from AyCode.Core/Extensions/AcJsonSerializerOptions.cs rename to AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs index 50e0d2d..fa951a1 100644 --- a/AyCode.Core/Extensions/AcJsonSerializerOptions.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs @@ -1,4 +1,4 @@ -namespace AyCode.Core.Extensions; +namespace AyCode.Core.Serializers.Jsons; public enum AcSerializerType : byte { diff --git a/AyCode.Core/Extensions/MergeContractResolver.cs b/AyCode.Core/Serializers/Jsons/MergeContractResolver.cs similarity index 99% rename from AyCode.Core/Extensions/MergeContractResolver.cs rename to AyCode.Core/Serializers/Jsons/MergeContractResolver.cs index 959a5fa..ad8930c 100644 --- a/AyCode.Core/Extensions/MergeContractResolver.cs +++ b/AyCode.Core/Serializers/Jsons/MergeContractResolver.cs @@ -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 { } diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs index fc8e1b5..9579ea9 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs @@ -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; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDataSourceTests.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDataSourceTests.cs index a1ea8f6..bc26932 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDataSourceTests.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDataSourceTests.cs @@ -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; diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs index 24861ef..41c5d1e 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs @@ -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; diff --git a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs index 04f8f1f..a3e9e13 100644 --- a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs +++ b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs @@ -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 { diff --git a/AyCode.Services.Server/SignalRs/AcSignalRSendToClientService.cs b/AyCode.Services.Server/SignalRs/AcSignalRSendToClientService.cs index 7206f38..ef64b3b 100644 --- a/AyCode.Services.Server/SignalRs/AcSignalRSendToClientService.cs +++ b/AyCode.Services.Server/SignalRs/AcSignalRSendToClientService.cs @@ -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; diff --git a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs index 687d9ce..8ee705a 100644 --- a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs +++ b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs @@ -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; diff --git a/AyCode.Services.Tests/SignalRs/PostJsonDataMessageTests.cs b/AyCode.Services.Tests/SignalRs/PostJsonDataMessageTests.cs index 4900265..46be20a 100644 --- a/AyCode.Services.Tests/SignalRs/PostJsonDataMessageTests.cs +++ b/AyCode.Services.Tests/SignalRs/PostJsonDataMessageTests.cs @@ -1,4 +1,5 @@ using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Jsons; using AyCode.Services.SignalRs; namespace AyCode.Services.Tests.SignalRs; diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index ab453af..5babc57 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -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; diff --git a/AyCode.Services/SignalRs/IAcSignalRHubClient.cs b/AyCode.Services/SignalRs/IAcSignalRHubClient.cs index 64e734b..6daa17c 100644 --- a/AyCode.Services/SignalRs/IAcSignalRHubClient.cs +++ b/AyCode.Services/SignalRs/IAcSignalRHubClient.cs @@ -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; diff --git a/AyCode.Services/SignalRs/SignalRSerializationHelper.cs b/AyCode.Services/SignalRs/SignalRSerializationHelper.cs index 6f7287f..b050ca6 100644 --- a/AyCode.Services/SignalRs/SignalRSerializationHelper.cs +++ b/AyCode.Services/SignalRs/SignalRSerializationHelper.cs @@ -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;