AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs

2213 lines
86 KiB
C#
Raw Blame History

using System;
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Expressions;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// High-performance binary deserializer matching AcBinarySerializer.
/// Features:
/// - VarInt decoding for compact integers
/// - String intern table lookup
/// - Property name table for fast property resolution
/// - Reference resolution for circular/shared references
/// - Populate/Merge mode support
/// - Optimized with FrozenDictionary for type dispatch
/// - Zero-allocation hot paths using Span and MemoryMarshal
/// </summary>
public static partial class AcBinaryDeserializer
{
/// <summary>
/// Diagnostic logger for deserializer-level debugging (DEBUG builds only).
/// Set to non-null to log SequenceBinaryInput vs ArrayBinaryInput verification results.
/// </summary>
public static Action<string>? DiagnosticLogger { get; set; }
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
/// <summary>
/// Thread-safe registry for generated readers. Looked up in ReadObjectCore to bypass runtime path.
/// </summary>
internal static class GeneratedReaderRegistry
{
private static readonly ConcurrentDictionary<Type, IGeneratedBinaryReader> Readers = new();
internal static void Register(Type type, IGeneratedBinaryReader reader) => Readers[type] = reader;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static IGeneratedBinaryReader? TryGet(Type type) =>
Readers.TryGetValue(type, out var reader) ? reader : null;
}
/// <summary>
/// Registers a source-generated binary reader for the specified type.
/// Once registered, ReadObjectCore bypasses the runtime wrapper/property loop
/// and calls the generated reader directly.
/// </summary>
internal static void RegisterGeneratedReader(Type type, IGeneratedBinaryReader reader)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(reader);
GeneratedReaderRegistry.Register(type, reader);
}
/// <summary>
/// ThreadLocal cache for type conversion info.
/// </summary>
[ThreadStatic]
private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;
// Type dispatch table for fast ReadValue — generic per TInput, JIT specializes each
private delegate object? TypeReader<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase;
private static class TypeReaderTable<TInput> where TInput : struct, IBinaryInputBase
{
public static readonly TypeReader<TInput>?[] Readers = InitReaders();
private static TypeReader<TInput>?[] InitReaders()
{
var readers = new TypeReader<TInput>?[byte.MaxValue + 1];
readers[BinaryTypeCode.Null] = static (_, _, _) => null;
readers[BinaryTypeCode.True] = static (_, _, _) => true;
readers[BinaryTypeCode.False] = static (_, _, _) => false;
readers[BinaryTypeCode.Int8] = static (ctx, _, _) => (sbyte)ctx.ReadByte();
readers[BinaryTypeCode.UInt8] = static (ctx, _, _) => ctx.ReadByte();
readers[BinaryTypeCode.Int16] = static (ctx, _, _) => ctx.ReadInt16Unsafe();
readers[BinaryTypeCode.UInt16] = static (ctx, _, _) => ctx.ReadUInt16Unsafe();
readers[BinaryTypeCode.Int32] = static (ctx, type, _) => ReadInt32Value(ctx, type);
readers[BinaryTypeCode.UInt32] = static (ctx, _, _) => ctx.ReadVarUInt();
readers[BinaryTypeCode.Int64] = static (ctx, _, _) => ctx.ReadVarLong();
readers[BinaryTypeCode.UInt64] = static (ctx, _, _) => ctx.ReadVarULong();
readers[BinaryTypeCode.Float32] = static (ctx, _, _) => ctx.ReadSingleUnsafe();
readers[BinaryTypeCode.Float64] = static (ctx, _, _) => ctx.ReadDoubleUnsafe();
readers[BinaryTypeCode.Decimal] = static (ctx, _, _) => ctx.ReadDecimalUnsafe();
readers[BinaryTypeCode.Char] = static (ctx, _, _) => ctx.ReadCharUnsafe();
readers[BinaryTypeCode.String] = static (ctx, _, _) => ReadPlainString(ctx);
readers[BinaryTypeCode.StringInterned] = static (ctx, _, _) => ctx.GetInternedString((int)ctx.ReadVarUInt());
readers[BinaryTypeCode.StringEmpty] = static (_, _, _) => string.Empty;
readers[BinaryTypeCode.StringInternFirst] = static (ctx, _, _) => ReadAndRegisterInternedString(ctx);
readers[BinaryTypeCode.DateTime] = static (ctx, _, _) => ctx.ReadDateTimeUnsafe();
readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _, _) => ctx.ReadDateTimeOffsetUnsafe();
readers[BinaryTypeCode.TimeSpan] = static (ctx, _, _) => ctx.ReadTimeSpanUnsafe();
readers[BinaryTypeCode.Guid] = static (ctx, _, _) => ctx.ReadGuidUnsafe();
readers[BinaryTypeCode.Enum] = static (ctx, type, _) => ReadEnumValue(ctx, type);
readers[BinaryTypeCode.Object] = ReadObject;
readers[BinaryTypeCode.ObjectRefFirst] = ReadObjectRefFirst;
readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata;
readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
readers[BinaryTypeCode.ObjectWithTypeNameRefFirst] = ReadObjectWithTypeNameRefFirst;
readers[BinaryTypeCode.ObjectWithTypeIndex] = ReadObjectWithTypeIndex;
readers[BinaryTypeCode.ObjectWithTypeIndexRefFirst] = ReadObjectWithTypeIndexRefFirst;
readers[BinaryTypeCode.Array] = ReadArray;
readers[BinaryTypeCode.Dictionary] = ReadDictionary;
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
// Register FixStr readers
for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
{
var length = BinaryTypeCode.DecodeFixStrLength(code);
readers[code] = CreateFixStrReader<TInput>(length);
}
// Register FixObj slot readers (0..SlotCount-1)
for (int slot = 0; slot < BinaryTypeCode.SlotCount; slot++)
{
readers[slot] = CreateFixObjReader<TInput>(slot);
}
return readers;
}
}
/// <summary>
/// Creates a reader for FixStr with the given length.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeReader<TInput> CreateFixStrReader<TInput>(int length)
where TInput : struct, IBinaryInputBase
{
if (length == 0)
return static (_, _, _) => string.Empty;
return (ctx, _, _) => ctx.ReadStringUtf8(length);
}
/// <summary>
/// Creates a reader for FixObj slot (0..SlotCount-1).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeReader<TInput> CreateFixObjReader<TInput>(int slot)
where TInput : struct, IBinaryInputBase
{
return (ctx, targetType, depth) => ReadObjectFromSlot(ctx, slot, targetType, depth);
}
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
#region Public API
/// <summary>
/// Deserialize binary data to object of type T.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize<T>(byte[] data) => Deserialize<T>(data, AcBinarySerializerOptions.Default);
/// <summary>
/// Deserialize binary data to object of type T with options.
/// Zero-copy: ArrayBinaryInput references the byte[] directly.
/// </summary>
public static T? Deserialize<T>(byte[] data, AcBinarySerializerOptions options)
{
if (data.Length == 0) return default;
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;
var targetType = typeof(T);
if (AcSerializerCommon.IsExpressionType(targetType))
return (T?)(object?)DeserializeExpression(data, targetType, options);
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
context.InitInput(new ArrayBinaryInput(data));
try { return (T?)DeserializeCore(context, targetType); }
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
}
/// <summary>
/// Deserialize binary data to object of type T from a sub-range of a byte[].
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize<T>(byte[] data, int offset, int length)
=> Deserialize<T>(data, offset, length, AcBinarySerializerOptions.Default);
/// <summary>
/// Deserialize binary data to object of type T from a sub-range with options.
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
/// </summary>
public static T? Deserialize<T>(byte[] data, int offset, int length, AcBinarySerializerOptions options)
{
if (length == 0) return default;
if (length == 1 && data[offset] == BinaryTypeCode.Null) return default;
var targetType = typeof(T);
if (AcSerializerCommon.IsExpressionType(targetType))
return (T?)(object?)DeserializeExpression(data, offset, length, targetType, options);
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
context.InitInput(new ArrayBinaryInput(data, offset, length));
try { return (T?)DeserializeCore(context, targetType); }
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
}
/// <summary>
/// Deserialize binary data to specified type.
/// </summary>
public static object? Deserialize(byte[] data, Type targetType)
=> Deserialize(data, 0, data.Length, targetType, AcBinarySerializerOptions.Default);
/// <summary>
/// Deserialize binary data to specified type with options.
/// </summary>
public static object? Deserialize(byte[] data, Type targetType, AcBinarySerializerOptions options)
=> Deserialize(data, 0, data.Length, targetType, options);
/// <summary>
/// Deserialize binary data to specified type from a sub-range.
/// </summary>
public static object? Deserialize(byte[] data, int offset, int length, Type targetType)
=> Deserialize(data, offset, length, targetType, AcBinarySerializerOptions.Default);
/// <summary>
/// Deserialize binary data to specified type from a sub-range with options.
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
/// </summary>
public static object? Deserialize(byte[] data, int offset, int length, Type targetType, AcBinarySerializerOptions options)
{
if (length == 0) return null;
if (length == 1 && data[offset] == BinaryTypeCode.Null) return null;
if (AcSerializerCommon.IsExpressionType(targetType))
return DeserializeExpression(data, offset, length, targetType, options);
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
context.InitInput(new ArrayBinaryInput(data, offset, length));
try { return DeserializeCore(context, targetType); }
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
}
/// <summary>
/// Deserialize binary data from a ReadOnlySequence (e.g., from SignalR/Pipes).
/// Single-segment: zero-copy via FirstSpan. Multi-segment: linearize into pooled buffer.
/// </summary>
public static T? Deserialize<T>(ReadOnlySequence<byte> data)
=> Deserialize<T>(data, AcBinarySerializerOptions.Default);
/// <summary>
/// Deserialize binary data from a ReadOnlySequence with options.
/// Single-segment: zero-copy via FirstSpan. Multi-segment: uses SequenceBinaryInput for true streaming.
/// </summary>
public static T? Deserialize<T>(ReadOnlySequence<byte> data, AcBinarySerializerOptions options)
{
if (data.Length == 0) return default;
if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg))
return Deserialize<T>(seg.Array!, seg.Offset, seg.Count, options);
VerifyAgainstLinearized(data, typeof(T), options);
return DeserializeSequence<T, SequenceBinaryInput>(new SequenceBinaryInput(data), typeof(T), options);
}
/// <summary>
/// Deserialize binary data from a ReadOnlySequence to specified type.
/// </summary>
public static object? Deserialize(ReadOnlySequence<byte> data, Type targetType)
=> Deserialize(data, targetType, AcBinarySerializerOptions.Default);
/// <summary>
/// Deserialize binary data from a ReadOnlySequence to specified type with options.
/// </summary>
public static object? Deserialize(ReadOnlySequence<byte> data, Type targetType, AcBinarySerializerOptions options)
{
if (data.Length == 0) return null;
if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg2))
return Deserialize(seg2.Array!, seg2.Offset, seg2.Count, targetType, options);
VerifyAgainstLinearized(data, targetType, options);
return DeserializeSequence<SequenceBinaryInput>(new SequenceBinaryInput(data), targetType, options);
}
/// <summary>
/// Deserialize from PipeReader with segment streaming (read per chunk via PipeReaderBinaryInput).
/// Data is consumed as it arrives from the network, enabling pipeline parallelism.
/// </summary>
public static T? Deserialize<T>(System.IO.Pipelines.PipeReader pipeReader, AcBinarySerializerOptions options)
=> DeserializeSequence<T, PipeReaderBinaryInput>(new PipeReaderBinaryInput(pipeReader), typeof(T), options);
/// <summary>
/// Deserialize from PipeReader to specified type with segment streaming.
/// </summary>
public static object? Deserialize(System.IO.Pipelines.PipeReader pipeReader, Type targetType, AcBinarySerializerOptions options)
=> DeserializeSequence<PipeReaderBinaryInput>(new PipeReaderBinaryInput(pipeReader), targetType, options);
/// <summary>
/// Internal: Deserialize with any TInput (multi-segment or other future input types).
/// </summary>
private static T? DeserializeSequence<T, TInput>(TInput input, Type targetType, AcBinarySerializerOptions options)
where TInput : struct, IBinaryInputBase
{
if (AcSerializerCommon.IsExpressionType(targetType))
{
var (buf, off, len) = LinearizeSequence(input);
return Deserialize<T>(buf, off, len, options);
}
var context = DeserializationContextPool<TInput>.Get(options);
context.InitInput(input);
try { return (T?)DeserializeCore(context, targetType); }
finally
{
context.Input.Release();
DeserializationContextPool<TInput>.Return(context);
}
}
/// <summary>
/// Internal: Deserialize with any TInput (multi-segment, non-generic result).
/// </summary>
private static object? DeserializeSequence<TInput>(TInput input, Type targetType, AcBinarySerializerOptions options)
where TInput : struct, IBinaryInputBase
{
if (AcSerializerCommon.IsExpressionType(targetType))
{
var (buf, off, len) = LinearizeSequence(input);
return Deserialize(buf, off, len, targetType, options);
}
var context = DeserializationContextPool<TInput>.Get(options);
context.InitInput(input);
try { return DeserializeCore(context, targetType); }
finally
{
context.Input.Release();
DeserializationContextPool<TInput>.Return(context);
}
}
/// <summary>
/// Fallback: linearize a TInput into a contiguous byte[] range (for Expression deserialization).
/// </summary>
private static (byte[] buffer, int offset, int length) LinearizeSequence<TInput>(TInput input)
where TInput : struct, IBinaryInputBase
{
input.Initialize(out var buffer, out var position, out var bufferLength);
return (buffer, position, bufferLength - position);
}
/// <summary>
/// Core deserialization: ReadHeader + ReadValue with unified error handling.
/// All public Deserialize overloads delegate here after pool/init setup.
/// </summary>
private static object? DeserializeCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
where TInput : struct, IBinaryInputBase
{
try
{
context.ReadHeader();
return ReadValue(context, targetType, 0);
}
catch (AcBinaryDeserializationException) { throw; }
catch (Exception ex)
{
throw new AcBinaryDeserializationException(
$"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
context.Position, targetType, ex);
}
}
/// <summary>
/// DEBUG-only verification: linearizes multi-segment data to byte[] and deserializes
/// with ArrayBinaryInput to determine if drift is caused by SequenceBinaryInput or by
/// the serialized data itself (generated reader / serializer bug).
///
/// Result: VERIFY_OK → SequenceBinaryInput is the culprit.
/// VERIFY_FAIL → bug is in serializer or generated reader (data itself is bad).
/// </summary>
[Conditional("DEBUG")]
private static void VerifyAgainstLinearized(ReadOnlySequence<byte> data, Type targetType, AcBinarySerializerOptions options)
{
if (DiagnosticLogger == null) return;
var bytes = data.ToArray();
var segmentCount = 0;
foreach (var _ in data) segmentCount++;
try
{
var result = Deserialize(bytes, targetType, options);
DiagnosticLogger($"[VERIFY_OK] ArrayBinaryInput succeeded for {targetType.Name}, " +
$"{bytes.Length} bytes, {segmentCount} segments → SequenceBinaryInput is suspect");
}
catch (Exception ex)
{
DiagnosticLogger($"[VERIFY_FAIL] ArrayBinaryInput ALSO FAILED for {targetType.Name}, " +
$"{bytes.Length} bytes, {segmentCount} segments: {ex.Message}");
}
}
/// <summary>
/// Deserialize Expression from binary data.
/// </summary>
private static Expression? DeserializeExpression(byte[] data, Type targetExpressionType, AcBinarySerializerOptions options)
=> DeserializeExpression(data, 0, data.Length, targetExpressionType, options);
/// <summary>
/// Deserialize Expression from binary data sub-range.
/// </summary>
private static Expression? DeserializeExpression(byte[] data, int offset, int length, Type targetExpressionType, AcBinarySerializerOptions options)
{
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
context.InitInput(new ArrayBinaryInput(data, offset, length));
try
{
var node = (AcExpressionNode?)DeserializeCore(context, typeof(AcExpressionNode));
if (node == null) return null;
var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType);
return AcExpressionRebuilder.FromNode(node, entityType);
}
finally
{
DeserializationContextPool<ArrayBinaryInput>.Return(context);
}
}
/// <summary>
/// Core populate: ReadHeader + dispatch by typeCode. Used by Populate, PopulateMerge, ThenPopulate.
/// </summary>
private static void PopulateCore<TInput>(BinaryDeserializationContext<TInput> context, object target)
where TInput : struct, IBinaryInputBase
{
var targetType = target.GetType();
try
{
context.ReadHeader();
var typeCode = context.PeekByte();
if (typeCode < BinaryTypeCode.SlotCount)
{
// FixObj slot: marker byte is the slot index
context.ReadByte();
context.GetWrapper(targetType, typeCode);
if (typeCode >= context._nextRuntimeSlot)
context._nextRuntimeSlot = typeCode + 1;
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.Object)
{
context.ReadByte();
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.ObjectRefFirst)
{
// ObjectRefFirst: [marker][VarUInt cacheIndex][props...]
// Read marker + cacheIndex, register target in intern cache, then populate
context.ReadByte();
var cacheIndex = (int)context.ReadVarUInt();
context.RegisterInternedValueAt(cacheIndex, target);
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
{
context.ReadByte();
ReadInlineMetadataForPopulate(context, targetType);
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst)
{
// ObjectWithMetadataRefFirst: [marker][VarUInt cacheIndex][metadata...][props...]
context.ReadByte();
var cacheIndex = (int)context.ReadVarUInt();
context.RegisterInternedValueAt(cacheIndex, target);
ReadInlineMetadataForPopulate(context, targetType);
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
{
context.ReadByte();
PopulateList(context, targetList, targetType, 0);
}
else
{
throw new AcBinaryDeserializationException(
$"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
context.Position, targetType);
}
}
catch (AcBinaryDeserializationException) { throw; }
catch (Exception ex)
{
throw new AcBinaryDeserializationException(
$"Failed to populate object of type '{targetType.Name}': {ex.Message}",
context.Position, targetType, ex);
}
}
/// <summary>
/// Populate existing object from binary data.
/// </summary>
public static void Populate<T>(byte[] data, T target) where T : class
{
ArgumentNullException.ThrowIfNull(target);
if (data.Length == 0) return;
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
var context = DeserializationContextPool<ArrayBinaryInput>.Get(AcBinarySerializerOptions.Default);
context.InitInput(new ArrayBinaryInput(data));
try { PopulateCore(context, target); }
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
}
/// <summary>
/// Populate existing object from binary data with options.
/// </summary>
public static void Populate<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
=> Populate(data, 0, data.Length, target, options);
/// <summary>
/// Populate existing object from binary data sub-range.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate<T>(byte[] data, int offset, int length, T target) where T : class
=> Populate(data, offset, length, target, AcBinarySerializerOptions.Default);
/// <summary>
/// Populate existing object from binary data sub-range with options.
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
/// </summary>
public static void Populate<T>(byte[] data, int offset, int length, T target, AcBinarySerializerOptions options) where T : class
{
ArgumentNullException.ThrowIfNull(target);
if (length == 0) return;
if (length == 1 && data[offset] == BinaryTypeCode.Null) return;
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
context.InitInput(new ArrayBinaryInput(data, offset, length));
try { PopulateCore(context, target); }
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
}
/// <summary>
/// Populate with merge semantics for IId collections from byte[] (zero-copy).
/// </summary>
public static void PopulateMerge<T>(byte[] data, T target) where T : class
=> PopulateMerge(data, 0, data.Length, target, AcBinarySerializerOptions.Default);
/// <summary>
/// Populate with merge semantics for IId collections from byte[] with options (zero-copy).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void PopulateMerge<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
=> PopulateMerge(data, 0, data.Length, target, options);
/// <summary>
/// Populate with merge semantics for IId collections from a sub-range of byte[].
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void PopulateMerge<T>(byte[] data, int offset, int length, T target) where T : class
=> PopulateMerge(data, offset, length, target, AcBinarySerializerOptions.Default);
/// <summary>
/// Populate with merge semantics for IId collections from a sub-range with options.
/// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
/// </summary>
public static void PopulateMerge<T>(byte[] data, int offset, int length, T target, AcBinarySerializerOptions? options) where T : class
{
ArgumentNullException.ThrowIfNull(target);
if (length == 0) return;
if (length == 1 && data[offset] == BinaryTypeCode.Null) return;
var opts = options ?? AcBinarySerializerOptions.Default;
var context = DeserializationContextPool<ArrayBinaryInput>.Get(opts);
context.InitInput(new ArrayBinaryInput(data, offset, length));
context.IsMergeMode = true;
context.RemoveOrphanedItems = opts.RemoveOrphanedItems;
try
{
PopulateMergeCore(context, target);
}
finally
{
DeserializationContextPool<ArrayBinaryInput>.Return(context);
}
}
/// <summary>
/// Core merge populate: handles IId collection merge for top-level arrays.
/// </summary>
private static void PopulateMergeCore<TInput>(BinaryDeserializationContext<TInput> context, object target)
where TInput : struct, IBinaryInputBase
{
var targetType = target.GetType();
try
{
context.ReadHeader();
var typeCode = context.PeekByte();
if (typeCode < BinaryTypeCode.SlotCount)
{
// FixObj slot: marker byte is the slot index
context.ReadByte();
context.GetWrapper(targetType, typeCode);
if (typeCode >= context._nextRuntimeSlot)
context._nextRuntimeSlot = typeCode + 1;
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.Object)
{
context.ReadByte();
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.ObjectRefFirst)
{
context.ReadByte();
var cacheIndex = (int)context.ReadVarUInt();
context.RegisterInternedValueAt(cacheIndex, target);
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
{
context.ReadByte();
ReadInlineMetadataForPopulate(context, targetType);
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst)
{
context.ReadByte();
var cacheIndex = (int)context.ReadVarUInt();
context.RegisterInternedValueAt(cacheIndex, target);
ReadInlineMetadataForPopulate(context, targetType);
PopulateObject(context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
{
context.ReadByte();
var elementType = GetCollectionElementType(targetType);
if (elementType != null)
{
var wrapper = context.GetWrapper(elementType);
var elementMetadata = wrapper.Metadata;
if (elementMetadata.IsComplexType && elementMetadata.IsIId && elementMetadata.IdGetter != null)
{
MergeIIdCollectionWithMetadata(context, targetList, elementType, wrapper, 0);
return;
}
}
PopulateList(context, targetList, targetType, 0);
}
else
{
throw new AcBinaryDeserializationException(
$"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
context.Position, targetType);
}
}
catch (AcBinaryDeserializationException) { throw; }
catch (Exception ex)
{
throw new AcBinaryDeserializationException(
$"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}",
context.Position, targetType, ex);
}
}
#endregion
#region Chain API
/// <summary>
/// Create a deserialize chain that parses binary data once and allows multiple deserializations.
/// Maintains reference identity for IId objects across chain operations.
/// </summary>
public static IDeserializeChain<T> CreateDeserializeChain<T>(byte[] data)
=> CreateDeserializeChain<T>(data, AcBinarySerializerOptions.Default);
/// <summary>
/// Create a deserialize chain with options.
/// </summary>
public static IDeserializeChain<T> CreateDeserializeChain<T>(byte[] data, AcBinarySerializerOptions options)
{
if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null))
return EmptyDeserializeChain<T>.Instance;
var chainTracker = new AcSerializerCommon.ChainReferenceTracker();
var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
context.InitInput(new ArrayBinaryInput(data));
context.ChainTracker = chainTracker;
try
{
var result = (T?)DeserializeCore(context, typeof(T));
return new BinaryDeserializeChain<T>(data, options, chainTracker, result);
}
finally
{
DeserializationContextPool<ArrayBinaryInput>.Return(context);
}
}
#endregion
#region Chain Implementations
/// <summary>
/// Binary implementation of deserialize chain.
/// Maintains reference identity for IId objects across chain operations.
/// </summary>
private sealed class BinaryDeserializeChain<T> : IDeserializeChain<T>
{
private readonly byte[] _data;
private readonly AcBinarySerializerOptions _options;
private readonly AcSerializerCommon.ChainReferenceTracker _chainTracker;
private bool _isDisposed;
public T? Value { get; }
public BinaryDeserializeChain(byte[] data, AcBinarySerializerOptions options, AcSerializerCommon.ChainReferenceTracker chainTracker, T? value)
{
_data = data;
_options = options;
_chainTracker = chainTracker;
Value = value;
}
public TResult? ThenDeserialize<TResult>()
{
ThrowIfDisposed();
var context = DeserializationContextPool<ArrayBinaryInput>.Get(_options);
context.InitInput(new ArrayBinaryInput(_data));
context.ChainTracker = _chainTracker;
try { return (TResult?)DeserializeCore(context, typeof(TResult)); }
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
}
public IDeserializeChain<T> ThenPopulate(object target)
{
ArgumentNullException.ThrowIfNull(target);
ThrowIfDisposed();
var context = DeserializationContextPool<ArrayBinaryInput>.Get(_options);
context.InitInput(new ArrayBinaryInput(_data));
context.ChainTracker = _chainTracker;
try
{
PopulateCore(context, target);
return this;
}
finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_isDisposed, this);
}
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
_chainTracker.Clear();
}
}
#endregion
#region Value Reading
/// <summary>
/// Tries to read and set a primitive value directly using typed setters to avoid boxing.
/// The type code marker byte is already consumed by the caller.
/// Returns true if handled, false if should fall back to generic path.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryReadAndSetTypedValue<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterBase propInfo, byte typeCode)
where TInput : struct, IBinaryInputBase
{
// Only handle if we have a typed setter
if (propInfo.AccessorType == PropertyAccessorType.Object)
return false;
// Handle based on property setter type and incoming data type.
// The marker byte (typeCode) is already consumed — no ReadByte() needed.
switch (propInfo.AccessorType)
{
case PropertyAccessorType.Int32:
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
if (typeCode == BinaryTypeCode.Int32)
{
propInfo.SetInt32(target, context.ReadVarInt());
return true;
}
break;
case PropertyAccessorType.Int64:
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
if (typeCode == BinaryTypeCode.Int32)
{
propInfo.SetInt64(target, context.ReadVarInt());
return true;
}
if (typeCode == BinaryTypeCode.Int64)
{
propInfo.SetInt64(target, context.ReadVarLong());
return true;
}
break;
case PropertyAccessorType.Boolean:
if (typeCode == BinaryTypeCode.True)
{
propInfo.SetBoolean(target, true);
return true;
}
if (typeCode == BinaryTypeCode.False)
{
propInfo.SetBoolean(target, false);
return true;
}
break;
case PropertyAccessorType.Double:
if (typeCode == BinaryTypeCode.Float64)
{
propInfo.SetDouble(target, context.ReadDoubleUnsafe());
return true;
}
break;
case PropertyAccessorType.Single:
if (typeCode == BinaryTypeCode.Float32)
{
propInfo.SetSingle(target, context.ReadSingleUnsafe());
return true;
}
break;
case PropertyAccessorType.Decimal:
if (typeCode == BinaryTypeCode.Decimal)
{
propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
return true;
}
break;
case PropertyAccessorType.DateTime:
if (typeCode == BinaryTypeCode.DateTime)
{
propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
return true;
}
break;
case PropertyAccessorType.Guid:
if (typeCode == BinaryTypeCode.Guid)
{
propInfo.SetGuid(target, context.ReadGuidUnsafe());
return true;
}
break;
case PropertyAccessorType.Byte:
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
if (typeCode == BinaryTypeCode.UInt8)
{
propInfo.SetByte(target, context.ReadByte());
return true;
}
break;
case PropertyAccessorType.Int16:
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
if (typeCode == BinaryTypeCode.Int16)
{
propInfo.SetInt16(target, context.ReadInt16Unsafe());
return true;
}
break;
case PropertyAccessorType.UInt16:
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
if (typeCode == BinaryTypeCode.UInt16)
{
propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
return true;
}
break;
case PropertyAccessorType.UInt32:
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
if (typeCode == BinaryTypeCode.UInt32)
{
propInfo.SetUInt32(target, context.ReadVarUInt());
return true;
}
break;
case PropertyAccessorType.UInt64:
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
if (typeCode == BinaryTypeCode.UInt64)
{
propInfo.SetUInt64(target, context.ReadVarULong());
return true;
}
break;
case PropertyAccessorType.Enum:
if (typeCode == BinaryTypeCode.Enum)
{
var enumByte = context.ReadByte();
int enumValue;
if (BinaryTypeCode.IsTinyInt(enumByte))
enumValue = BinaryTypeCode.DecodeTinyInt(enumByte);
else if (enumByte == BinaryTypeCode.Int32)
enumValue = context.ReadVarInt();
else
return false;
propInfo.SetEnumAsInt32(target, enumValue);
return true;
}
// Enum can also be encoded as TinyInt directly
if (BinaryTypeCode.IsTinyInt(typeCode))
{
propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
return true;
}
break;
case PropertyAccessorType.String:
if (BinaryTypeCode.IsFixStr(typeCode))
{
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length));
return true;
}
if (typeCode == BinaryTypeCode.String)
{
propInfo.SetValue(target, ReadPlainString(context));
return true;
}
if (typeCode == BinaryTypeCode.StringEmpty)
{
propInfo.SetValue(target, string.Empty);
return true;
}
if (typeCode == BinaryTypeCode.StringInterned)
{
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
return true;
}
if (typeCode == BinaryTypeCode.StringInternFirst)
{
propInfo.SetValue(target, ReadAndRegisterInternedString(context));
return true;
}
break;
}
return false;
}
/// <summary>
/// Bridge for generated readers to call ReadValue for unknown/complex/collection property types.
/// Reads typeCode + dispatches via TypeReaderTable — same as runtime ReadValue.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static object? ReadValueGenerated<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
return ReadValue(context, targetType, depth);
}
/// <summary>
/// Optimized value reader using FrozenDictionary dispatch table.
/// </summary>
private static object? ReadValue<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
if (context.IsAtEnd) return null;
var typeCode = context.ReadByte();
// Handle tiny int first (most common case for small integers, >= 192)
if (BinaryTypeCode.IsTinyInt(typeCode))
{
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
return ConvertToTargetType(intValue, targetType);
}
// Handle null
if (typeCode == BinaryTypeCode.Null) return null;
// Handle FixStr (short strings with length in type code)
if (BinaryTypeCode.IsFixStr(typeCode))
{
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
return length == 0 ? string.Empty : context.ReadStringUtf8(length);
}
var reader = TypeReaderTable<TInput>.Readers[typeCode];
if (reader != null)
{
return reader(context, targetType, depth);
}
throw new AcBinaryDeserializationException(
$"Unknown type code: {typeCode}",
context.Position, targetType);
}
/// <summary>
/// Sima string olvas<61>sa - NEM regisztr<74>l az intern t<>bl<62>ba.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadPlainString<TInput>(BinaryDeserializationContext<TInput> context)
where TInput : struct, IBinaryInputBase
{
var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty;
return context.ReadStringUtf8(length);
}
/// <summary>
/// Read interned string (StringInternFirst marker) and register in cache at specified index.
/// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadAndRegisterInternedString<TInput>(BinaryDeserializationContext<TInput> context)
where TInput : struct, IBinaryInputBase
{
// First StringInternFirst marker proves payload uses string interning →
// plain String entries appear only once, so _stringCache would never hit
context.DisableStringCaching();
var cacheIndex = (int)context.ReadVarUInt();
var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty;
var str = context.ReadStringUtf8(length);
context.RegisterInternedValueAt(cacheIndex, str);
return str;
}
///// <summary>
///// Read a string and register it in the intern table for future references.
///// </summary>
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//private static string ReadAndInternString<TInput>(BinaryDeserializationContext<TInput> context, int streamPosition)
//{
// var length = (int)context.ReadVarUInt();
// if (length == 0) return string.Empty;
// var str = context.ReadStringUtf8(length);
// // Always register strings that meet the minimum intern length threshold
// if (str.Length >= context.MinStringInternLength)
// {
// context.RegisterInternedString(str, streamPosition);
// }
// return str;
//}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadInt32Value<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
where TInput : struct, IBinaryInputBase
{
var value = context.ReadVarInt();
return ConvertToTargetType(value, targetType);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ConvertToTargetType(int value, Type targetType)
{
var info = GetConversionInfo(targetType);
if (info.IsEnum)
return Enum.ToObject(info.UnderlyingType, value);
return info.TypeCode switch
{
TypeCode.Int32 => value,
TypeCode.Int64 => (long)value,
TypeCode.Int16 => (short)value,
TypeCode.Byte => (byte)value,
TypeCode.SByte => (sbyte)value,
TypeCode.UInt16 => (ushort)value,
TypeCode.UInt32 => (uint)value,
TypeCode.UInt64 => (ulong)value,
TypeCode.Double => (double)value,
TypeCode.Single => (float)value,
TypeCode.Decimal => (decimal)value,
_ => value
};
}
/// <summary>
/// Gets type conversion info with ThreadLocal caching.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeConversionInfo GetConversionInfo(Type targetType)
{
// Fast path: check ThreadLocal cache first
var localCache = t_typeConversionLocalCache;
if (localCache != null && localCache.TryGetValue(targetType, out var cached))
{
return cached;
}
// Slow path
return GetConversionInfoSlow(targetType);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static TypeConversionInfo GetConversionInfoSlow(Type targetType)
{
var info = TypeConversionCache.GetOrAdd(targetType, static type =>
{
var underlying = Nullable.GetUnderlyingType(type) ?? type;
return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
});
// Get or create local cache
var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();
// Clear when full - reuses internal array, better than new Dictionary()
if (localCache.Count >= TypeMetadataBase.MaxLocalCacheSize)
{
localCache.Clear();
}
localCache[targetType] = info;
return info;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadEnumValue<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
where TInput : struct, IBinaryInputBase
{
var info = GetConversionInfo(targetType);
var nextByte = context.ReadByte();
int intValue;
if (BinaryTypeCode.IsTinyInt(nextByte))
{
intValue = BinaryTypeCode.DecodeTinyInt(nextByte);
}
else if (nextByte == BinaryTypeCode.Int32)
{
intValue = context.ReadVarInt();
}
else
{
throw new AcBinaryDeserializationException(
$"Invalid enum encoding: {nextByte}",
context.Position, targetType);
}
return info.IsEnum ? Enum.ToObject(info.UnderlyingType, intValue) : intValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte[] ReadByteArray<TInput>(BinaryDeserializationContext<TInput> context)
where TInput : struct, IBinaryInputBase
{
var length = (int)context.ReadVarUInt();
if (length == 0) return [];
return context.ReadBytes(length);
}
#endregion
#region Object Reading
/// <summary>
/// Reads an ObjectRef - looks up previously registered object from shared intern cache.
/// Wire format: [ObjectRef][VarUInt cacheIndex]
///
/// IMPORTANT / BUG FIX TODO:
/// Cross-type deserialization esetén ha egy objektum SkipObject-tel lett átugorva
/// (mert a target típuson nincs megfelelő property), de később ObjectRef hivatkozik rá,
/// az intern cache-ben nincs objektum → exception.
///
/// Megoldás: SkipObject során a stream pozíciót kell beírni a cache-be (boxolt int).
/// Itt az "is int" check-kel meg kell különböztetni:
/// - Ha a cached value valódi objektum → visszaadjuk (jelenlegi működés)
/// - Ha a cached value boxolt int (stream pozíció) → reposition + ReadObject a target type-ra,
/// majd az eredményt visszaírjuk a cache-be (hogy a következő ref ne olvasson újra)
///
/// Az "is int" check biztonságos, mert az intern cache-be csak string és class instance
/// kerülhet — egyik sem matchel az "is int"-re. A check az ObjectRef path-ban van,
/// ami ritka eset (2+ referencia), tehát nem lassítja a hot path-ot.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRef<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var cacheIndex = (int)context.ReadVarUInt();
return context.GetInternedObject(cacheIndex);
}
/// <summary>
/// FixObj slot read: marker byte (0..SlotCount-1) is the slot index.
/// First occurrence: wrapper is null in slot → resolve from targetType, cache in slot.
/// Subsequent: direct array access (~1-2ns).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectFromSlot<TInput>(
BinaryDeserializationContext<TInput> context,
int slot,
Type targetType,
int depth)
where TInput : struct, IBinaryInputBase
{
var wrapper = context.GetWrapper(targetType, slot);
// Track highest slot used for Clear()
if (slot >= context._nextRuntimeSlot)
context._nextRuntimeSlot = slot + 1;
// SGen fast path (same as ReadObjectCore)
if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode)
{
var generatedReader = wrapper.GeneratedReader;
if (generatedReader != null)
return generatedReader.ReadObject(context, depth, cacheIndex: -1);
}
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex: -1);
}
/// <summary>
/// Object olvasása (nem tracked, vagy UseMetadata nélkül).
/// Wire format: [Object][props...]
/// </summary>
private static object? ReadObject<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
return ReadObjectCore(context, targetType, depth, cacheIndex: -1);
}
/// <summary>
/// Object olvasása első előforduláskor (ObjectRefFirst marker).
/// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
/// Az objektumot regisztráljuk a cache-be a megadott index-re.
/// </summary>
private static object? ReadObjectRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var cacheIndex = (int)context.ReadVarUInt();
return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex);
}
/// <summary>
/// Polymorphic PREFIX marker: declared type ≠ runtime type.
/// Wire format: [ObjectWithTypeName (68)] [TypeName string] [inner marker: Object/Array/Dict/...] [body...]
/// Reads the runtime type name, resolves it, registers wrapper in poly slot cache,
/// then reads the inner marker via ReadValue.
/// </summary>
private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var typeName = ReadPlainString(context);
var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
?? throw new AcBinaryDeserializationException(
$"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.",
context.Position, null);
var wrapper = context.GetWrapper(resolvedType);
context.RegisterPolymorphicWrapper(wrapper);
// Next byte is the actual inner marker (Object/Array/Dict/etc.) — read it via ReadValue
return ReadValue(context, resolvedType, depth);
}
/// <summary>
/// Polymorphic COMBINED marker: first type occurrence + ref tracking first occurrence.
/// Wire format: [ObjectWithTypeNameRefFirst (69)] [TypeName string] [VarUInt refCacheIndex] [properties...]
/// Object body follows directly — no inner Object/ObjectRefFirst marker.
/// </summary>
private static object? ReadObjectWithTypeNameRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var typeName = ReadPlainString(context);
var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
?? throw new AcBinaryDeserializationException(
$"Cannot resolve type '{typeName}' for ObjectWithTypeNameRefFirst at position {context.Position}.",
context.Position, null);
var wrapper = context.GetWrapper(resolvedType);
context.RegisterPolymorphicWrapper(wrapper);
var cacheIndex = (int)context.ReadVarUInt();
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
}
/// <summary>
/// Polymorphic PREFIX marker with cached type index.
/// Wire format: [ObjectWithTypeIndex (70)] [VarUInt typeIndex] [inner marker: Object/Array/Dict/...] [body...]
/// Looks up the previously registered wrapper by index (~1-2ns array access),
/// then reads the inner marker via ReadValue.
/// </summary>
private static object? ReadObjectWithTypeIndex<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var typeIndex = (int)context.ReadVarUInt();
var wrapper = context.GetPolymorphicWrapper(typeIndex);
return ReadValue(context, wrapper.Metadata.SourceType, depth);
}
/// <summary>
/// Polymorphic COMBINED marker: cached type index + ref tracking first occurrence.
/// Wire format: [ObjectWithTypeIndexRefFirst (71)] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]
/// Object body follows directly — no inner Object/ObjectRefFirst marker. 0 dictionary lookup.
/// </summary>
private static object? ReadObjectWithTypeIndexRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var typeIndex = (int)context.ReadVarUInt();
var wrapper = context.GetPolymorphicWrapper(typeIndex);
var cacheIndex = (int)context.ReadVarUInt();
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
}
/// <summary>
/// Object olvasás core implementáció.
/// </summary>
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
private static object? ReadObjectCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth, int cacheIndex)
where TInput : struct, IBinaryInputBase
{
// Handle dictionary types
if (IsDictionaryType(targetType, out var keyType, out var valueType))
{
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
}
var wrapper = context.GetWrapper(targetType);
// SGen fast path: generated reader bypasses GetWrapper + CreateInstance + PopulateObject entirely.
// Only when not in UseMetadata mode (cross-type CacheMap not known at compile time),
// not in ChainMode (needs post-read identity tracking),
// and UseGeneratedCode is enabled (matching serializer-side check).
if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode)
{
var generatedReader = wrapper.GeneratedReader;
if (generatedReader != null)
return generatedReader.ReadObject(context, depth, cacheIndex);
}
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
}
/// <summary>
/// Object olvasás with pre-resolved wrapper (eliminates GetWrapper dictionary lookup).
/// </summary>
private static object? ReadObjectCoreWithWrapper<TInput>(BinaryDeserializationContext<TInput> context, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int depth, int cacheIndex)
where TInput : struct, IBinaryInputBase
{
var metadata = wrapper.Metadata;
var targetType = metadata.SourceType;
var instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
if (cacheIndex >= 0)
{
context.RegisterInternedValueAt(cacheIndex, instance);
}
PopulateObject(context, instance, wrapper, depth, skipDefaultWrite: true);
// ChainMode kezelés
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
{
var id = GetIdBoxed(instance, metadata);
if (id != null && !IsDefaultValue(id, metadata.IdType))
{
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
{
CopyProperties(instance, existingObj!, metadata);
return existingObj;
}
context.ChainTracker.TryRegisterIIdObject(instance);
}
}
return instance;
}
/// <summary>
/// Object olvasása UseMetadata módban (nem tracked).
/// Wire format:
/// Első előfordulás: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...]
/// Ismételt: [ObjectWithMetadata][propNameHash (4b)][props...]
/// </summary>
private static object? ReadObjectWithMetadata<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: -1);
}
/// <summary>
/// Object olvasása UseMetadata módban, első tracked előfordulás (ObjectWithMetadataRefFirst marker).
/// Wire format: [ObjectWithMetadataRefFirst][VarUInt cacheIndex][propNameHash (4b)][...][props...]
/// Az objektumot regisztráljuk a cache-be a megadott index-re.
/// </summary>
private static object? ReadObjectWithMetadataRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var cacheIndex = (int)context.ReadVarUInt();
return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: cacheIndex);
}
/// <summary>
/// ObjectWithMetadata olvasás core implementáció.
/// </summary>
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
private static object? ReadObjectWithMetadataCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth, int cacheIndex)
where TInput : struct, IBinaryInputBase
{
// Inline metadata: propNameHash mindig jön
var propNameHash = context.ReadInt32Raw();
// Source hash-ek keresése — ha nincs, első előfordulás → olvasás a stream-ből
var sourceHashes = context.FindSourceHashes(propNameHash);
if (sourceHashes == null)
{
var propCount = (int)context.ReadVarUInt();
sourceHashes = new int[propCount];
for (var i = 0; i < propCount; i++)
{
sourceHashes[i] = context.ReadInt32Raw();
}
context.RegisterInlineMetadata(propNameHash, sourceHashes);
}
// Handle dictionary types
if (IsDictionaryType(targetType, out var keyType, out var valueType))
{
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
}
var wrapper = context.GetWrapper(targetType);
var metadata = wrapper.Metadata;
var instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
if (cacheIndex >= 0)
{
context.RegisterInternedValueAt(cacheIndex, instance);
}
// CacheMap felépítése ha még nincs
if (wrapper.CacheMap == null)
BuildCacheMap(wrapper, sourceHashes);
PopulateObject(context, instance, wrapper, depth, skipDefaultWrite: true);
// ChainMode kezelés
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
{
var id = GetIdBoxed(instance, metadata);
if (id != null && !IsDefaultValue(id, metadata.IdType))
{
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
{
CopyProperties(instance, existingObj!, metadata);
return existingObj;
}
context.ChainTracker.TryRegisterIIdObject(instance);
}
}
return instance;
}
/// <summary>
/// Inline metadata olvasása a Populate path-hoz.
/// Az ObjectWithMetadata marker már consume-álva van.
/// Beolvassa a propNameHash-t + hash-eket (első előfordulásnál), és felépíti a cacheMap-et.
/// </summary>
private static void ReadInlineMetadataForPopulate<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
where TInput : struct, IBinaryInputBase
{
var propNameHash = context.ReadInt32Raw();
var sourceHashes = context.FindSourceHashes(propNameHash);
if (sourceHashes == null)
{
var propCount = (int)context.ReadVarUInt();
sourceHashes = new int[propCount];
for (var i = 0; i < propCount; i++)
{
sourceHashes[i] = context.ReadInt32Raw();
}
context.RegisterInlineMetadata(propNameHash, sourceHashes);
}
var wrapper = context.GetWrapper(targetType);
if (wrapper.CacheMap == null)
BuildCacheMap(wrapper, sourceHashes);
}
/// <summary>
/// CacheMap felépítése: source property hash-ek → target PropertySetter mapping.
/// Fast path: same-type esetén props[index+1] egyezik. Cross-type: keresés index+1-től.
/// </summary>
private static void BuildCacheMap(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int[] sourceHashes)
{
var properties = wrapper.Metadata.PropertiesArray;
var cacheMap = new BinaryPropertySetterBase?[sourceHashes.Length];
var index = -1;
for (var i = 0; i < sourceHashes.Length; i++)
{
var hash = sourceHashes[i];
var nextIdx = index + 1;
if (nextIdx < properties.Length && properties[nextIdx].PropertyNameHash == hash)
{
cacheMap[i] = properties[nextIdx];
index = nextIdx;
}
else
{
for (var j = nextIdx; j < properties.Length; j++)
{
if (properties[j].PropertyNameHash == hash)
{
cacheMap[i] = properties[j];
index = j;
break;
}
}
}
}
wrapper.CacheMap = cacheMap;
}
#endregion
#region Array Reading
private static object? ReadArray<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var elementType = GetCollectionElementType(targetType);
if (elementType == null) elementType = typeof(object);
var count = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
// Optimized path for primitive arrays
if (targetType.IsArray && count > 0)
{
var result = TryReadPrimitiveArray(context, elementType, count);
if (result != null) return result;
}
if (targetType.IsArray)
{
var array = Array.CreateInstance(elementType, count);
for (int i = 0; i < count; i++)
{
var value = ReadValue(context, elementType, nextDepth);
array.SetValue(value, i);
}
return array;
}
IList? list = null;
try
{
var instance = Activator.CreateInstance(targetType);
if (instance is IList l) list = l;
}
catch
{
/* Fallback to List<T> */
}
list ??= GetOrCreateListFactory(elementType)(count);
var acObservable = list as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
for (int i = 0; i < count; i++)
{
var value = ReadValue(context, elementType, nextDepth);
list.Add(value);
}
}
finally
{
acObservable?.EndUpdate();
}
return list;
}
/// <summary>
/// Optimized primitive array reader using bulk operations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Array? TryReadPrimitiveArray<TInput>(BinaryDeserializationContext<TInput> context, Type elementType, int count)
where TInput : struct, IBinaryInputBase
{
// Int32 array
if (ReferenceEquals(elementType, IntType))
{
var array = new int[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (BinaryTypeCode.IsTinyInt(typeCode))
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
else if (typeCode == BinaryTypeCode.Int32)
array[i] = context.ReadVarInt();
else
return null; // Fall back to generic path
}
return array;
}
// Double array
if (ReferenceEquals(elementType, DoubleType))
{
var array = new double[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Float64) return null;
array[i] = context.ReadDoubleUnsafe();
}
return array;
}
// Long array
if (ReferenceEquals(elementType, LongType))
{
var array = new long[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (BinaryTypeCode.IsTinyInt(typeCode))
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
else if (typeCode == BinaryTypeCode.Int32)
array[i] = context.ReadVarInt();
else if (typeCode == BinaryTypeCode.Int64)
array[i] = context.ReadVarLong();
else
return null;
}
return array;
}
// Bool array
if (ReferenceEquals(elementType, BoolType))
{
var array = new bool[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.True) array[i] = true;
else if (typeCode == BinaryTypeCode.False) array[i] = false;
else return null;
}
return array;
}
// Guid array
if (ReferenceEquals(elementType, GuidType))
{
var array = new Guid[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Guid) return null;
array[i] = context.ReadGuidUnsafe();
}
return array;
}
// Decimal array
if (ReferenceEquals(elementType, DecimalType))
{
var array = new decimal[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Decimal) return null;
array[i] = context.ReadDecimalUnsafe();
}
return array;
}
// DateTime array
if (ReferenceEquals(elementType, DateTimeType))
{
var array = new DateTime[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.DateTime) return null;
array[i] = context.ReadDateTimeUnsafe();
}
return array;
}
// Float array
if (ReferenceEquals(elementType, FloatType))
{
var array = new float[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Float32) return null;
array[i] = context.ReadSingleUnsafe();
}
return array;
}
// Short (Int16) array
if (ReferenceEquals(elementType, ShortType))
{
var array = new short[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Int16) return null;
array[i] = context.ReadInt16Unsafe();
}
return array;
}
// UShort (UInt16) array
if (ReferenceEquals(elementType, UShortType))
{
var array = new ushort[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.UInt16) return null;
array[i] = context.ReadUInt16Unsafe();
}
return array;
}
// UInt32 array
if (ReferenceEquals(elementType, UIntType))
{
var array = new uint[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.UInt32) return null;
array[i] = context.ReadVarUInt();
}
return array;
}
// UInt64 array
if (ReferenceEquals(elementType, ULongType))
{
var array = new ulong[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.UInt64) return null;
array[i] = context.ReadVarULong();
}
return array;
}
// SByte (Int8) array
if (ReferenceEquals(elementType, SByteType))
{
var array = new sbyte[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Int8) return null;
array[i] = unchecked((sbyte)context.ReadByte());
}
return array;
}
// Char array
if (ReferenceEquals(elementType, CharType))
{
var array = new char[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.Char) return null;
array[i] = context.ReadCharUnsafe();
}
return array;
}
// DateTimeOffset array
if (ReferenceEquals(elementType, DateTimeOffsetType))
{
var array = new DateTimeOffset[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.DateTimeOffset) return null;
array[i] = context.ReadDateTimeOffsetUnsafe();
}
return array;
}
// TimeSpan array
if (ReferenceEquals(elementType, TimeSpanType))
{
var array = new TimeSpan[count];
for (int i = 0; i < count; i++)
{
var typeCode = context.ReadByte();
if (typeCode != BinaryTypeCode.TimeSpan) return null;
array[i] = context.ReadTimeSpanUnsafe();
}
return array;
}
return null;
}
#endregion
#region Dictionary Reading
private static object? ReadDictionary<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
if (!IsDictionaryType(targetType, out var keyType, out var valueType))
{
keyType = typeof(string);
valueType = typeof(object);
}
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
}
private static object ReadDictionaryAsObject<TInput>(BinaryDeserializationContext<TInput> context, Type keyType, Type valueType, int depth)
where TInput : struct, IBinaryInputBase
{
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
var count = (int)context.ReadVarUInt();
var dict = (IDictionary)Activator.CreateInstance(dictType, count)!;
var nextDepth = depth + 1;
for (int i = 0; i < count; i++)
{
var key = ReadValue(context, keyType, nextDepth);
var value = ReadValue(context, valueType, nextDepth);
if (key != null)
dict.Add(key, value);
}
return dict;
}
#endregion
#region Skip Value
private static void SkipValue<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
where TInput : struct, IBinaryInputBase
{
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.Null) return;
if (BinaryTypeCode.IsTinyInt(typeCode)) return;
// Handle FixStr (short strings)
if (BinaryTypeCode.IsFixStr(typeCode))
{
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
if (length > 0)
context.Skip(length);
return;
}
switch (typeCode)
{
case BinaryTypeCode.True:
case BinaryTypeCode.False:
case BinaryTypeCode.StringEmpty:
return;
case BinaryTypeCode.Int8:
case BinaryTypeCode.UInt8:
context.Skip(1);
return;
case BinaryTypeCode.Int16:
case BinaryTypeCode.UInt16:
case BinaryTypeCode.Char:
context.Skip(2);
return;
case BinaryTypeCode.Int32:
context.ReadVarInt();
return;
case BinaryTypeCode.UInt32:
context.ReadVarUInt();
return;
case BinaryTypeCode.Float32:
context.Skip(4);
return;
case BinaryTypeCode.Int64:
context.ReadVarLong();
return;
case BinaryTypeCode.UInt64:
context.ReadVarULong();
return;
case BinaryTypeCode.Float64:
case BinaryTypeCode.TimeSpan:
context.Skip(8);
return;
case BinaryTypeCode.DateTime:
context.Skip(9);
return;
case BinaryTypeCode.DateTimeOffset:
context.Skip(10);
return;
case BinaryTypeCode.Guid:
case BinaryTypeCode.Decimal:
context.Skip(16);
return;
case BinaryTypeCode.String:
SkipPlainString(context);
return;
case BinaryTypeCode.StringInterned:
context.ReadVarUInt();
return;
case BinaryTypeCode.StringInternFirst:
// First occurrence - must register even when skipping
SkipAndRegisterInternedString(context);
return;
case BinaryTypeCode.ByteArray:
var byteLen = (int)context.ReadVarUInt();
context.Skip(byteLen);
return;
case BinaryTypeCode.Enum:
var enumByte = context.ReadByte();
if (BinaryTypeCode.IsTinyInt(enumByte)) return;
if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt();
return;
case BinaryTypeCode.Object:
SkipObject(context, metaData);
return;
case BinaryTypeCode.ObjectRefFirst:
SkipObjectRefFirst(context, metaData);
return;
case BinaryTypeCode.ObjectWithMetadata:
SkipObjectWithMetadata(context, metaData, cacheIndex: -1);
return;
case BinaryTypeCode.ObjectWithMetadataRefFirst:
{
var cacheIdx = (int)context.ReadVarUInt();
SkipObjectWithMetadata(context, metaData, cacheIndex: cacheIdx);
return;
}
case BinaryTypeCode.ObjectRef:
context.ReadVarUInt();
return;
case BinaryTypeCode.Array:
SkipArray(context, metaData);
return;
case BinaryTypeCode.Dictionary:
SkipDictionary(context, metaData);
return;
}
}
/// <summary>
/// Sima string kihagy<67>sa - NEM regisztr<74>l.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SkipPlainString<TInput>(BinaryDeserializationContext<TInput> context)
where TInput : struct, IBinaryInputBase
{
var byteLen = (int)context.ReadVarUInt();
if (byteLen > 0)
{
context.Skip(byteLen);
}
}
/// <summary>
/// Skip an interned string (StringInternFirst) - must still read cacheIndex and register in cache.
/// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes]
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SkipAndRegisterInternedString<TInput>(BinaryDeserializationContext<TInput> context)
where TInput : struct, IBinaryInputBase
{
var cacheIndex = (int)context.ReadVarUInt();
var byteLen = (int)context.ReadVarUInt();
if (byteLen == 0) return;
var str = context.ReadStringUtf8(byteLen);
context.RegisterInternedValueAt(cacheIndex, str);
}
/// <summary>
/// Skip ObjectRefFirst - must read cacheIndex and register placeholder in cache.
/// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
/// </summary>
private static void SkipObjectRefFirst<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
where TInput : struct, IBinaryInputBase
{
var cacheIndex = (int)context.ReadVarUInt();
// Register placeholder (stream position as boxed int for potential lazy load)
context.RegisterInternedValueAt(cacheIndex, context.Position);
SkipObject(context, metaData);
}
///// <summary>
///// Skip a string but still register it in the intern table if it meets the length threshold.
///// </summary>
///// <param name="context">Deserialization context</param>
///// <param name="streamPosition">Position before the type code was read</param>
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//private static void SkipAndInternString<TInput>(BinaryDeserializationContext<TInput> context, int streamPosition)
//{
// var byteLen = (int)context.ReadVarUInt();
// if (byteLen == 0) return;
// var str = context.ReadStringUtf8(byteLen);
// if (str.Length >= context.MinStringInternLength)
// {
// context.RegisterInternedString(str, streamPosition);
// }
//}
/// <summary>
/// Object kihagyása metadata nélkül — nem támogatott, mert nem tudjuk a property számot.
/// </summary>
private static void SkipObject<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
where TInput : struct, IBinaryInputBase
{
throw new NotSupportedException(
"SkipObject nem támogatott metadata nélkül. " +
"A property szám nem határozható meg típus metadata nélkül.");
}
/// <summary>
/// Skip ObjectWithMetadata/ObjectWithMetadataRefFirst.
/// </summary>
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
private static void SkipObjectWithMetadata<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData, int cacheIndex)
where TInput : struct, IBinaryInputBase
{
if (cacheIndex >= 0)
{
// Register placeholder for potential lazy load
context.RegisterInternedValueAt(cacheIndex, context.Position);
}
var propNameHash = context.ReadInt32Raw();
var sourceHashes = context.FindSourceHashes(propNameHash);
if (sourceHashes == null)
{
var propCount = (int)context.ReadVarUInt();
sourceHashes = new int[propCount];
for (var i = 0; i < propCount; i++)
{
sourceHashes[i] = context.ReadInt32Raw();
}
context.RegisterInlineMetadata(propNameHash, sourceHashes);
}
// Skip all properties
for (var i = 0; i < sourceHashes.Length; i++)
{
SkipValue(context, metaData);
}
}
private static void SkipArray<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
where TInput : struct, IBinaryInputBase
{
var count = (int)context.ReadVarUInt();
for (int i = 0; i < count; i++)
{
SkipValue(context, metaData);
}
}
private static void SkipDictionary<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
where TInput : struct, IBinaryInputBase
{
var count = (int)context.ReadVarUInt();
for (int i = 0; i < count; i++)
{
SkipValue(context, metaData); // key
SkipValue(context, metaData); // value
}
}
#endregion
#region IId Registration Helpers
/// <summary>
/// Gets Id as boxed object - only used for ChainMode (rare path).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? GetIdBoxed(object instance, BinaryDeserializeTypeMetadata metadata)
{
return metadata.IdAccessorType switch
{
IdAccessorType.Int32 => metadata.GetIdInt32(instance),
IdAccessorType.Int64 => metadata.GetIdInt64(instance),
IdAccessorType.Guid => metadata.GetIdGuid(instance),
_ => null
};
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
{
if (metadata.CompiledConstructor != null)
return metadata.CompiledConstructor();
try
{
return Activator.CreateInstance(type);
}
catch (MissingMethodException ex)
{
throw new AcBinaryDeserializationException(
$"Cannot create instance of type '{type.FullName}' because it does not have a parameterless constructor.",
0, type, ex);
}
}
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();
}
#endregion
}
// Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs