1214 lines
42 KiB
C#
1214 lines
42 KiB
C#
using AyCode.Core.Helpers;
|
|
using AyCode.Core.Serializers.Expressions;
|
|
using System.Buffers;
|
|
using System.Collections;
|
|
using System.Collections.Concurrent;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using static AyCode.Core.Helpers.JsonUtilities;
|
|
|
|
namespace AyCode.Core.Serializers.Binaries;
|
|
|
|
/// <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
|
|
/// - Optimized buffer management with ArrayPool
|
|
/// - Zero-allocation hot paths using Span and MemoryMarshal
|
|
/// - Automatic Expression to AcExpressionNode conversion
|
|
/// </summary>
|
|
public static partial class AcBinarySerializer
|
|
{
|
|
|
|
// Pre-computed UTF8 encoder for string operations
|
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
|
private static readonly Type StringType = typeof(string);
|
|
private static readonly Type GuidType = typeof(Guid);
|
|
private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
|
|
private static readonly Type TimeSpanType = typeof(TimeSpan);
|
|
private static readonly Type IntType = typeof(int);
|
|
private static readonly Type LongType = typeof(long);
|
|
private static readonly Type FloatType = typeof(float);
|
|
private static readonly Type DoubleType = typeof(double);
|
|
private static readonly Type DecimalType = typeof(decimal);
|
|
private static readonly Type BoolType = typeof(bool);
|
|
private static readonly Type DateTimeType = typeof(DateTime);
|
|
|
|
#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 runtimeType = value.GetType();
|
|
|
|
// Handle IQueryable types - convert to AcExpressionNode (serialize the Expression)
|
|
object actualValue = value;
|
|
if (value is IQueryable queryable)
|
|
{
|
|
actualValue = AcSerializerCommon.QueryableToNode(queryable);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
// Handle Expression types - convert to AcExpressionNode
|
|
else if (AcSerializerCommon.IsExpressionType(runtimeType))
|
|
{
|
|
actualValue = AcExpressionConverter.ToNode((Expression)(object)value);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
|
|
var context = SerializeCore(actualValue, runtimeType, options);
|
|
try
|
|
{
|
|
return context.ToArray();
|
|
}
|
|
finally
|
|
{
|
|
BinarySerializationContextPool.Return(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize object to an IBufferWriter for zero-copy scenarios.
|
|
/// This avoids the final ToArray() allocation by writing directly to the caller's buffer.
|
|
/// </summary>
|
|
public static void Serialize<T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null)
|
|
{
|
|
var span = writer.GetSpan(1);
|
|
span[0] = BinaryTypeCode.Null;
|
|
writer.Advance(1);
|
|
return;
|
|
}
|
|
|
|
var runtimeType = value.GetType();
|
|
|
|
// Handle IQueryable types - convert to AcExpressionNode (serialize the Expression)
|
|
object actualValue = value;
|
|
if (value is IQueryable queryable)
|
|
{
|
|
actualValue = AcSerializerCommon.QueryableToNode(queryable);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
// Handle Expression types - convert to AcExpressionNode
|
|
else if (AcSerializerCommon.IsExpressionType(runtimeType))
|
|
{
|
|
actualValue = AcExpressionConverter.ToNode((Expression)(object)value);
|
|
runtimeType = typeof(AcExpressionNode);
|
|
}
|
|
|
|
var context = SerializeCore(actualValue, runtimeType, options);
|
|
try
|
|
{
|
|
context.WriteTo(writer);
|
|
}
|
|
finally
|
|
{
|
|
BinarySerializationContextPool.Return(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the serialized size without allocating the final array.
|
|
/// Useful for pre-allocating buffers.
|
|
/// </summary>
|
|
public static int GetSerializedSize<T>(T value, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null) return 1;
|
|
|
|
var runtimeType = value.GetType();
|
|
var context = SerializeCore(value, runtimeType, options);
|
|
try
|
|
{
|
|
return context.Position;
|
|
}
|
|
finally
|
|
{
|
|
BinarySerializationContextPool.Return(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize object and keep the pooled buffer for zero-copy consumers.
|
|
/// Caller must dispose the returned result to release the buffer.
|
|
/// </summary>
|
|
public static BinarySerializationResult SerializeToPooledBuffer<T>(T value, AcBinarySerializerOptions options)
|
|
{
|
|
if (value == null)
|
|
{
|
|
return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]);
|
|
}
|
|
|
|
var runtimeType = value.GetType();
|
|
var context = SerializeCore(value, runtimeType, options);
|
|
try
|
|
{
|
|
return context.DetachResult();
|
|
}
|
|
finally
|
|
{
|
|
BinarySerializationContextPool.Return(context);
|
|
}
|
|
}
|
|
|
|
private static BinarySerializationContext SerializeCore(object value, Type runtimeType, AcBinarySerializerOptions options)
|
|
{
|
|
var context = BinarySerializationContextPool.Get(options);
|
|
context.WriteHeaderPlaceholder();
|
|
|
|
// Single-pass serialization - no scan phase needed!
|
|
// Reference tracking happens inline via TryTrack during WriteObject
|
|
|
|
// Estimate and reserve header space to avoid body shift later
|
|
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
|
|
context.ReserveHeaderSpace(estimatedHeaderSize);
|
|
|
|
WriteValue(value, runtimeType, context, 0);
|
|
context.FinalizeHeaderSections();
|
|
return context;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Property Metadata Registration
|
|
|
|
private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null)
|
|
{
|
|
if (IsPrimitiveOrStringFast(type)) return;
|
|
|
|
visited ??= new HashSet<Type>();
|
|
if (!visited.Add(type)) return;
|
|
|
|
if (IsDictionaryType(type, out var keyType, out var valueType))
|
|
{
|
|
if (keyType != null) RegisterMetadataForType(keyType, context, visited);
|
|
if (valueType != null) RegisterMetadataForType(valueType, context, visited);
|
|
return;
|
|
}
|
|
|
|
if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
|
|
{
|
|
var elementType = GetCollectionElementType(type);
|
|
if (elementType != null)
|
|
{
|
|
RegisterMetadataForType(elementType, context, visited);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var wrapper = context.GetWrapper(type);
|
|
var metadata = wrapper.Metadata;
|
|
var properties = metadata.Properties;
|
|
|
|
// Use index-based iteration for array access
|
|
for (var i = 0; i < properties.Length; i++)
|
|
{
|
|
var prop = properties[i];
|
|
|
|
if (!context.ShouldIncludePropertyInMetadata(prop))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Use caching registration to avoid dictionary lookup during serialization
|
|
context.RegisterPropertyNameAndCache(prop);
|
|
|
|
if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType))
|
|
{
|
|
RegisterMetadataForType(nestedType, context, visited);
|
|
}
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool TryResolveNestedMetadataType(Type propertyType, out Type nestedType)
|
|
{
|
|
nestedType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
|
|
|
if (IsPrimitiveOrStringFast(nestedType))
|
|
return false;
|
|
|
|
if (IsDictionaryType(nestedType, out var _, out var valueType) && valueType != null)
|
|
{
|
|
if (!IsPrimitiveOrStringFast(valueType))
|
|
{
|
|
nestedType = valueType;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (typeof(IEnumerable).IsAssignableFrom(nestedType) && !ReferenceEquals(nestedType, StringType))
|
|
{
|
|
var elementType = GetCollectionElementType(nestedType);
|
|
if (elementType != null && !IsPrimitiveOrStringFast(elementType))
|
|
{
|
|
nestedType = elementType;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#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;
|
|
}
|
|
|
|
// Handle byte arrays specially (value-like, no reference tracking)
|
|
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, type, context, depth);
|
|
return;
|
|
}
|
|
|
|
// Handle complex objects with single-pass reference tracking
|
|
WriteObject(value, type, context, depth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized primitive writer using TypeCode dispatch.
|
|
/// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context)
|
|
{
|
|
// Fast path: check TypeCode first (handles most primitives)
|
|
var typeCode = Type.GetTypeCode(type);
|
|
|
|
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:
|
|
WriteFloat64Unsafe((double)value, context);
|
|
return true;
|
|
case TypeCode.String:
|
|
WriteString((string)value, context);
|
|
return true;
|
|
case TypeCode.Single:
|
|
WriteFloat32Unsafe((float)value, context);
|
|
return true;
|
|
case TypeCode.Decimal:
|
|
WriteDecimalUnsafe((decimal)value, context);
|
|
return true;
|
|
case TypeCode.DateTime:
|
|
WriteDateTimeUnsafe((DateTime)value, context);
|
|
return true;
|
|
case TypeCode.Byte:
|
|
context.WriteByte(BinaryTypeCode.UInt8);
|
|
context.WriteByte((byte)value);
|
|
return true;
|
|
case TypeCode.Int16:
|
|
WriteInt16Unsafe((short)value, context);
|
|
return true;
|
|
case TypeCode.UInt16:
|
|
WriteUInt16Unsafe((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:
|
|
WriteCharUnsafe((char)value, context);
|
|
return true;
|
|
}
|
|
|
|
// Handle special types by reference comparison (faster than type equality)
|
|
if (ReferenceEquals(type, GuidType))
|
|
{
|
|
WriteGuidUnsafe((Guid)value, context);
|
|
return true;
|
|
}
|
|
if (ReferenceEquals(type, DateTimeOffsetType))
|
|
{
|
|
WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context);
|
|
return true;
|
|
}
|
|
if (ReferenceEquals(type, TimeSpanType))
|
|
{
|
|
WriteTimeSpanUnsafe((TimeSpan)value, context);
|
|
return true;
|
|
}
|
|
if (type.IsEnum)
|
|
{
|
|
WriteEnum(value, context);
|
|
return true;
|
|
}
|
|
|
|
// Handle nullable types - use cached check instead of GetUnderlyingType
|
|
// For nullable, value is already unwrapped when boxed, so we can use value.GetType()
|
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
|
{
|
|
// When boxed, nullable value types are unwrapped to their underlying type
|
|
// So we can just call TryWritePrimitive with the actual runtime type
|
|
return TryWritePrimitive(value, value.GetType(), context);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Optimized Primitive Writers using MemoryMarshal
|
|
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized float64 writer using batched write.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteFloat64Unsafe(double value, BinarySerializationContext context)
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized float32 writer using batched write.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteFloat32Unsafe(float value, BinarySerializationContext context)
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized decimal writer using direct memory copy of bits.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDecimalUnsafe(decimal value, BinarySerializationContext context)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Decimal);
|
|
context.WriteDecimalBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized DateTime writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDateTimeUnsafe(DateTime value, BinarySerializationContext context)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.DateTime);
|
|
context.WriteDateTimeBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized Guid writer using direct memory copy.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteGuidUnsafe(Guid value, BinarySerializationContext context)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Guid);
|
|
context.WriteGuidBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized DateTimeOffset writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteDateTimeOffsetUnsafe(DateTimeOffset value, BinarySerializationContext context)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.DateTimeOffset);
|
|
context.WriteDateTimeOffsetBits(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized TimeSpan writer.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteTimeSpanUnsafe(TimeSpan value, BinarySerializationContext context)
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteInt16Unsafe(short value, BinarySerializationContext context)
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteUInt16Unsafe(ushort value, BinarySerializationContext context)
|
|
{
|
|
context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value);
|
|
}
|
|
|
|
[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 WriteCharUnsafe(char value, BinarySerializationContext context)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Char);
|
|
context.WriteRaw(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteEnum(object value, BinarySerializationContext context)
|
|
{
|
|
// Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead
|
|
var intValue = GetEnumAsInt32Fast(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fast enum to int conversion avoiding Convert.ToInt32 overhead.
|
|
/// Handles all common enum underlying types.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int GetEnumAsInt32Fast(object enumValue)
|
|
{
|
|
var type = enumValue.GetType();
|
|
var underlyingType = type.GetEnumUnderlyingType();
|
|
|
|
if (ReferenceEquals(underlyingType, IntType))
|
|
return (int)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(byte)))
|
|
return (byte)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(sbyte)))
|
|
return (sbyte)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(short)))
|
|
return (short)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(ushort)))
|
|
return (ushort)enumValue;
|
|
if (ReferenceEquals(underlyingType, typeof(uint)))
|
|
return unchecked((int)(uint)enumValue);
|
|
|
|
// Fallback for rare cases (long, ulong)
|
|
return Convert.ToInt32(enumValue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized string writer with FixStr for short strings.
|
|
/// Uses stackalloc for small strings to avoid allocations.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteString(string value, BinarySerializationContext context)
|
|
{
|
|
if (value.Length == 0)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.StringEmpty);
|
|
return;
|
|
}
|
|
|
|
if (context.UseStringInterning && value.Length >= context.MinStringInternLength)
|
|
{
|
|
var index = context.RegisterInternedString(value);
|
|
context.WriteByte(BinaryTypeCode.StringInterned);
|
|
context.WriteVarUInt((uint)index);
|
|
return;
|
|
}
|
|
|
|
// Try FixStr for short ASCII strings (saves 1-2 bytes per string)
|
|
if (System.Text.Ascii.IsValid(value) && BinaryTypeCode.CanEncodeAsFixStr(value.Length))
|
|
{
|
|
context.WriteFixStr(value);
|
|
return;
|
|
}
|
|
|
|
// Standard string encoding
|
|
context.WriteByte(BinaryTypeCode.String);
|
|
context.WriteStringUtf8(value);
|
|
}
|
|
|
|
[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)
|
|
{
|
|
var wrapper = context.GetWrapper(type);
|
|
var metadata = wrapper.Metadata;
|
|
|
|
// Single-pass reference tracking
|
|
if (context.UseReferenceHandling)
|
|
{
|
|
switch (metadata.IdAccessorType)
|
|
{
|
|
case AcSerializerCommon.IdAccessorType.Int32:
|
|
if (!context.TryTrack(wrapper, value, out int intId))
|
|
{
|
|
// Already seen → write reference
|
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
|
context.WriteVarInt(intId);
|
|
return;
|
|
}
|
|
// First occurrence → write object with refId
|
|
context.WriteByte(BinaryTypeCode.Object);
|
|
context.WriteVarInt(intId);
|
|
break;
|
|
|
|
case AcSerializerCommon.IdAccessorType.Int64:
|
|
if (!context.TryTrack(wrapper, value, out long longId))
|
|
{
|
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
|
context.WriteVarLong(longId);
|
|
return;
|
|
}
|
|
context.WriteByte(BinaryTypeCode.Object);
|
|
context.WriteVarLong(longId);
|
|
break;
|
|
|
|
case AcSerializerCommon.IdAccessorType.Guid:
|
|
if (!context.TryTrack(wrapper, value, out Guid guidId))
|
|
{
|
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
|
context.WriteGuidBits(guidId);
|
|
return;
|
|
}
|
|
context.WriteByte(BinaryTypeCode.Object);
|
|
context.WriteGuidBits(guidId);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No reference handling - just write object marker
|
|
context.WriteByte(BinaryTypeCode.Object);
|
|
}
|
|
|
|
// Write properties
|
|
var nextDepth = depth + 1;
|
|
var properties = metadata.Properties;
|
|
var propCount = properties.Length;
|
|
|
|
// Single-pass serialization with SKIP markers
|
|
// - No property count needed (fixed property order)
|
|
// - No property indices needed (sequential order)
|
|
// - Single getter call per property
|
|
// - Write value OR skip marker in one operation
|
|
|
|
for (var i = 0; i < propCount; i++)
|
|
{
|
|
var prop = properties[i];
|
|
|
|
// Skip if filter says no - write skip marker
|
|
if (context.PropertyFilter != null && !context.ShouldSerializeProperty(value, prop))
|
|
{
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
continue;
|
|
}
|
|
|
|
// Write property value OR skip marker (single operation, single getter call)
|
|
WritePropertyOrSkip(value, prop, context, nextDepth);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a property value is null or default without boxing for value types.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop)
|
|
{
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
return prop.GetInt32(obj) == 0;
|
|
case PropertyAccessorType.Int64:
|
|
return prop.GetInt64(obj) == 0L;
|
|
case PropertyAccessorType.Boolean:
|
|
return !prop.GetBoolean(obj);
|
|
case PropertyAccessorType.Double:
|
|
return prop.GetDouble(obj) == 0.0;
|
|
case PropertyAccessorType.Single:
|
|
return prop.GetSingle(obj) == 0f;
|
|
case PropertyAccessorType.Decimal:
|
|
return prop.GetDecimal(obj) == 0m;
|
|
case PropertyAccessorType.Byte:
|
|
return prop.GetByte(obj) == 0;
|
|
case PropertyAccessorType.Int16:
|
|
return prop.GetInt16(obj) == 0;
|
|
case PropertyAccessorType.UInt16:
|
|
return prop.GetUInt16(obj) == 0;
|
|
case PropertyAccessorType.UInt32:
|
|
return prop.GetUInt32(obj) == 0;
|
|
case PropertyAccessorType.UInt64:
|
|
return prop.GetUInt64(obj) == 0;
|
|
case PropertyAccessorType.Guid:
|
|
return prop.GetGuid(obj) == Guid.Empty;
|
|
case PropertyAccessorType.Enum:
|
|
return prop.GetEnumAsInt32(obj) == 0;
|
|
case PropertyAccessorType.DateTime:
|
|
// DateTime default is not typically skipped
|
|
return false;
|
|
default:
|
|
// Object type - use regular getter
|
|
var value = prop.GetValue(obj);
|
|
if (value == null) return true;
|
|
if (prop.PropertyTypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a property value using typed getters to avoid boxing.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth)
|
|
{
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
WriteInt32(prop.GetInt32(obj), context);
|
|
return;
|
|
case PropertyAccessorType.Int64:
|
|
WriteInt64(prop.GetInt64(obj), context);
|
|
return;
|
|
case PropertyAccessorType.Boolean:
|
|
context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
return;
|
|
case PropertyAccessorType.Double:
|
|
WriteFloat64Unsafe(prop.GetDouble(obj), context);
|
|
return;
|
|
case PropertyAccessorType.Single:
|
|
WriteFloat32Unsafe(prop.GetSingle(obj), context);
|
|
return;
|
|
case PropertyAccessorType.Decimal:
|
|
WriteDecimalUnsafe(prop.GetDecimal(obj), context);
|
|
return;
|
|
case PropertyAccessorType.DateTime:
|
|
WriteDateTimeUnsafe(prop.GetDateTime(obj), context);
|
|
return;
|
|
case PropertyAccessorType.Byte:
|
|
context.WriteByte(BinaryTypeCode.UInt8);
|
|
context.WriteByte(prop.GetByte(obj));
|
|
return;
|
|
case PropertyAccessorType.Int16:
|
|
WriteInt16Unsafe(prop.GetInt16(obj), context);
|
|
return;
|
|
case PropertyAccessorType.UInt16:
|
|
WriteUInt16Unsafe(prop.GetUInt16(obj), context);
|
|
return;
|
|
case PropertyAccessorType.UInt32:
|
|
WriteUInt32(prop.GetUInt32(obj), context);
|
|
return;
|
|
case PropertyAccessorType.UInt64:
|
|
WriteUInt64(prop.GetUInt64(obj), context);
|
|
return;
|
|
case PropertyAccessorType.Guid:
|
|
WriteGuidUnsafe(prop.GetGuid(obj), context);
|
|
return;
|
|
case PropertyAccessorType.Enum:
|
|
var enumValue = prop.GetEnumAsInt32(obj);
|
|
if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Enum);
|
|
context.WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Enum);
|
|
context.WriteByte(BinaryTypeCode.Int32);
|
|
context.WriteVarInt(enumValue);
|
|
}
|
|
return;
|
|
default:
|
|
// Fallback to object getter for reference types
|
|
var value = prop.GetValue(obj);
|
|
WriteValue(value, prop.PropertyType, context, depth);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a property value OR a skip marker if the value is default/null.
|
|
/// Single-pass optimization: checks default + writes value in one operation.
|
|
/// Avoids double getter calls.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WritePropertyOrSkip(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth)
|
|
{
|
|
switch (prop.AccessorType)
|
|
{
|
|
case PropertyAccessorType.Int32:
|
|
{
|
|
int value = prop.GetInt32(obj);
|
|
if (value == 0)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteInt32(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Int64:
|
|
{
|
|
long value = prop.GetInt64(obj);
|
|
if (value == 0L)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteInt64(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Boolean:
|
|
{
|
|
bool value = prop.GetBoolean(obj);
|
|
if (!value)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
context.WriteByte(BinaryTypeCode.True);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Double:
|
|
{
|
|
double value = prop.GetDouble(obj);
|
|
if (value == 0.0)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteFloat64Unsafe(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Single:
|
|
{
|
|
float value = prop.GetSingle(obj);
|
|
if (value == 0f)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteFloat32Unsafe(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Decimal:
|
|
{
|
|
decimal value = prop.GetDecimal(obj);
|
|
if (value == 0m)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteDecimalUnsafe(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.DateTime:
|
|
{
|
|
DateTime value = prop.GetDateTime(obj);
|
|
// DateTime always written (no default skip)
|
|
WriteDateTimeUnsafe(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Byte:
|
|
{
|
|
byte value = prop.GetByte(obj);
|
|
if (value == 0)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
{
|
|
context.WriteByte(BinaryTypeCode.UInt8);
|
|
context.WriteByte(value);
|
|
}
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Int16:
|
|
{
|
|
short value = prop.GetInt16(obj);
|
|
if (value == 0)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteInt16Unsafe(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.UInt16:
|
|
{
|
|
ushort value = prop.GetUInt16(obj);
|
|
if (value == 0)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteUInt16Unsafe(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.UInt32:
|
|
{
|
|
uint value = prop.GetUInt32(obj);
|
|
if (value == 0)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteUInt32(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.UInt64:
|
|
{
|
|
ulong value = prop.GetUInt64(obj);
|
|
if (value == 0)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteUInt64(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Guid:
|
|
{
|
|
Guid value = prop.GetGuid(obj);
|
|
if (value == Guid.Empty)
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
else
|
|
WriteGuidUnsafe(value, context);
|
|
return;
|
|
}
|
|
case PropertyAccessorType.Enum:
|
|
{
|
|
int enumValue = prop.GetEnumAsInt32(obj);
|
|
if (enumValue == 0)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
}
|
|
else if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Enum);
|
|
context.WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Enum);
|
|
context.WriteByte(BinaryTypeCode.Int32);
|
|
context.WriteVarInt(enumValue);
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
{
|
|
// Object type - use regular getter
|
|
var value = prop.GetValue(obj);
|
|
|
|
// SKIP marker only for null (reference types)
|
|
// Empty string, empty collections, etc. are valid values and must be written!
|
|
if (value == null)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
|
}
|
|
else
|
|
{
|
|
WriteValue(value, prop.PropertyType, context, depth);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Specialized Array Writers
|
|
|
|
/// <summary>
|
|
/// Optimized array writer with specialized paths for primitive arrays.
|
|
/// </summary>
|
|
private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth)
|
|
{
|
|
context.WriteByte(BinaryTypeCode.Array);
|
|
var nextDepth = depth + 1;
|
|
|
|
// Optimized path for primitive arrays
|
|
var elementType = GetCollectionElementType(type);
|
|
if (elementType != null && type.IsArray)
|
|
{
|
|
if (TryWritePrimitiveArray(enumerable, elementType, context))
|
|
return;
|
|
}
|
|
|
|
// For IList, we can write the count directly
|
|
if (enumerable is IList list)
|
|
{
|
|
var count = list.Count;
|
|
context.WriteVarUInt((uint)count);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var item = list[i];
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specialized array writer for primitive arrays using bulk memory operations.
|
|
/// Optimized for Blazor Hybrid compatibility (WASM, Android, Windows, iOS).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context)
|
|
{
|
|
// Int32 array - very common case
|
|
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
|
|
{
|
|
context.WriteVarUInt((uint)intArray.Length);
|
|
context.WriteInt32ArrayOptimized(intArray);
|
|
return true;
|
|
}
|
|
|
|
// Double array - bulk write as raw bytes
|
|
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
|
|
{
|
|
context.WriteVarUInt((uint)doubleArray.Length);
|
|
context.WriteDoubleArrayBulk(doubleArray);
|
|
return true;
|
|
}
|
|
|
|
// Long array
|
|
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
|
|
{
|
|
context.WriteVarUInt((uint)longArray.Length);
|
|
context.WriteLongArrayOptimized(longArray);
|
|
return true;
|
|
}
|
|
|
|
// Float array - bulk write as raw bytes
|
|
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
|
|
{
|
|
context.WriteVarUInt((uint)floatArray.Length);
|
|
context.WriteFloatArrayBulk(floatArray);
|
|
return true;
|
|
}
|
|
|
|
// Bool array - pack as bytes
|
|
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
|
|
{
|
|
context.WriteVarUInt((uint)boolArray.Length);
|
|
for (var i = 0; i < boolArray.Length; i++)
|
|
{
|
|
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
|
}
|
|
context.WriteVarUInt((uint)boolArray.Length);
|
|
return true;
|
|
}
|
|
|
|
// Guid array - bulk write
|
|
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
|
|
{
|
|
context.WriteVarUInt((uint)guidArray.Length);
|
|
context.WriteGuidArrayBulk(guidArray);
|
|
return true;
|
|
}
|
|
|
|
// Decimal array
|
|
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
|
|
{
|
|
context.WriteVarUInt((uint)decimalArray.Length);
|
|
for (var i = 0; i < decimalArray.Length; i++)
|
|
{
|
|
WriteDecimalUnsafe(decimalArray[i], context);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// DateTime array
|
|
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
|
|
{
|
|
context.WriteVarUInt((uint)dateTimeArray.Length);
|
|
for (var i = 0; i < dateTimeArray.Length; i++)
|
|
{
|
|
WriteDateTimeUnsafe(dateTimeArray[i], context);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// String array - common case
|
|
if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray)
|
|
{
|
|
context.WriteVarUInt((uint)stringArray.Length);
|
|
for (var i = 0; i < stringArray.Length; i++)
|
|
{
|
|
var s = stringArray[i];
|
|
if (s == null)
|
|
context.WriteByte(BinaryTypeCode.Null);
|
|
else
|
|
WriteString(s, context);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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 Serialization Result
|
|
|
|
// Implementation moved to AcBinarySerializer.BinarySerializationResult.cs
|
|
|
|
#endregion
|
|
|
|
#region Context Pool
|
|
|
|
// Implementation moved to AcBinarySerializer.BinarySerializationContext.cs
|
|
|
|
#endregion
|
|
|
|
#region Serialization Context
|
|
|
|
// Implementation moved to AcBinarySerializer.BinarySerializationContext.cs
|
|
|
|
#endregion
|
|
|
|
#region Type Metadata
|
|
|
|
private static Type? GetCollectionElementType(Type type)
|
|
{
|
|
if (type.IsArray)
|
|
{
|
|
return type.GetElementType();
|
|
}
|
|
|
|
if (type.IsGenericType)
|
|
{
|
|
var args = type.GetGenericArguments();
|
|
if (args.Length == 1)
|
|
{
|
|
return args[0];
|
|
}
|
|
}
|
|
|
|
foreach (var iface in type.GetInterfaces())
|
|
{
|
|
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
|
{
|
|
return iface.GetGenericArguments()[0];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
// Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs
|
|
|
|
#endregion
|
|
} |