Refactor: new high-performance binary serializer/deserializer
Major overhaul of binary serialization: - Rewrote AcBinarySerializer as a static, optimized, feature-rich serializer with VarInt encoding, string interning, property name tables, reference handling, and optional metadata. - Added AcBinaryDeserializer with matching features, including merge/populate support and robust error handling. - Introduced AcBinarySerializerOptions and AcSerializerOptions base class for unified serializer configuration (JSON/binary). - Added generic extension methods for "any" serialization/deserialization based on options. - Updated tests and benchmarks for new APIs; fixed null byte code and added DateTimeKind test. - Fixed namespace typos and improved code style and documentation.
This commit is contained in:
parent
b9e83e2ef8
commit
2147d981db
|
|
@ -1,7 +1,7 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
namespace AyCode.Core.Tests.serialization;
|
||||
|
||||
[TestClass]
|
||||
public class AcBinarySerializerTests
|
||||
|
|
@ -13,7 +13,7 @@ public class AcBinarySerializerTests
|
|||
{
|
||||
var result = AcBinarySerializer.Serialize<object?>(null);
|
||||
Assert.AreEqual(1, result.Length);
|
||||
Assert.AreEqual((byte)32, result[0]); // BinaryTypeCode.Null = 32
|
||||
Assert.AreEqual((byte)0, result[0]); // BinaryTypeCode.Null = 0
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
@ -70,6 +70,20 @@ public class AcBinarySerializerTests
|
|||
Assert.AreEqual(value, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(DateTimeKind.Unspecified)]
|
||||
[DataRow(DateTimeKind.Utc)]
|
||||
[DataRow(DateTimeKind.Local)]
|
||||
public void Serialize_DateTime_PreservesKind(DateTimeKind kind)
|
||||
{
|
||||
var value = new DateTime(2024, 12, 25, 10, 30, 45, kind);
|
||||
var binary = AcBinarySerializer.Serialize(value);
|
||||
var result = AcBinaryDeserializer.Deserialize<DateTime>(binary);
|
||||
|
||||
Assert.AreEqual(value.Ticks, result.Ticks);
|
||||
Assert.AreEqual(value.Kind, result.Kind);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialize_Guid_RoundTrip()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,1388 @@
|
|||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
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
|
||||
/// </summary>
|
||||
public static class AcBinaryDeserializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
||||
|
||||
#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
|
||||
|
||||
private static object? ReadValue(ref BinaryDeserializationContext context, Type targetType, int depth)
|
||||
{
|
||||
if (context.IsAtEnd) return null;
|
||||
|
||||
var typeCode = context.ReadByte();
|
||||
|
||||
// Handle null
|
||||
if (typeCode == BinaryTypeCode.Null) return null;
|
||||
|
||||
// Handle tiny int
|
||||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||||
{
|
||||
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||||
return ConvertToTargetType(intValue, targetType);
|
||||
}
|
||||
|
||||
// Handle type-specific codes
|
||||
switch (typeCode)
|
||||
{
|
||||
case BinaryTypeCode.True:
|
||||
return true;
|
||||
case BinaryTypeCode.False:
|
||||
return false;
|
||||
case BinaryTypeCode.Int8:
|
||||
return (sbyte)context.ReadByte();
|
||||
case BinaryTypeCode.UInt8:
|
||||
return context.ReadByte();
|
||||
case BinaryTypeCode.Int16:
|
||||
return context.ReadInt16();
|
||||
case BinaryTypeCode.UInt16:
|
||||
return context.ReadUInt16();
|
||||
case BinaryTypeCode.Int32:
|
||||
return ReadInt32Value(ref context, targetType);
|
||||
case BinaryTypeCode.UInt32:
|
||||
return context.ReadVarUInt();
|
||||
case BinaryTypeCode.Int64:
|
||||
return context.ReadVarLong();
|
||||
case BinaryTypeCode.UInt64:
|
||||
return context.ReadVarULong();
|
||||
case BinaryTypeCode.Float32:
|
||||
return context.ReadSingle();
|
||||
case BinaryTypeCode.Float64:
|
||||
return context.ReadDouble();
|
||||
case BinaryTypeCode.Decimal:
|
||||
return context.ReadDecimal();
|
||||
case BinaryTypeCode.Char:
|
||||
return context.ReadChar();
|
||||
case BinaryTypeCode.String:
|
||||
return ReadAndInternString(ref context);
|
||||
case BinaryTypeCode.StringInterned:
|
||||
return context.GetInternedString((int)context.ReadVarUInt());
|
||||
case BinaryTypeCode.StringEmpty:
|
||||
return string.Empty;
|
||||
case BinaryTypeCode.DateTime:
|
||||
return context.ReadDateTime();
|
||||
case BinaryTypeCode.DateTimeOffset:
|
||||
return context.ReadDateTimeOffset();
|
||||
case BinaryTypeCode.TimeSpan:
|
||||
return context.ReadTimeSpan();
|
||||
case BinaryTypeCode.Guid:
|
||||
return context.ReadGuid();
|
||||
case BinaryTypeCode.Enum:
|
||||
return ReadEnumValue(ref context, targetType);
|
||||
case BinaryTypeCode.Object:
|
||||
return ReadObject(ref context, targetType, depth);
|
||||
case BinaryTypeCode.ObjectRef:
|
||||
var refId = context.ReadVarInt();
|
||||
return context.GetReferencedObject(refId);
|
||||
case BinaryTypeCode.Array:
|
||||
return ReadArray(ref context, targetType, depth);
|
||||
case BinaryTypeCode.Dictionary:
|
||||
return ReadDictionary(ref context, targetType, depth);
|
||||
case BinaryTypeCode.ByteArray:
|
||||
return ReadByteArray(ref context);
|
||||
default:
|
||||
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.ReadString(length);
|
||||
// Register for future StringInterned references
|
||||
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 string ReadStringValue(ref BinaryDeserializationContext context)
|
||||
{
|
||||
var length = (int)context.ReadVarUInt();
|
||||
if (length == 0) return string.Empty;
|
||||
return context.ReadString(length);
|
||||
}
|
||||
|
||||
[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 = ReadStringValue(ref context);
|
||||
}
|
||||
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 = ReadStringValue(ref context);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
foreach (var prop in metadata.PropertiesArray)
|
||||
{
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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:
|
||||
var strLen = (int)context.ReadVarUInt();
|
||||
context.Skip(strLen);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (context.HasMetadata)
|
||||
{
|
||||
context.ReadVarUInt();
|
||||
}
|
||||
else
|
||||
{
|
||||
var nameCode = context.ReadByte();
|
||||
if (nameCode == BinaryTypeCode.String)
|
||||
{
|
||||
var len = (int)context.ReadVarUInt();
|
||||
context.Skip(len);
|
||||
}
|
||||
}
|
||||
// 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
|
||||
|
||||
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; }
|
||||
|
||||
// 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
|
||||
_propertyNames = null;
|
||||
_internedStrings = null;
|
||||
_references = null;
|
||||
IsMergeMode = false;
|
||||
}
|
||||
|
||||
public void ReadHeader()
|
||||
{
|
||||
if (_data.Length < 2) return;
|
||||
|
||||
FormatVersion = ReadByte();
|
||||
var flags = ReadByte();
|
||||
|
||||
HasMetadata = flags == BinaryTypeCode.MetadataHeader;
|
||||
|
||||
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] = ReadString(len);
|
||||
}
|
||||
}
|
||||
// Note: Interned strings are built dynamically during deserialization
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public short ReadInt16()
|
||||
{
|
||||
if (_position + 2 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var result = BitConverter.ToInt16(_data.Slice(_position, 2));
|
||||
_position += 2;
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ushort ReadUInt16()
|
||||
{
|
||||
if (_position + 2 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var result = BitConverter.ToUInt16(_data.Slice(_position, 2));
|
||||
_position += 2;
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ReadSingle()
|
||||
{
|
||||
if (_position + 4 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var result = BitConverter.ToSingle(_data.Slice(_position, 4));
|
||||
_position += 4;
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double ReadDouble()
|
||||
{
|
||||
if (_position + 8 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var result = BitConverter.ToDouble(_data.Slice(_position, 8));
|
||||
_position += 8;
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public decimal ReadDecimal()
|
||||
{
|
||||
if (_position + 16 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var bits = new int[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
bits[i] = BitConverter.ToInt32(_data.Slice(_position + i * 4, 4));
|
||||
}
|
||||
_position += 16;
|
||||
return new decimal(bits);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public char ReadChar()
|
||||
{
|
||||
if (_position + 2 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var result = BitConverter.ToChar(_data.Slice(_position, 2));
|
||||
_position += 2;
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTime ReadDateTime()
|
||||
{
|
||||
if (_position + 9 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var ticks = BitConverter.ToInt64(_data.Slice(_position, 8));
|
||||
var kind = (DateTimeKind)_data[_position + 8];
|
||||
_position += 9;
|
||||
return new DateTime(ticks, kind);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTimeOffset ReadDateTimeOffset()
|
||||
{
|
||||
if (_position + 10 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var utcTicks = BitConverter.ToInt64(_data.Slice(_position, 8));
|
||||
var offsetMinutes = BitConverter.ToInt16(_data.Slice(_position + 8, 2));
|
||||
_position += 10;
|
||||
var offset = TimeSpan.FromMinutes(offsetMinutes);
|
||||
// We stored UtcTicks, so we need to add offset to get local ticks
|
||||
var localTicks = utcTicks + offset.Ticks;
|
||||
return new DateTimeOffset(localTicks, offset);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TimeSpan ReadTimeSpan()
|
||||
{
|
||||
if (_position + 8 > _data.Length)
|
||||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||||
var ticks = BitConverter.ToInt64(_data.Slice(_position, 8));
|
||||
_position += 8;
|
||||
return new TimeSpan(ticks);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Guid ReadGuid()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadString(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
|
||||
}
|
||||
|
|
@ -1,6 +1,1094 @@
|
|||
namespace AyCode.Core.Extensions;
|
||||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using static AyCode.Core.Extensions.JsonUtilities;
|
||||
|
||||
public class AcBinarySerializer
|
||||
namespace AyCode.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// High-performance binary serializer optimized for speed and memory efficiency.
|
||||
/// Features:
|
||||
/// - VarInt encoding for compact integers (MessagePack-style)
|
||||
/// - String interning for repeated strings
|
||||
/// - Property name table for fast lookup
|
||||
/// - Reference handling for circular/shared references
|
||||
/// - Optional metadata for schema evolution
|
||||
/// </summary>
|
||||
public static class AcBinarySerializer
|
||||
{
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> TypeMetadataCache = new();
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to binary with default options.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] Serialize<T>(T value) => Serialize(value, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to binary with specified options.
|
||||
/// </summary>
|
||||
public static byte[] Serialize<T>(T value, AcBinarySerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return [BinaryTypeCode.Null];
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
|
||||
// Use context-based serialization for all types to ensure consistent format
|
||||
var context = BinarySerializationContextPool.Get(options);
|
||||
try
|
||||
{
|
||||
// Reserve space for header
|
||||
context.WriteHeaderPlaceholder();
|
||||
|
||||
// Phase 1: If reference handling enabled, scan for multi-referenced objects
|
||||
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
ScanReferences(value, context, 0);
|
||||
}
|
||||
|
||||
// Phase 2: If metadata enabled, collect property names
|
||||
if (options.UseMetadata && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
CollectPropertyNames(value, context, 0);
|
||||
}
|
||||
|
||||
// Write metadata section
|
||||
context.WriteMetadata();
|
||||
|
||||
// Phase 3: Write the actual data
|
||||
WriteValue(value, type, context, 0);
|
||||
|
||||
// Finalize and return
|
||||
return context.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
BinarySerializationContextPool.Return(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize to existing buffer writer (for streaming scenarios).
|
||||
/// </summary>
|
||||
public static void Serialize<T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
||||
{
|
||||
var bytes = Serialize(value, options);
|
||||
writer.Write(bytes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reference Scanning
|
||||
|
||||
private static void ScanReferences(object? value, BinarySerializationContext context, int depth)
|
||||
{
|
||||
if (value == null || depth > context.MaxDepth) return;
|
||||
|
||||
var type = value.GetType();
|
||||
if (IsPrimitiveOrStringFast(type)) return;
|
||||
if (!context.TrackForScanning(value)) return;
|
||||
|
||||
if (value is byte[]) return; // byte arrays are value types
|
||||
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
if (entry.Value != null)
|
||||
ScanReferences(entry.Value, context, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (item != null)
|
||||
ScanReferences(item, context, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue != null)
|
||||
ScanReferences(propValue, context, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Name Collection
|
||||
|
||||
private static void CollectPropertyNames(object? value, BinarySerializationContext context, int depth)
|
||||
{
|
||||
if (value == null || depth > context.MaxDepth) return;
|
||||
|
||||
var type = value.GetType();
|
||||
if (IsPrimitiveOrStringFast(type)) return;
|
||||
|
||||
if (value is byte[]) return;
|
||||
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
if (entry.Value != null)
|
||||
CollectPropertyNames(entry.Value, context, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (item != null)
|
||||
CollectPropertyNames(item, context, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
context.RegisterPropertyName(prop.Name);
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue != null)
|
||||
CollectPropertyNames(propValue, context, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value Writing
|
||||
|
||||
private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try writing as primitive first
|
||||
if (TryWritePrimitive(value, type, context))
|
||||
return;
|
||||
|
||||
if (depth > context.MaxDepth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for object reference
|
||||
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarInt(refId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle byte arrays specially
|
||||
if (value is byte[] byteArray)
|
||||
{
|
||||
WriteByteArray(byteArray, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle dictionaries
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
WriteDictionary(dictionary, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle collections/arrays
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
WriteArray(enumerable, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle complex objects
|
||||
WriteObject(value, type, context, depth);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
var typeCode = Type.GetTypeCode(underlyingType);
|
||||
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Int32:
|
||||
WriteInt32((int)value, context);
|
||||
return true;
|
||||
case TypeCode.Int64:
|
||||
WriteInt64((long)value, context);
|
||||
return true;
|
||||
case TypeCode.Boolean:
|
||||
context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False);
|
||||
return true;
|
||||
case TypeCode.Double:
|
||||
WriteFloat64((double)value, context);
|
||||
return true;
|
||||
case TypeCode.String:
|
||||
WriteString((string)value, context);
|
||||
return true;
|
||||
case TypeCode.Single:
|
||||
WriteFloat32((float)value, context);
|
||||
return true;
|
||||
case TypeCode.Decimal:
|
||||
WriteDecimal((decimal)value, context);
|
||||
return true;
|
||||
case TypeCode.DateTime:
|
||||
WriteDateTime((DateTime)value, context);
|
||||
return true;
|
||||
case TypeCode.Byte:
|
||||
context.WriteByte(BinaryTypeCode.UInt8);
|
||||
context.WriteByte((byte)value);
|
||||
return true;
|
||||
case TypeCode.Int16:
|
||||
WriteInt16((short)value, context);
|
||||
return true;
|
||||
case TypeCode.UInt16:
|
||||
WriteUInt16((ushort)value, context);
|
||||
return true;
|
||||
case TypeCode.UInt32:
|
||||
WriteUInt32((uint)value, context);
|
||||
return true;
|
||||
case TypeCode.UInt64:
|
||||
WriteUInt64((ulong)value, context);
|
||||
return true;
|
||||
case TypeCode.SByte:
|
||||
context.WriteByte(BinaryTypeCode.Int8);
|
||||
context.WriteByte(unchecked((byte)(sbyte)value));
|
||||
return true;
|
||||
case TypeCode.Char:
|
||||
WriteChar((char)value, context);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(underlyingType, GuidType))
|
||||
{
|
||||
WriteGuid((Guid)value, context);
|
||||
return true;
|
||||
}
|
||||
if (ReferenceEquals(underlyingType, DateTimeOffsetType))
|
||||
{
|
||||
WriteDateTimeOffset((DateTimeOffset)value, context);
|
||||
return true;
|
||||
}
|
||||
if (ReferenceEquals(underlyingType, TimeSpanType))
|
||||
{
|
||||
WriteTimeSpan((TimeSpan)value, context);
|
||||
return true;
|
||||
}
|
||||
if (underlyingType.IsEnum)
|
||||
{
|
||||
WriteEnum(value, context);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Primitive Writers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteInt32(int value, BinarySerializationContext context)
|
||||
{
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
context.WriteByte(tiny);
|
||||
return;
|
||||
}
|
||||
context.WriteByte(BinaryTypeCode.Int32);
|
||||
context.WriteVarInt(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteInt64(long value, BinarySerializationContext context)
|
||||
{
|
||||
if (value >= int.MinValue && value <= int.MaxValue)
|
||||
{
|
||||
WriteInt32((int)value, context);
|
||||
return;
|
||||
}
|
||||
context.WriteByte(BinaryTypeCode.Int64);
|
||||
context.WriteVarLong(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteFloat64(double value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Float64);
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
BitConverter.TryWriteBytes(buffer, value);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteFloat32(float value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Float32);
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
BitConverter.TryWriteBytes(buffer, value);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteDecimal(decimal value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Decimal);
|
||||
var bits = decimal.GetBits(value);
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
BitConverter.TryWriteBytes(buffer[(i * 4)..], bits[i]);
|
||||
}
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteDateTime(DateTime value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.DateTime);
|
||||
Span<byte> buffer = stackalloc byte[9];
|
||||
BitConverter.TryWriteBytes(buffer, value.Ticks);
|
||||
buffer[8] = (byte)value.Kind;
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteGuid(Guid value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Guid);
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
value.TryWriteBytes(buffer);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteDateTimeOffset(DateTimeOffset value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.DateTimeOffset);
|
||||
Span<byte> buffer = stackalloc byte[10];
|
||||
BitConverter.TryWriteBytes(buffer, value.UtcTicks);
|
||||
BitConverter.TryWriteBytes(buffer[8..], (short)value.Offset.TotalMinutes);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteTimeSpan(TimeSpan value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.TimeSpan);
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
BitConverter.TryWriteBytes(buffer, value.Ticks);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteInt16(short value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Int16);
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
BitConverter.TryWriteBytes(buffer, value);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteUInt16(ushort value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.UInt16);
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
BitConverter.TryWriteBytes(buffer, value);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteUInt32(uint value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.UInt32);
|
||||
context.WriteVarUInt(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteUInt64(ulong value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.UInt64);
|
||||
context.WriteVarULong(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteChar(char value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Char);
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
BitConverter.TryWriteBytes(buffer, value);
|
||||
context.WriteBytes(buffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteEnum(object value, BinarySerializationContext context)
|
||||
{
|
||||
var intValue = Convert.ToInt32(value);
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Enum);
|
||||
context.WriteByte(tiny);
|
||||
return;
|
||||
}
|
||||
context.WriteByte(BinaryTypeCode.Enum);
|
||||
context.WriteByte(BinaryTypeCode.Int32);
|
||||
context.WriteVarInt(intValue);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteString(string value, BinarySerializationContext context)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.StringEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try string interning - but only write ref if already interned
|
||||
if (context.UseStringInterning && value.Length >= context.MinStringInternLength)
|
||||
{
|
||||
if (context.TryGetInternedStringIndex(value, out var index))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.StringInterned);
|
||||
context.WriteVarUInt((uint)index);
|
||||
return;
|
||||
}
|
||||
// Register for future references
|
||||
context.RegisterInternedString(value);
|
||||
}
|
||||
|
||||
// Write inline string
|
||||
context.WriteByte(BinaryTypeCode.String);
|
||||
var utf8Length = Encoding.UTF8.GetByteCount(value);
|
||||
context.WriteVarUInt((uint)utf8Length);
|
||||
context.WriteString(value, utf8Length);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteByteArray(byte[] value, BinarySerializationContext context)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ByteArray);
|
||||
context.WriteVarUInt((uint)value.Length);
|
||||
context.WriteBytes(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Type Writers
|
||||
|
||||
private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
|
||||
// Register object reference if needed
|
||||
if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
|
||||
{
|
||||
context.WriteVarInt(refId);
|
||||
context.MarkAsWritten(value, refId);
|
||||
}
|
||||
else if (context.UseReferenceHandling)
|
||||
{
|
||||
context.WriteVarInt(-1); // No ref ID
|
||||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
var nextDepth = depth + 1;
|
||||
var writtenCount = 0;
|
||||
|
||||
// Count non-null, non-default properties first
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue == null) continue;
|
||||
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
|
||||
writtenCount++;
|
||||
}
|
||||
|
||||
context.WriteVarUInt((uint)writtenCount);
|
||||
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue == null) continue;
|
||||
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
|
||||
|
||||
// Write property index or name
|
||||
if (context.UseMetadata)
|
||||
{
|
||||
var propIndex = context.GetPropertyNameIndex(prop.Name);
|
||||
context.WriteVarUInt((uint)propIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteString(prop.Name, context);
|
||||
}
|
||||
|
||||
WriteValue(propValue, prop.PropertyType, context, nextDepth);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteArray(IEnumerable enumerable, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Array);
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
// For IList, we can write the count directly
|
||||
if (enumerable is IList list)
|
||||
{
|
||||
context.WriteVarUInt((uint)list.Count);
|
||||
foreach (var item in list)
|
||||
{
|
||||
var itemType = item?.GetType() ?? typeof(object);
|
||||
WriteValue(item, itemType, context, nextDepth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For other IEnumerable, collect first
|
||||
var items = new List<object?>();
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
context.WriteVarUInt((uint)items.Count);
|
||||
foreach (var item in items)
|
||||
{
|
||||
var itemType = item?.GetType() ?? typeof(object);
|
||||
WriteValue(item, itemType, context, nextDepth);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Dictionary);
|
||||
context.WriteVarUInt((uint)dictionary.Count);
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
// Write key
|
||||
var keyType = entry.Key?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Key, keyType, context, nextDepth);
|
||||
|
||||
// Write value
|
||||
var valueType = entry.Value?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Value, valueType, context, nextDepth);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region VarInt Encoding (Static Methods for Direct Use)
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length signed integer (ZigZag encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarInt(Span<byte> buffer, int value)
|
||||
{
|
||||
// ZigZag encoding
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
return WriteVarUInt(buffer, encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length unsigned integer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarUInt(Span<byte> buffer, uint value)
|
||||
{
|
||||
var i = 0;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
buffer[i++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
buffer[i++] = (byte)value;
|
||||
return i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length signed long (ZigZag encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarLong(Span<byte> buffer, long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
return WriteVarULong(buffer, encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write variable-length unsigned long.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int WriteVarULong(Span<byte> buffer, ulong value)
|
||||
{
|
||||
var i = 0;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
buffer[i++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
buffer[i++] = (byte)value;
|
||||
return i;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Metadata
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static BinaryTypeMetadata GetTypeMetadata(Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDefaultValueFast(object value, TypeCode typeCode, Type propertyType)
|
||||
{
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Int32: return (int)value == 0;
|
||||
case TypeCode.Int64: return (long)value == 0L;
|
||||
case TypeCode.Double: return (double)value == 0.0;
|
||||
case TypeCode.Decimal: return (decimal)value == 0m;
|
||||
case TypeCode.Single: return (float)value == 0f;
|
||||
case TypeCode.Byte: return (byte)value == 0;
|
||||
case TypeCode.Int16: return (short)value == 0;
|
||||
case TypeCode.UInt16: return (ushort)value == 0;
|
||||
case TypeCode.UInt32: return (uint)value == 0;
|
||||
case TypeCode.UInt64: return (ulong)value == 0;
|
||||
case TypeCode.SByte: return (sbyte)value == 0;
|
||||
case TypeCode.Boolean: return (bool)value == false;
|
||||
case TypeCode.String: return string.IsNullOrEmpty((string)value);
|
||||
}
|
||||
|
||||
if (propertyType.IsEnum) return Convert.ToInt32(value) == 0;
|
||||
if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal sealed class BinaryTypeMetadata
|
||||
{
|
||||
public BinaryPropertyAccessor[] Properties { get; }
|
||||
|
||||
public BinaryTypeMetadata(Type type)
|
||||
{
|
||||
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead &&
|
||||
p.GetIndexParameters().Length == 0 &&
|
||||
!HasJsonIgnoreAttribute(p))
|
||||
.Select(p => new BinaryPropertyAccessor(p))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class BinaryPropertyAccessor
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly byte[] NameUtf8;
|
||||
public readonly Type PropertyType;
|
||||
public readonly TypeCode TypeCode;
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
||||
public BinaryPropertyAccessor(PropertyInfo prop)
|
||||
{
|
||||
Name = prop.Name;
|
||||
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
|
||||
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
TypeCode = Type.GetTypeCode(PropertyType);
|
||||
_getter = CreateCompiledGetter(prop.DeclaringType!, prop);
|
||||
}
|
||||
|
||||
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 object? GetValue(object obj) => _getter(obj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context Pool
|
||||
|
||||
private static class BinarySerializationContextPool
|
||||
{
|
||||
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
|
||||
private const int MaxPoolSize = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
|
||||
{
|
||||
if (Pool.TryDequeue(out var context))
|
||||
{
|
||||
context.Reset(options);
|
||||
return context;
|
||||
}
|
||||
return new BinarySerializationContext(options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(BinarySerializationContext context)
|
||||
{
|
||||
if (Pool.Count < MaxPoolSize)
|
||||
{
|
||||
context.Clear();
|
||||
Pool.Enqueue(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Context
|
||||
|
||||
internal sealed class BinarySerializationContext : IDisposable
|
||||
{
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
|
||||
// Reference handling
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
private Dictionary<object, int>? _writtenRefs;
|
||||
private HashSet<object>? _multiReferenced;
|
||||
private int _nextRefId;
|
||||
|
||||
// String interning
|
||||
private Dictionary<string, int>? _internedStrings;
|
||||
private List<string>? _internedStringList;
|
||||
|
||||
// Property name table
|
||||
private Dictionary<string, int>? _propertyNames;
|
||||
private List<string>? _propertyNameList;
|
||||
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public bool UseStringInterning { get; private set; }
|
||||
public bool UseMetadata { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
public byte MinStringInternLength { get; private set; }
|
||||
|
||||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||
{
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(options.InitialBufferCapacity);
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
public void Reset(AcBinarySerializerOptions options)
|
||||
{
|
||||
_position = 0;
|
||||
_nextRefId = 1;
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
UseStringInterning = options.UseStringInterning;
|
||||
UseMetadata = options.UseMetadata;
|
||||
MaxDepth = options.MaxDepth;
|
||||
MinStringInternLength = options.MinStringInternLength;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_position = 0;
|
||||
_nextRefId = 1;
|
||||
_scanOccurrences?.Clear();
|
||||
_writtenRefs?.Clear();
|
||||
_multiReferenced?.Clear();
|
||||
_internedStrings?.Clear();
|
||||
_internedStringList?.Clear();
|
||||
_propertyNames?.Clear();
|
||||
_propertyNameList?.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = null!;
|
||||
}
|
||||
}
|
||||
|
||||
#region Buffer Writing
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void EnsureCapacity(int additionalBytes)
|
||||
{
|
||||
var required = _position + additionalBytes;
|
||||
if (required <= _buffer.Length) return;
|
||||
|
||||
var newSize = Math.Max(_buffer.Length * 2, required);
|
||||
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
|
||||
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = newBuffer;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteByte(byte value)
|
||||
{
|
||||
EnsureCapacity(1);
|
||||
_buffer[_position++] = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
EnsureCapacity(data.Length);
|
||||
data.CopyTo(_buffer.AsSpan(_position));
|
||||
_position += data.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarInt(int value)
|
||||
{
|
||||
EnsureCapacity(5);
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUIntInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarUInt(uint value)
|
||||
{
|
||||
EnsureCapacity(5);
|
||||
WriteVarUIntInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarUIntInternal(uint value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarLong(long value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULongInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarULong(ulong value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
WriteVarULongInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarULongInternal(ulong value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteString(string value, int utf8Length)
|
||||
{
|
||||
EnsureCapacity(utf8Length);
|
||||
Encoding.UTF8.GetBytes(value, _buffer.AsSpan(_position, utf8Length));
|
||||
_position += utf8Length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header and Metadata
|
||||
|
||||
private int _headerPosition;
|
||||
|
||||
public void WriteHeaderPlaceholder()
|
||||
{
|
||||
// Reserve space for: version (1) + flags (1)
|
||||
EnsureCapacity(2);
|
||||
_headerPosition = _position;
|
||||
_position += 2;
|
||||
}
|
||||
|
||||
public void WriteMetadata()
|
||||
{
|
||||
// Write version at header position
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
|
||||
var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0;
|
||||
|
||||
if (!UseMetadata || !hasPropertyNames)
|
||||
{
|
||||
_buffer[_headerPosition + 1] = BinaryTypeCode.NoMetadataHeader;
|
||||
return;
|
||||
}
|
||||
|
||||
_buffer[_headerPosition + 1] = BinaryTypeCode.MetadataHeader;
|
||||
|
||||
// Write property name count
|
||||
WriteVarUInt((uint)_propertyNameList!.Count);
|
||||
|
||||
// Write property names
|
||||
foreach (var name in _propertyNameList)
|
||||
{
|
||||
var utf8Length = Encoding.UTF8.GetByteCount(name);
|
||||
WriteVarUInt((uint)utf8Length);
|
||||
WriteString(name, utf8Length);
|
||||
}
|
||||
|
||||
// NOTE: Interned strings are handled "on-the-fly" during serialization
|
||||
// First occurrence is written inline, subsequent occurrences use StringInterned reference
|
||||
// We don't need to write them in metadata since deserializer builds table dynamically
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reference Handling
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
|
||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
count++;
|
||||
_multiReferenced.Add(obj);
|
||||
return false;
|
||||
}
|
||||
count = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteRef(object obj, out int refId)
|
||||
{
|
||||
if (_multiReferenced != null && _multiReferenced.Contains(obj))
|
||||
{
|
||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||
if (!_writtenRefs.ContainsKey(obj))
|
||||
{
|
||||
refId = _nextRefId++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, int refId)
|
||||
{
|
||||
_writtenRefs![obj] = refId;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out int refId)
|
||||
{
|
||||
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
|
||||
return true;
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Interning
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetInternedStringIndex(string value, out int index)
|
||||
{
|
||||
if (_internedStrings != null && _internedStrings.TryGetValue(value, out index))
|
||||
return true;
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterInternedString(string value)
|
||||
{
|
||||
_internedStrings ??= new Dictionary<string, int>(32, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(32);
|
||||
|
||||
if (!_internedStrings.ContainsKey(value))
|
||||
{
|
||||
var index = _internedStringList.Count;
|
||||
_internedStrings[value] = index;
|
||||
_internedStringList.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Names
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterPropertyName(string name)
|
||||
{
|
||||
_propertyNames ??= new Dictionary<string, int>(64, StringComparer.Ordinal);
|
||||
_propertyNameList ??= new List<string>(64);
|
||||
|
||||
if (!_propertyNames.ContainsKey(name))
|
||||
{
|
||||
_propertyNames[name] = _propertyNameList.Count;
|
||||
_propertyNameList.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetPropertyNameIndex(string name)
|
||||
{
|
||||
return _propertyNames!.TryGetValue(name, out var index) ? index : -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
var result = new byte[_position];
|
||||
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Options for AcBinarySerializer and AcBinaryDeserializer.
|
||||
/// Optimized for speed and memory efficiency over raw size.
|
||||
/// </summary>
|
||||
public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||
{
|
||||
public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Binary;
|
||||
|
||||
/// <summary>
|
||||
/// Current binary format version. Incremented when breaking changes are made.
|
||||
/// </summary>
|
||||
public const byte FormatVersion = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Default options instance with metadata and string interning enabled.
|
||||
/// </summary>
|
||||
public static readonly AcBinarySerializerOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options optimized for maximum speed (no metadata, no interning).
|
||||
/// Use when deserializer knows the exact type structure.
|
||||
/// </summary>
|
||||
public static readonly AcBinarySerializerOptions FastMode = new()
|
||||
{
|
||||
UseMetadata = false,
|
||||
UseStringInterning = false,
|
||||
UseReferenceHandling = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options for shallow serialization (root level only).
|
||||
/// </summary>
|
||||
public static readonly AcBinarySerializerOptions ShallowCopy = new()
|
||||
{
|
||||
MaxDepth = 0,
|
||||
UseReferenceHandling = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include metadata header with property names.
|
||||
/// When enabled, property names are stored once and referenced by index.
|
||||
/// Improves deserialization speed and allows schema evolution.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseMetadata { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to intern repeated strings.
|
||||
/// When enabled, duplicate strings are stored once and referenced by index.
|
||||
/// Reduces size and memory for objects with many repeated string values.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseStringInterning { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum string length to consider for interning.
|
||||
/// Shorter strings are written inline to avoid overhead.
|
||||
/// Default: 4 (strings shorter than 4 chars are not interned)
|
||||
/// </summary>
|
||||
public byte MinStringInternLength { get; init; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Initial capacity for serialization buffer.
|
||||
/// Default: 4096 bytes
|
||||
/// </summary>
|
||||
public int InitialBufferCapacity { get; init; } = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with specified max depth.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AcBinarySerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without reference handling.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without metadata (faster but less flexible).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AcBinarySerializerOptions WithoutMetadata() => new() { UseMetadata = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary type codes for serialization.
|
||||
/// Designed for fast switch dispatch and compact storage.
|
||||
/// Lower 5 bits = type code (0-31)
|
||||
/// Upper 3 bits = flags (interned, reference, has-type-info)
|
||||
/// </summary>
|
||||
internal static class BinaryTypeCode
|
||||
{
|
||||
// Primitive types (0-15)
|
||||
public const byte Null = 0;
|
||||
public const byte True = 1;
|
||||
public const byte False = 2;
|
||||
public const byte Int8 = 3;
|
||||
public const byte UInt8 = 4;
|
||||
public const byte Int16 = 5;
|
||||
public const byte UInt16 = 6;
|
||||
public const byte Int32 = 7;
|
||||
public const byte UInt32 = 8;
|
||||
public const byte Int64 = 9;
|
||||
public const byte UInt64 = 10;
|
||||
public const byte Float32 = 11;
|
||||
public const byte Float64 = 12;
|
||||
public const byte Decimal = 13;
|
||||
public const byte Char = 14;
|
||||
|
||||
// String types (16-19)
|
||||
public const byte String = 16; // Inline UTF8 string
|
||||
public const byte StringInterned = 17; // Reference to interned string by index
|
||||
public const byte StringEmpty = 18; // Empty string marker
|
||||
|
||||
// Date/Time types (20-23)
|
||||
public const byte DateTime = 20;
|
||||
public const byte DateTimeOffset = 21;
|
||||
public const byte TimeSpan = 22;
|
||||
public const byte Guid = 23;
|
||||
|
||||
// Enum (24)
|
||||
public const byte Enum = 24;
|
||||
|
||||
// Complex types (25-31)
|
||||
public const byte Object = 25; // Start of object
|
||||
public const byte ObjectEnd = 26; // End of object marker
|
||||
public const byte ObjectRef = 27; // Reference to previously serialized object
|
||||
public const byte Array = 28; // Start of array/list
|
||||
public const byte Dictionary = 29; // Start of dictionary
|
||||
public const byte ByteArray = 30; // Optimized byte[] storage
|
||||
|
||||
// Special markers (32+, for header/meta)
|
||||
public const byte MetadataHeader = 32; // Binary has metadata section
|
||||
public const byte NoMetadataHeader = 33; // Binary has no metadata
|
||||
|
||||
// Compact integer variants (for VarInt optimization)
|
||||
public const byte Int32Tiny = 64; // -16 to 111 stored in single byte (value = code - 64 - 16)
|
||||
public const byte Int32TinyMax = 191; // Upper bound for tiny int
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code represents a reference (string or object).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsReference(byte code) => code == StringInterned || code == ObjectRef;
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code is a tiny int (single byte int32 encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsTinyInt(byte code) => code >= Int32Tiny && code <= Int32TinyMax;
|
||||
|
||||
/// <summary>
|
||||
/// Decode tiny int value from type code.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
|
||||
|
||||
/// <summary>
|
||||
/// Encode small int value (-16 to 111) as type code.
|
||||
/// Returns true if value fits in tiny encoding.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryEncodeTinyInt(int value, out byte code)
|
||||
{
|
||||
if (value >= -16 && value <= 111)
|
||||
{
|
||||
code = (byte)(value + 16 + Int32Tiny);
|
||||
return true;
|
||||
}
|
||||
code = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
namespace AyCode.Core.Extensions;
|
||||
|
||||
public enum AcSerializerType : byte
|
||||
{
|
||||
Json = 0,
|
||||
Binary = 1,
|
||||
}
|
||||
|
||||
public abstract class AcSerializerOptions
|
||||
{
|
||||
public abstract AcSerializerType SerializerType { get; init; }
|
||||
/// <summary>
|
||||
/// Whether to use $id/$ref reference handling for circular references.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseReferenceHandling { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum depth for serialization/deserialization.
|
||||
/// 0 = root level only (primitives of root object)
|
||||
/// 1 = root + first level of nested objects/collections
|
||||
/// byte.MaxValue (255) = effectively unlimited
|
||||
/// Default: byte.MaxValue
|
||||
/// </summary>
|
||||
public byte MaxDepth { get; init; } = byte.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for AcJsonSerializer and AcJsonDeserializer.
|
||||
/// </summary>
|
||||
public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
||||
{
|
||||
public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Default options instance with reference handling enabled and max depth.
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for shallow serialization (root level only, no references).
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with specified max depth.
|
||||
/// </summary>
|
||||
public static AcJsonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without reference handling.
|
||||
/// </summary>
|
||||
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
}
|
||||
|
|
@ -11,47 +11,6 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace AyCode.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Options for AcJsonSerializer and AcJsonDeserializer.
|
||||
/// </summary>
|
||||
public sealed class AcJsonSerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options instance with reference handling enabled and max depth.
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for shallow serialization (root level only, no references).
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use $id/$ref reference handling for circular references.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseReferenceHandling { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum depth for serialization/deserialization.
|
||||
/// 0 = root level only (primitives of root object)
|
||||
/// 1 = root + first level of nested objects/collections
|
||||
/// byte.MaxValue (255) = effectively unlimited
|
||||
/// Default: byte.MaxValue
|
||||
/// </summary>
|
||||
public byte MaxDepth { get; init; } = byte.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with specified max depth.
|
||||
/// </summary>
|
||||
public static AcJsonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without reference handling.
|
||||
/// </summary>
|
||||
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cached result for IId type info lookup.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -340,8 +340,7 @@ public static class SerializeObjectExtensions
|
|||
/// <summary>
|
||||
/// Serialize object to JSON string with default options.
|
||||
/// </summary>
|
||||
public static string ToJson<T>(this T source)
|
||||
=> AcJsonSerializer.Serialize(source);
|
||||
public static string ToJson<T>(this T source) => AcJsonSerializer.Serialize(source);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to JSON string with specified options.
|
||||
|
|
@ -445,14 +444,83 @@ public static class SerializeObjectExtensions
|
|||
public static T MessagePackTo<T>(this byte[] message, MessagePackSerializerOptions options)
|
||||
=> MessagePackSerializer.Deserialize<T>(message, options);
|
||||
|
||||
|
||||
public static object ToAny<T>(this T source, AcSerializerOptions options)
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json) return ToJson(source, (AcJsonSerializerOptions)options);
|
||||
return ToBinary(source, (AcBinarySerializerOptions)options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize data (JSON string or binary byte[]) to object based on options.
|
||||
/// </summary>
|
||||
public static T? AnyTo<T>(this object data, AcSerializerOptions options)
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
return ((string)data).JsonTo<T>((AcJsonSerializerOptions)options);
|
||||
return ((byte[])data).BinaryTo<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize data to specified type based on options.
|
||||
/// </summary>
|
||||
public static object? AnyTo(this object data, Type targetType, AcSerializerOptions options)
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
return ((string)data).JsonTo(targetType, (AcJsonSerializerOptions)options);
|
||||
return ((byte[])data).BinaryTo(targetType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object from data based on options.
|
||||
/// </summary>
|
||||
public static void AnyTo<T>(this object data, T target, AcSerializerOptions options) where T : class
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
((string)data).JsonTo(target, (AcJsonSerializerOptions)options);
|
||||
else
|
||||
((byte[])data).BinaryTo(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate existing object with merge semantics based on options.
|
||||
/// </summary>
|
||||
public static void AnyToMerge<T>(this object data, T target, AcSerializerOptions options) where T : class
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
((string)data).JsonTo(target, (AcJsonSerializerOptions)options); // JSON always merges
|
||||
else
|
||||
((byte[])data).BinaryToMerge(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone object via serialization based on options.
|
||||
/// </summary>
|
||||
public static T? CloneToAny<T>(this T source, AcSerializerOptions options) where T : class
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
return source.CloneTo<T>((AcJsonSerializerOptions)options);
|
||||
return source.BinaryCloneTo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy object properties to target via serialization based on options.
|
||||
/// </summary>
|
||||
public static void CopyToAny<T>(this T source, T target, AcSerializerOptions options) where T : class
|
||||
{
|
||||
if (options.SerializerType == AcSerializerType.Json)
|
||||
source.CopyTo(target, (AcJsonSerializerOptions)options);
|
||||
else
|
||||
source.BinaryCopyTo(target);
|
||||
}
|
||||
|
||||
#region Binary Serialization Extension Methods
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to binary byte array with default options.
|
||||
/// Significantly faster than JSON, especially for large data in WASM.
|
||||
/// </summary>
|
||||
public static byte[] ToBinary<T>(this T source)
|
||||
=> AcBinarySerializer.Serialize(source);
|
||||
public static byte[] ToBinary<T>(this T source) => AcBinarySerializer.Serialize(source);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to binary byte array with specified options.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
{
|
||||
protected readonly List<AcDynamicMethodCallModel<SignalRAttribute>> DynamicMethodCallModels = [];
|
||||
protected TLogger Logger = logger;
|
||||
protected IConfiguration Configuration = configuration;
|
||||
protected IConfiguration Configuration = configuration;
|
||||
|
||||
protected AcSerializerOptions SerializerOptions = new AcBinarySerializerOptions();
|
||||
|
||||
#region Connection Lifecycle
|
||||
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ namespace BenchmarkSuite1
|
|||
pointsPerMeasurement: 5);
|
||||
|
||||
var binaryWithRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
|
||||
var binaryNoRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.WithoutReferenceHandling);
|
||||
var binaryNoRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.WithoutReferenceHandling());
|
||||
var json = AcJsonSerializer.Serialize(order, AcJsonSerializerOptions.WithoutReferenceHandling());
|
||||
var jsonBytes = Encoding.UTF8.GetByteCount(json);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue