Refactor: Add high-perf JSON serializer & merge support

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

View File

@ -7,6 +7,9 @@ using MessagePack;
using MessagePack.Resolvers; using MessagePack.Resolvers;
using BenchmarkDotNet.Configs; using BenchmarkDotNet.Configs;
using System.IO; using System.IO;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Serializers.Binaries;
using System.Diagnostics;
namespace AyCode.Benchmark namespace AyCode.Benchmark
{ {
@ -67,6 +70,12 @@ namespace AyCode.Benchmark
var config = ManualConfig.Create(DefaultConfig.Instance) var config = ManualConfig.Create(DefaultConfig.Instance)
.WithArtifactsPath(benchmarkDir); .WithArtifactsPath(benchmarkDir);
if (args.Length > 0 && args[0] == "--quick")
{
RunQuickBenchmark();
return;
}
if (args.Length > 0 && args[0] == "--test") if (args.Length > 0 && args[0] == "--test")
{ {
var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir); var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir);
@ -112,6 +121,7 @@ namespace AyCode.Benchmark
} }
Console.WriteLine("Usage:"); Console.WriteLine("Usage:");
Console.WriteLine(" --quick Quick benchmark with tabular output (AcBinary vs MessagePack)");
Console.WriteLine(" --test Quick AcBinary test"); Console.WriteLine(" --test Quick AcBinary test");
Console.WriteLine(" --testmsgpack Quick MessagePack test"); Console.WriteLine(" --testmsgpack Quick MessagePack test");
Console.WriteLine(" --minimal Minimal benchmark"); Console.WriteLine(" --minimal Minimal benchmark");
@ -134,6 +144,193 @@ namespace AyCode.Benchmark
} }
} }
/// <summary>
/// Quick benchmark comparing AcBinary vs MessagePack with tabular output.
/// Tests: WithRef, NoRef, Serialize, Deserialize, Populate, Merge
/// </summary>
static void RunQuickBenchmark(int iterations = 1000)
{
Console.WriteLine();
Console.WriteLine("????????????????????????????????????????????????????????????????????????????????");
Console.WriteLine("? AcBinary vs MessagePack Quick Benchmark ?");
Console.WriteLine("????????????????????????????????????????????????????????????????????????????????");
Console.WriteLine();
// Create test data with shared references
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
var testOrder = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta);
// Options
var withRefOptions = new AcBinarySerializerOptions();
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
// Warm up
Console.WriteLine("Warming up...");
for (int i = 0; i < 100; i++)
{
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
_ = MessagePackSerializer.Serialize(testOrder, msgPackOptions);
}
// Pre-serialize data for deserialization tests
var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);
var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions);
var msgPackData = MessagePackSerializer.Serialize(testOrder, msgPackOptions);
Console.WriteLine($"Iterations: {iterations:N0}");
Console.WriteLine();
// Size comparison
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine("? SIZE COMPARISON ?");
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine("? Format ? Size (bytes) ? vs MessagePack ? Savings ?");
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine($"? AcBinary (WithRef) ? {acBinaryWithRef.Length,14:N0} ? {100.0 * acBinaryWithRef.Length / msgPackData.Length,13:F1}% ? {msgPackData.Length - acBinaryWithRef.Length,14:N0} ?");
Console.WriteLine($"? AcBinary (NoRef) ? {acBinaryNoRef.Length,14:N0} ? {100.0 * acBinaryNoRef.Length / msgPackData.Length,13:F1}% ? {msgPackData.Length - acBinaryNoRef.Length,14:N0} ?");
Console.WriteLine($"? MessagePack ? {msgPackData.Length,14:N0} ? {100.0,13:F1}% ? {"(baseline)",14} ?");
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine();
// Benchmark results storage
var results = new List<(string Operation, string Mode, double AcBinaryMs, double MsgPackMs)>();
// Serialize benchmarks
var sw = Stopwatch.StartNew();
// AcBinary WithRef Serialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
var acWithRefSerialize = sw.Elapsed.TotalMilliseconds;
// AcBinary NoRef Serialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
var acNoRefSerialize = sw.Elapsed.TotalMilliseconds;
// MessagePack Serialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = MessagePackSerializer.Serialize(testOrder, msgPackOptions);
var msgPackSerialize = sw.Elapsed.TotalMilliseconds;
results.Add(("Serialize", "WithRef", acWithRefSerialize, msgPackSerialize));
results.Add(("Serialize", "NoRef", acNoRefSerialize, msgPackSerialize));
// Deserialize benchmarks
// AcBinary WithRef Deserialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryWithRef);
var acWithRefDeserialize = sw.Elapsed.TotalMilliseconds;
// AcBinary NoRef Deserialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryNoRef);
var acNoRefDeserialize = sw.Elapsed.TotalMilliseconds;
// MessagePack Deserialize
sw.Restart();
for (int i = 0; i < iterations; i++)
_ = MessagePackSerializer.Deserialize<TestOrder>(msgPackData, msgPackOptions);
var msgPackDeserialize = sw.Elapsed.TotalMilliseconds;
results.Add(("Deserialize", "WithRef", acWithRefDeserialize, msgPackDeserialize));
results.Add(("Deserialize", "NoRef", acNoRefDeserialize, msgPackDeserialize));
// Populate benchmark (AcBinary only)
sw.Restart();
for (int i = 0; i < iterations; i++)
{
var target = CreatePopulateTarget(testOrder);
AcBinaryDeserializer.Populate(acBinaryNoRef, target);
}
var acPopulate = sw.Elapsed.TotalMilliseconds;
results.Add(("Populate", "NoRef", acPopulate, 0)); // MessagePack doesn't have Populate
// PopulateMerge benchmark (AcBinary only)
sw.Restart();
for (int i = 0; i < iterations; i++)
{
var target = CreatePopulateTarget(testOrder);
AcBinaryDeserializer.PopulateMerge(acBinaryNoRef.AsSpan(), target);
}
var acMerge = sw.Elapsed.TotalMilliseconds;
results.Add(("Merge", "NoRef", acMerge, 0));
// Round-trip
var acWithRefRoundTrip = acWithRefSerialize + acWithRefDeserialize;
var acNoRefRoundTrip = acNoRefSerialize + acNoRefDeserialize;
var msgPackRoundTrip = msgPackSerialize + msgPackDeserialize;
results.Add(("Round-trip", "WithRef", acWithRefRoundTrip, msgPackRoundTrip));
results.Add(("Round-trip", "NoRef", acNoRefRoundTrip, msgPackRoundTrip));
// Print performance table
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine("? PERFORMANCE COMPARISON (lower is better) ?");
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine("? Operation ? AcBinary (ms) ? MessagePack ? Ratio ?");
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
foreach (var r in results)
{
var opName = $"{r.Operation} ({r.Mode})";
if (r.MsgPackMs > 0)
{
var ratio = r.AcBinaryMs / r.MsgPackMs;
var ratioStr = ratio < 1 ? $"{ratio:F2}x faster" : $"{ratio:F2}x slower";
Console.WriteLine($"? {opName,-24} ? {r.AcBinaryMs,14:F2} ? {r.MsgPackMs,14:F2} ? {ratioStr,14} ?");
}
else
{
Console.WriteLine($"? {opName,-24} ? {r.AcBinaryMs,14:F2} ? {"N/A",14} ? {"(unique)",14} ?");
}
}
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine();
// Summary
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine("? SUMMARY ?");
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
var sizeAdvantage = 100.0 - (100.0 * acBinaryNoRef.Length / msgPackData.Length);
Console.WriteLine($"? Size advantage: AcBinary is {sizeAdvantage:F1}% smaller than MessagePack ?");
var serializeRatio = acNoRefSerialize / msgPackSerialize;
var deserializeRatio = acNoRefDeserialize / msgPackDeserialize;
Console.WriteLine($"? Serialize (NoRef): AcBinary is {(serializeRatio < 1 ? $"{1/serializeRatio:F2}x faster" : $"{serializeRatio:F2}x slower"),-20} ?");
Console.WriteLine($"? Deserialize (NoRef): AcBinary is {(deserializeRatio < 1 ? $"{1/deserializeRatio:F2}x faster" : $"{deserializeRatio:F2}x slower"),-18} ?");
Console.WriteLine("???????????????????????????????????????????????????????????????????????????????");
Console.WriteLine();
}
static TestOrder CreatePopulateTarget(TestOrder source)
{
var target = new TestOrder { Id = source.Id };
foreach (var item in source.Items)
{
target.Items.Add(new TestOrderItem { Id = item.Id });
}
return target;
}
static (string InDir, string OutDir) CreateMSTestDeployDirs(string mstestBase) static (string InDir, string OutDir) CreateMSTestDeployDirs(string mstestBase)
{ {
var user = Environment.UserName ?? "Deploy"; var user = Environment.UserName ?? "Deploy";

View File

@ -12,6 +12,8 @@ using MongoDB.Bson;
using MongoDB.Bson.IO; using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using System.IO; using System.IO;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Benchmarks; namespace AyCode.Core.Benchmarks;

View File

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

View File

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

View File

@ -3,6 +3,7 @@ using AyCode.Core.Enums;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Loggers; using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Jsons;
using Newtonsoft.Json; using Newtonsoft.Json;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;

View File

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

View File

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

View File

@ -1,5 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using MessagePack; using MessagePack;
using MessagePack.Resolvers; using MessagePack.Resolvers;
@ -205,9 +207,9 @@ public class QuickBenchmark
var sizeDiff = msgPackData.Length - acBinaryData.Length; var sizeDiff = msgPackData.Length - acBinaryData.Length;
if (sizeDiff > 0) if (sizeDiff > 0)
Console.WriteLine($"? AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)"); Console.WriteLine($" AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)");
else else
Console.WriteLine($"?? AcBinary {-sizeDiff:N0} bytes larger"); Console.WriteLine($"⚠️ AcBinary {-sizeDiff:N0} bytes larger");
Assert.IsNotNull(acBinaryResult); Assert.IsNotNull(acBinaryResult);
Assert.IsNotNull(msgPackResult); Assert.IsNotNull(msgPackResult);
@ -279,7 +281,7 @@ public class QuickBenchmark
Console.WriteLine(); Console.WriteLine();
var sizeSaving = msgPack.Length - acWithIntern.Length; var sizeSaving = msgPack.Length - acWithIntern.Length;
Console.WriteLine($"? String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)"); Console.WriteLine($" String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)");
Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller"); Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller");
} }

View File

@ -1,5 +1,6 @@
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Jsons;
using MessagePack; using MessagePack;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using Newtonsoft.Json; using Newtonsoft.Json;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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