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:
Loretta 2025-12-12 21:03:39 +01:00
parent b9e83e2ef8
commit 2147d981db
9 changed files with 2804 additions and 52 deletions

View File

@ -1,7 +1,7 @@
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
namespace AyCode.Core.Tests.Serialization; namespace AyCode.Core.Tests.serialization;
[TestClass] [TestClass]
public class AcBinarySerializerTests public class AcBinarySerializerTests
@ -13,7 +13,7 @@ public class AcBinarySerializerTests
{ {
var result = AcBinarySerializer.Serialize<object?>(null); var result = AcBinarySerializer.Serialize<object?>(null);
Assert.AreEqual(1, result.Length); Assert.AreEqual(1, result.Length);
Assert.AreEqual((byte)32, result[0]); // BinaryTypeCode.Null = 32 Assert.AreEqual((byte)0, result[0]); // BinaryTypeCode.Null = 0
} }
[TestMethod] [TestMethod]
@ -70,6 +70,20 @@ public class AcBinarySerializerTests
Assert.AreEqual(value, result); 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] [TestMethod]
public void Serialize_Guid_RoundTrip() public void Serialize_Guid_RoundTrip()
{ {

View File

@ -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
}

View File

@ -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
} }

View File

@ -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;
}
}

View File

@ -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 };
}

View File

@ -11,47 +11,6 @@ using Newtonsoft.Json;
namespace AyCode.Core.Extensions; 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> /// <summary>
/// Cached result for IId type info lookup. /// Cached result for IId type info lookup.
/// </summary> /// </summary>

View File

@ -340,8 +340,7 @@ public static class SerializeObjectExtensions
/// <summary> /// <summary>
/// Serialize object to JSON string with default options. /// Serialize object to JSON string with default options.
/// </summary> /// </summary>
public static string ToJson<T>(this T source) public static string ToJson<T>(this T source) => AcJsonSerializer.Serialize(source);
=> AcJsonSerializer.Serialize(source);
/// <summary> /// <summary>
/// Serialize object to JSON string with specified options. /// 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) public static T MessagePackTo<T>(this byte[] message, MessagePackSerializerOptions options)
=> MessagePackSerializer.Deserialize<T>(message, 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 #region Binary Serialization Extension Methods
/// <summary> /// <summary>
/// Serialize object to binary byte array with default options. /// Serialize object to binary byte array with default options.
/// Significantly faster than JSON, especially for large data in WASM. /// Significantly faster than JSON, especially for large data in WASM.
/// </summary> /// </summary>
public static byte[] ToBinary<T>(this T source) public static byte[] ToBinary<T>(this T source) => AcBinarySerializer.Serialize(source);
=> AcBinarySerializer.Serialize(source);
/// <summary> /// <summary>
/// Serialize object to binary byte array with specified options. /// Serialize object to binary byte array with specified options.

View File

@ -17,7 +17,9 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
{ {
protected readonly List<AcDynamicMethodCallModel<SignalRAttribute>> DynamicMethodCallModels = []; protected readonly List<AcDynamicMethodCallModel<SignalRAttribute>> DynamicMethodCallModels = [];
protected TLogger Logger = logger; protected TLogger Logger = logger;
protected IConfiguration Configuration = configuration; protected IConfiguration Configuration = configuration;
protected AcSerializerOptions SerializerOptions = new AcBinarySerializerOptions();
#region Connection Lifecycle #region Connection Lifecycle

View File

@ -153,7 +153,7 @@ namespace BenchmarkSuite1
pointsPerMeasurement: 5); pointsPerMeasurement: 5);
var binaryWithRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); 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 json = AcJsonSerializer.Serialize(order, AcJsonSerializerOptions.WithoutReferenceHandling());
var jsonBytes = Encoding.UTF8.GetByteCount(json); var jsonBytes = Encoding.UTF8.GetByteCount(json);