AyCode.Core/AyCode.Core/Extensions/AcBinaryDeserializer.cs

1584 lines
57 KiB
C#

using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using AyCode.Core.Helpers;
using static AyCode.Core.Extensions.JsonUtilities;
namespace AyCode.Core.Extensions;
/// <summary>
/// Exception thrown when binary deserialization fails.
/// </summary>
public class AcBinaryDeserializationException : Exception
{
public int Position { get; }
public Type? TargetType { get; }
public AcBinaryDeserializationException(string message, int position = 0, Type? targetType = null, Exception? innerException = null)
: base(message, innerException)
{
Position = position;
TargetType = targetType;
}
}
/// <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 class AcBinaryDeserializer
{
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
// Type dispatch table for fast ReadValue
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
private static readonly FrozenDictionary<byte, TypeReader> TypeReaders;
static AcBinaryDeserializer()
{
// Initialize type reader dispatch table
var readers = new Dictionary<byte, TypeReader>
{
[BinaryTypeCode.Null] = static (ref BinaryDeserializationContext _, Type _, int _) => null,
[BinaryTypeCode.True] = static (ref BinaryDeserializationContext _, Type _, int _) => true,
[BinaryTypeCode.False] = static (ref BinaryDeserializationContext _, Type _, int _) => false,
[BinaryTypeCode.Int8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte(),
[BinaryTypeCode.UInt8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte(),
[BinaryTypeCode.Int16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe(),
[BinaryTypeCode.UInt16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe(),
[BinaryTypeCode.Int32] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type),
[BinaryTypeCode.UInt32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt(),
[BinaryTypeCode.Int64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong(),
[BinaryTypeCode.UInt64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong(),
[BinaryTypeCode.Float32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe(),
[BinaryTypeCode.Float64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe(),
[BinaryTypeCode.Decimal] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe(),
[BinaryTypeCode.Char] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe(),
[BinaryTypeCode.String] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx),
[BinaryTypeCode.StringInterned] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()),
[BinaryTypeCode.StringEmpty] = static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty,
[BinaryTypeCode.DateTime] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe(),
[BinaryTypeCode.DateTimeOffset] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe(),
[BinaryTypeCode.TimeSpan] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe(),
[BinaryTypeCode.Guid] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe(),
[BinaryTypeCode.Enum] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type),
[BinaryTypeCode.Object] = ReadObject,
[BinaryTypeCode.ObjectRef] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()),
[BinaryTypeCode.Array] = ReadArray,
[BinaryTypeCode.Dictionary] = ReadDictionary,
[BinaryTypeCode.ByteArray] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx),
};
TypeReaders = readers.ToFrozenDictionary();
}
#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.AsSpan());
/// <summary>
/// Deserialize binary data to object of type T.
/// </summary>
public static T? Deserialize<T>(ReadOnlySpan<byte> data)
{
if (data.Length == 0) return default;
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;
var targetType = typeof(T);
var context = new BinaryDeserializationContext(data);
try
{
context.ReadHeader();
var result = ReadValue(ref context, targetType, 0);
return (T?)result;
}
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>
/// Deserialize binary data to specified type.
/// </summary>
public static object? Deserialize(ReadOnlySpan<byte> data, Type targetType)
{
if (data.Length == 0) return null;
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null;
var context = new BinaryDeserializationContext(data);
try
{
context.ReadHeader();
return ReadValue(ref 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>
/// Populate existing object from binary data.
/// </summary>
public static void Populate<T>(byte[] data, T target) where T : class
=> Populate(data.AsSpan(), target);
/// <summary>
/// Populate existing object from binary data.
/// </summary>
public static void Populate<T>(ReadOnlySpan<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 targetType = target.GetType();
var context = new BinaryDeserializationContext(data);
try
{
context.ReadHeader();
var typeCode = context.PeekByte();
if (typeCode == BinaryTypeCode.Object)
{
context.ReadByte(); // consume Object marker
PopulateObject(ref context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
{
context.ReadByte(); // consume Array marker
PopulateList(ref 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 with merge semantics for IId collections.
/// </summary>
public static void PopulateMerge<T>(ReadOnlySpan<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 targetType = target.GetType();
var context = new BinaryDeserializationContext(data) { IsMergeMode = true };
try
{
context.ReadHeader();
var typeCode = context.PeekByte();
if (typeCode == BinaryTypeCode.Object)
{
context.ReadByte();
PopulateObjectMerge(ref context, target, targetType, 0);
}
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
{
context.ReadByte();
PopulateListMerge(ref 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 Value Reading
/// <summary>
/// Optimized value reader using FrozenDictionary dispatch table.
/// </summary>
private static object? ReadValue(ref BinaryDeserializationContext context, Type targetType, int depth)
{
if (context.IsAtEnd) return null;
var typeCode = context.ReadByte();
// Handle null first
if (typeCode == BinaryTypeCode.Null) return null;
// Handle tiny int (most common case for small integers)
if (BinaryTypeCode.IsTinyInt(typeCode))
{
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
return ConvertToTargetType(intValue, targetType);
}
// Use dispatch table for type-specific reading
if (TypeReaders.TryGetValue(typeCode, out var reader))
{
return reader(ref context, targetType, depth);
}
throw new AcBinaryDeserializationException(
$"Unknown type code: {typeCode}",
context.Position, targetType);
}
/// <summary>
/// Read a string and register it in the intern table for future references.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadAndInternString(ref BinaryDeserializationContext context)
{
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);
}
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadInt32Value(ref BinaryDeserializationContext context, Type targetType)
{
var value = context.ReadVarInt();
return ConvertToTargetType(value, targetType);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ConvertToTargetType(int value, Type targetType)
{
var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (underlying.IsEnum)
return Enum.ToObject(underlying, value);
var typeCode = Type.GetTypeCode(underlying);
return 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
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
{
var underlying = Nullable.GetUnderlyingType(targetType) ?? 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);
}
if (underlying.IsEnum)
return Enum.ToObject(underlying, intValue);
return intValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte[] ReadByteArray(ref BinaryDeserializationContext context)
{
var length = (int)context.ReadVarUInt();
if (length == 0) return [];
return context.ReadBytes(length);
}
#endregion
#region Object Reading
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth)
{
// Read reference ID if present
int refId = -1;
if (context.HasReferenceHandling)
{
refId = context.ReadVarInt();
}
// Handle dictionary types
if (IsDictionaryType(targetType, out var keyType, out var valueType))
{
return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth);
}
var metadata = GetTypeMetadata(targetType);
var instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
// Register reference
if (refId > 0)
{
context.RegisterObject(refId, instance);
}
PopulateObject(ref context, instance, metadata, depth);
return instance;
}
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{
var metadata = GetTypeMetadata(targetType);
// Skip ref ID if present
if (context.HasReferenceHandling)
{
var refId = context.ReadVarInt();
if (refId > 0)
{
context.RegisterObject(refId, target);
}
}
PopulateObject(ref context, target, metadata, depth);
}
private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth)
{
var propertyCount = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
for (int i = 0; i < propertyCount; i++)
{
string propertyName;
if (context.HasMetadata)
{
var propIndex = (int)context.ReadVarUInt();
propertyName = context.GetPropertyName(propIndex);
}
else
{
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.String)
{
propertyName = ReadAndInternString(ref context);
}
else if (typeCode == BinaryTypeCode.StringInterned)
{
propertyName = context.GetInternedString((int)context.ReadVarUInt());
}
else if (typeCode == BinaryTypeCode.StringEmpty)
{
propertyName = string.Empty;
}
else
{
throw new AcBinaryDeserializationException(
$"Expected string for property name, got: {typeCode}",
context.Position, target.GetType());
}
}
if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
{
// Skip unknown property
SkipValue(ref context);
continue;
}
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
propInfo.SetValue(target, value);
}
}
private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{
var metadata = GetTypeMetadata(targetType);
// Skip ref ID if present
if (context.HasReferenceHandling)
{
var refId = context.ReadVarInt();
if (refId > 0)
{
context.RegisterObject(refId, target);
}
}
var propertyCount = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
for (int i = 0; i < propertyCount; i++)
{
string propertyName;
if (context.HasMetadata)
{
var propIndex = (int)context.ReadVarUInt();
propertyName = context.GetPropertyName(propIndex);
}
else
{
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.String)
{
propertyName = ReadAndInternString(ref context);
}
else if (typeCode == BinaryTypeCode.StringInterned)
{
propertyName = context.GetInternedString((int)context.ReadVarUInt());
}
else if (typeCode == BinaryTypeCode.StringEmpty)
{
propertyName = string.Empty;
}
else
{
throw new AcBinaryDeserializationException(
$"Expected string for property name, got: {typeCode}",
context.Position, targetType);
}
}
if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
{
SkipValue(ref context);
continue;
}
var peekCode = context.PeekByte();
// Handle IId collection merge
if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array)
{
var existingCollection = propInfo.GetValue(target);
if (existingCollection is IList existingList)
{
context.ReadByte(); // consume Array marker
MergeIIdCollection(ref context, existingList, propInfo, depth);
continue;
}
}
// Handle nested object merge
if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
{
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
context.ReadByte(); // consume Object marker
PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth);
continue;
}
}
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
propInfo.SetValue(target, value);
}
}
/// <summary>
/// Optimized IId collection merge with capacity hints and reduced boxing.
/// </summary>
private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
{
var elementType = propInfo.ElementType!;
var idGetter = propInfo.ElementIdGetter!;
var idType = propInfo.ElementIdType!;
var count = existingList.Count;
var acObservable = existingList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
// Build lookup dictionary with capacity hint
Dictionary<object, object>? existingById = null;
if (count > 0)
{
existingById = new Dictionary<object, object>(count);
for (var idx = 0; idx < count; idx++)
{
var item = existingList[idx];
if (item != null)
{
var id = idGetter(item);
if (id != null && !IsDefaultValue(id, idType))
existingById[id] = item;
}
}
}
var arrayCount = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
var elementMetadata = GetTypeMetadata(elementType);
for (int i = 0; i < arrayCount; i++)
{
var itemCode = context.PeekByte();
if (itemCode != BinaryTypeCode.Object)
{
var value = ReadValue(ref context, elementType, nextDepth);
if (value != null)
existingList.Add(value);
continue;
}
context.ReadByte(); // consume Object marker
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
// Read ref ID if present
if (context.HasReferenceHandling)
{
var refId = context.ReadVarInt();
if (refId > 0)
context.RegisterObject(refId, newItem);
}
PopulateObject(ref context, newItem, elementMetadata, nextDepth);
var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
{
if (existingById.TryGetValue(itemId, out var existingItem))
{
// Copy properties to existing item
CopyProperties(newItem, existingItem, elementMetadata);
continue;
}
}
existingList.Add(newItem);
}
}
finally
{
acObservable?.EndUpdate();
}
}
private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
{
var props = metadata.PropertiesArray;
for (var i = 0; i < props.Length; i++)
{
var prop = props[i];
var value = prop.GetValue(source);
if (value != null)
prop.SetValue(target, value);
}
}
#endregion
#region Array Reading
private static object? ReadArray(ref BinaryDeserializationContext context, Type targetType, int depth)
{
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(ref 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(ref 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)();
var acObservable = list as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
for (int i = 0; i < count; i++)
{
var value = ReadValue(ref 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(ref BinaryDeserializationContext context, Type elementType, int count)
{
// 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;
}
return null;
}
private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
{
var elementType = GetCollectionElementType(listType) ?? typeof(object);
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
targetList.Clear();
var count = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
for (int i = 0; i < count; i++)
{
var value = ReadValue(ref context, elementType, nextDepth);
targetList.Add(value);
}
}
finally
{
acObservable?.EndUpdate();
}
}
private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
{
var elementType = GetCollectionElementType(listType) ?? typeof(object);
var (isId, idType) = GetIdInfo(elementType);
if (!isId || idType == null)
{
// No IId, just replace
PopulateList(ref context, targetList, listType, depth);
return;
}
// IId merge logic
var idProp = elementType.GetProperty("Id");
if (idProp == null)
{
PopulateList(ref context, targetList, listType, depth);
return;
}
var idGetter = CreateCompiledGetter(elementType, idProp);
var propInfo = new BinaryPropertySetterInfo(
"Items", elementType, true, elementType, idType, idGetter);
MergeIIdCollection(ref context, targetList, propInfo, depth);
}
#endregion
#region Dictionary Reading
private static object? ReadDictionary(ref BinaryDeserializationContext context, Type targetType, int depth)
{
if (!IsDictionaryType(targetType, out var keyType, out var valueType))
{
keyType = typeof(string);
valueType = typeof(object);
}
return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth);
}
private static object ReadDictionaryAsObject(ref BinaryDeserializationContext context, Type keyType, Type valueType, int depth)
{
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
var count = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
for (int i = 0; i < count; i++)
{
var key = ReadValue(ref context, keyType, nextDepth);
var value = ReadValue(ref context, valueType, nextDepth);
if (key != null)
dict.Add(key, value);
}
return dict;
}
#endregion
#region Skip Value
private static void SkipValue(ref BinaryDeserializationContext context)
{
var typeCode = context.ReadByte();
if (typeCode == BinaryTypeCode.Null) return;
if (BinaryTypeCode.IsTinyInt(typeCode)) 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(); // Skip VarInt
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:
// CRITICAL FIX: Must register string in intern table even when skipping!
SkipAndInternString(ref context);
return;
case BinaryTypeCode.StringInterned:
context.ReadVarUInt();
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(ref context);
return;
case BinaryTypeCode.ObjectRef:
context.ReadVarInt();
return;
case BinaryTypeCode.Array:
SkipArray(ref context);
return;
case BinaryTypeCode.Dictionary:
SkipDictionary(ref context);
return;
}
}
/// <summary>
/// Skip a string but still register it in the intern table if it meets the length threshold.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SkipAndInternString(ref BinaryDeserializationContext context)
{
var byteLen = (int)context.ReadVarUInt();
if (byteLen == 0) return;
var str = context.ReadStringUtf8(byteLen);
if (str.Length >= context.MinStringInternLength)
{
context.RegisterInternedString(str);
}
}
private static void SkipObject(ref BinaryDeserializationContext context)
{
// Skip ref ID if present
if (context.HasReferenceHandling)
{
context.ReadVarInt();
}
var propCount = (int)context.ReadVarUInt();
for (int i = 0; i < propCount; i++)
{
// Skip property name - but must register in intern table!
if (context.HasMetadata)
{
context.ReadVarUInt();
}
else
{
var nameCode = context.ReadByte();
if (nameCode == BinaryTypeCode.String)
{
// CRITICAL FIX: Must register property name in intern table even when skipping!
SkipAndInternString(ref context);
}
else if (nameCode == BinaryTypeCode.StringInterned)
{
// Just read the index, no registration needed
context.ReadVarUInt();
}
// StringEmpty doesn't need any action
}
// Skip value
SkipValue(ref context);
}
}
private static void SkipArray(ref BinaryDeserializationContext context)
{
var count = (int)context.ReadVarUInt();
for (int i = 0; i < count; i++)
{
SkipValue(ref context);
}
}
private static void SkipDictionary(ref BinaryDeserializationContext context)
{
var count = (int)context.ReadVarUInt();
for (int i = 0; i < count; i++)
{
SkipValue(ref context); // key
SkipValue(ref context); // value
}
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
[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();
}
internal sealed class BinaryDeserializeTypeMetadata
{
private readonly Dictionary<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);
}
_propertiesDict = new Dictionary<string, BinaryPropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
PropertiesArray = new BinaryPropertySetterInfo[propsList.Count];
for (int i = 0; i < propsList.Count; i++)
{
var prop = propsList[i];
var propInfo = new BinaryPropertySetterInfo(prop, type);
_propertiesDict[prop.Name] = propInfo;
PropertiesArray[i] = propInfo;
}
}
[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 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);
var isCollection = ElementType != null && ElementType != typeof(object) &&
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
!ReferenceEquals(PropertyType, StringType);
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;
ElementType = elementType;
ElementIdType = elementIdType;
ElementIdGetter = elementIdGetter;
_setter = (_, _) => { };
_getter = _ => null;
}
private static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var valueParam = Expression.Parameter(typeof(object), "value");
var castObj = Expression.Convert(objParam, declaringType);
var castValue = Expression.Convert(valueParam, prop.PropertyType);
var propAccess = Expression.Property(castObj, prop);
var assign = Expression.Assign(propAccess, castValue);
return Expression.Lambda<Action<object, object?>>(assign, objParam, valueParam).Compile();
}
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetValue(object target, object? value) => _setter(target, value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object target) => _getter(target);
}
#endregion
#region Deserialization Context
/// <summary>
/// Optimized deserialization context using ref struct for zero allocation.
/// Uses MemoryMarshal for fast primitive reads.
/// </summary>
internal ref struct BinaryDeserializationContext
{
private readonly ReadOnlySpan<byte> _data;
private int _position;
// Header info
public byte FormatVersion { get; private set; }
public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; }
/// <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; // Assume true by default
MinStringInternLength = 4; // Default from AcBinarySerializerOptions
_propertyNames = null;
_internedStrings = null;
_references = null;
IsMergeMode = false;
}
public void ReadHeader()
{
if (_data.Length < 2) return;
FormatVersion = ReadByte();
var flags = ReadByte();
// Handle new flag-based header format (34+)
if (flags >= BinaryTypeCode.HeaderFlagsBase)
{
HasMetadata = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 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);
}
}
}
}
[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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadStringUtf8(int byteCount)
{
if (_position + byteCount > _data.Length)
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
var result = Encoding.UTF8.GetString(_data.Slice(_position, byteCount));
_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)
{
_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
}