Refactor: property index-based binary serialization
Switch to deterministic property index-based serialization/deserialization for improved performance and cross-platform consistency. Properties are now ordered alphabetically and accessed by index, enabling O(1) lookups and eliminating string/dictionary overhead. Introduce thread-local metadata caches, refactor metadata and populate logic, optimize string interning and enum handling, and remove legacy name-based code paths. Update diagnostics and documentation for clarity and maintainability.
This commit is contained in:
parent
0552268ac1
commit
a72f9883b4
|
|
@ -230,10 +230,7 @@ public class AcBinarySerializerDiagnosticTests
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false)
|
/// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false)
|
||||||
/// This test verifies what happens when:
|
/// This test verifies that property-index-based serialization correctly handles null properties.
|
||||||
/// 1. Metadata header registers ALL properties including StockTakingItems
|
|
||||||
/// 2. Body SKIPS StockTakingItems because it's null
|
|
||||||
/// 3. Deserializer reads the body and must correctly map indices
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices()
|
public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices()
|
||||||
|
|
@ -262,20 +259,13 @@ public class AcBinarySerializerDiagnosticTests
|
||||||
var marker = binary[pos++];
|
var marker = binary[pos++];
|
||||||
Console.WriteLine($"Marker: 0x{marker:X2}");
|
Console.WriteLine($"Marker: 0x{marker:X2}");
|
||||||
|
|
||||||
// Read property count from metadata header
|
// Skip any header data (strings interning, etc.)
|
||||||
if ((marker & 0x10) != 0) // HasMetadata flag
|
// New format uses PropertyIndex directly - no metadata header with property names
|
||||||
{
|
|
||||||
var propCount = binary[pos++];
|
|
||||||
Console.WriteLine($"\n=== METADATA HEADER ===");
|
|
||||||
Console.WriteLine($"Property count in header: {propCount}");
|
|
||||||
|
|
||||||
for (int i = 0; i < propCount; i++)
|
// Find Object marker (0x19)
|
||||||
{
|
while (pos < binary.Length && binary[pos] != 0x19)
|
||||||
var strLen = binary[pos++];
|
{
|
||||||
var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen);
|
pos++;
|
||||||
pos += strLen;
|
|
||||||
Console.WriteLine($" Header property [{i}]: '{propName}'");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
|
Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
|
||||||
|
|
@ -284,6 +274,7 @@ public class AcBinarySerializerDiagnosticTests
|
||||||
var bodyStart = pos;
|
var bodyStart = pos;
|
||||||
var objectMarker = binary[pos++];
|
var objectMarker = binary[pos++];
|
||||||
Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)");
|
Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)");
|
||||||
|
Assert.AreEqual(0x19, objectMarker, "Object marker should be 0x19");
|
||||||
|
|
||||||
// Read ref ID (if reference handling is enabled)
|
// Read ref ID (if reference handling is enabled)
|
||||||
// VarInt: if top bit is set, continue reading
|
// VarInt: if top bit is set, continue reading
|
||||||
|
|
@ -306,11 +297,11 @@ public class AcBinarySerializerDiagnosticTests
|
||||||
var bodyPropCount = binary[pos++];
|
var bodyPropCount = binary[pos++];
|
||||||
Console.WriteLine($"Property count in body: {bodyPropCount}");
|
Console.WriteLine($"Property count in body: {bodyPropCount}");
|
||||||
|
|
||||||
Console.WriteLine($"\n=== BODY PROPERTIES ===");
|
Console.WriteLine($"\n=== BODY PROPERTIES (PropertyIndex format) ===");
|
||||||
for (int i = 0; i < bodyPropCount && pos < binary.Length; i++)
|
for (int i = 0; i < bodyPropCount && pos < binary.Length; i++)
|
||||||
{
|
{
|
||||||
var propIndex = binary[pos++];
|
var propIndex = binary[pos++]; // This is now PropertyIndex (alphabetical order)
|
||||||
Console.WriteLine($" Body property [{i}]: index={propIndex}, next bytes: 0x{binary[pos]:X2} 0x{(pos + 1 < binary.Length ? binary[pos + 1] : 0):X2}");
|
Console.WriteLine($" Body property [{i}]: PropertyIndex={propIndex}");
|
||||||
|
|
||||||
// Skip the value (simplified - just log)
|
// Skip the value (simplified - just log)
|
||||||
var valueType = binary[pos];
|
var valueType = binary[pos];
|
||||||
|
|
@ -342,16 +333,6 @@ public class AcBinarySerializerDiagnosticTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find where 0xD6 (Creator = 6) appears in the body
|
|
||||||
Console.WriteLine($"\n=== 0xD6 OCCURRENCES ===");
|
|
||||||
for (int i = bodyStart; i < binary.Length; i++)
|
|
||||||
{
|
|
||||||
if (binary[i] == 0xD6)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Found 0xD6 (TinyInt 6 = Creator value) at position {i}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deserialize and verify
|
// Deserialize and verify
|
||||||
var result = binary.BinaryTo<SimStockTaking>();
|
var result = binary.BinaryTo<SimStockTaking>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,44 @@ using System.Collections;
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using AyCode.Core.Serializers;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
public static partial class AcBinaryDeserializer
|
public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
internal sealed class BinaryDeserializeTypeMetadata : TypeMetadataBase
|
internal sealed class BinaryDeserializeTypeMetadata : BinaryTypeMetadataBase
|
||||||
{
|
{
|
||||||
private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _properties;
|
private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _properties;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Properties array ordered alphabetically by name for index-based lookup.
|
||||||
|
/// This matches the serializer's ordering, enabling O(1) array access.
|
||||||
|
/// </summary>
|
||||||
public BinaryPropertySetterInfo[] PropertiesArray { get; }
|
public BinaryPropertySetterInfo[] PropertiesArray { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this type implements IId interface.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsIId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Compiled getter for the Id property (if IsIId is true).
|
|
||||||
/// </summary>
|
|
||||||
public Func<object, object?>? IdGetter { get; }
|
|
||||||
|
|
||||||
public BinaryDeserializeTypeMetadata(Type type) : base(type)
|
public BinaryDeserializeTypeMetadata(Type type) : base(type)
|
||||||
{
|
{
|
||||||
PropertiesArray = GetSerializableProperties(type, requiresRead: true, requiresWrite: true)
|
var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true);
|
||||||
.Where(static p => p.GetMethod is { IsPublic: true } && p.SetMethod is { IsPublic: true })
|
|
||||||
.Select(static p => new BinaryPropertySetterInfo(p, p.DeclaringType!))
|
PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length];
|
||||||
.ToArray();
|
for (var i = 0; i < orderedProperties.Length; i++)
|
||||||
|
{
|
||||||
|
PropertiesArray[i] = new BinaryPropertySetterInfo(orderedProperties[i], type);
|
||||||
|
}
|
||||||
|
|
||||||
_properties = PropertiesArray.Length == 0
|
_properties = PropertiesArray.Length == 0
|
||||||
? FrozenDictionary<string, BinaryPropertySetterInfo>.Empty
|
? FrozenDictionary<string, BinaryPropertySetterInfo>.Empty
|
||||||
: PropertiesArray.ToFrozenDictionary(static p => p.Name, static p => p, StringComparer.Ordinal);
|
: PropertiesArray.ToFrozenDictionary(static p => p.Name, static p => p, StringComparer.Ordinal);
|
||||||
|
|
||||||
// Check if type implements IId
|
|
||||||
var (isIId, _) = GetIdInfo(type);
|
|
||||||
IsIId = isIId;
|
|
||||||
if (isIId)
|
|
||||||
{
|
|
||||||
var idProp = type.GetProperty("Id");
|
|
||||||
if (idProp != null)
|
|
||||||
IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets property by index (O(1) array access). Used for index-based deserialization.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public BinaryPropertySetterInfo? GetPropertyByIndex(int index)
|
||||||
|
=> (uint)index < (uint)PropertiesArray.Length ? PropertiesArray[index] : null;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propertyInfo)
|
public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propertyInfo)
|
||||||
=> _properties.TryGetValue(name, out propertyInfo);
|
=> _properties.TryGetValue(name, out propertyInfo);
|
||||||
|
|
@ -87,7 +80,7 @@ public static partial class AcBinaryDeserializer
|
||||||
Type? elementType,
|
Type? elementType,
|
||||||
Type? elementIdType,
|
Type? elementIdType,
|
||||||
Func<object, object?>? elementIdGetter)
|
Func<object, object?>? elementIdGetter)
|
||||||
: base(CreateDummyProperty(), typeof(object))
|
: base(CreateDummyProperty(), typeof(DummyClass))
|
||||||
{
|
{
|
||||||
_isManualConstruction = true;
|
_isManualConstruction = true;
|
||||||
_manualName = name;
|
_manualName = name;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,497 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using AyCode.Core.Helpers;
|
||||||
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
|
public static partial class AcBinaryDeserializer
|
||||||
|
{
|
||||||
|
#region Populate Object Methods
|
||||||
|
|
||||||
|
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;
|
||||||
|
var targetType = target.GetType();
|
||||||
|
var targetTypeName = targetType.Name;
|
||||||
|
|
||||||
|
for (int i = 0; i < propertyCount; i++)
|
||||||
|
{
|
||||||
|
var propertyIndexStartPosition = context.Position;
|
||||||
|
|
||||||
|
// Read property index directly - no string lookup needed!
|
||||||
|
// PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms
|
||||||
|
var propIndex = (int)context.ReadVarUInt();
|
||||||
|
|
||||||
|
// O(1) array lookup instead of dictionary lookup
|
||||||
|
var propInfo = metadata.GetPropertyByIndex(propIndex);
|
||||||
|
|
||||||
|
if (propInfo == null)
|
||||||
|
{
|
||||||
|
// Skip unknown property (schema evolution - new property on sender side)
|
||||||
|
SkipValue(ref context);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIMIZATION: Reuse existing nested objects instead of creating new ones
|
||||||
|
var peekCode = context.PeekByte();
|
||||||
|
|
||||||
|
// Handle nested complex objects - reuse existing if available
|
||||||
|
if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
|
||||||
|
{
|
||||||
|
var existingObj = propInfo.GetValue(target);
|
||||||
|
if (existingObj != null)
|
||||||
|
{
|
||||||
|
context.ReadByte(); // consume Object marker
|
||||||
|
PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle collections - reuse existing collection and populate items
|
||||||
|
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
||||||
|
{
|
||||||
|
var existingCollection = propInfo.GetValue(target);
|
||||||
|
if (existingCollection is IList existingList)
|
||||||
|
{
|
||||||
|
context.ReadByte(); // consume Array marker
|
||||||
|
PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: read value and set (for primitives, strings, null cases)
|
||||||
|
var positionBeforeRead = context.Position;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// OPTIMIZATION: Use typed setters for primitives to avoid boxing
|
||||||
|
if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||||||
|
propInfo.SetValue(target, value);
|
||||||
|
}
|
||||||
|
catch (InvalidCastException ex)
|
||||||
|
{
|
||||||
|
// Add context about which property and what byte code was at the read position
|
||||||
|
throw new AcBinaryDeserializationException(
|
||||||
|
$"Type mismatch for property '{propInfo.Name}' (index {i}/{propertyCount}, propIndex={propIndex}) on '{targetTypeName}'. " +
|
||||||
|
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
|
||||||
|
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
||||||
|
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
||||||
|
$"Depth: {depth}. " +
|
||||||
|
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
|
||||||
|
$"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
|
||||||
|
$"Error: {ex.Message}",
|
||||||
|
positionBeforeRead,
|
||||||
|
propInfo.PropertyType,
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populate nested object, reusing existing object and recursively updating properties.
|
||||||
|
/// </summary>
|
||||||
|
private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
||||||
|
{
|
||||||
|
var metadata = GetTypeMetadata(targetType);
|
||||||
|
|
||||||
|
// Handle 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 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++)
|
||||||
|
{
|
||||||
|
// Read property index directly - O(1) array lookup
|
||||||
|
var propIndex = (int)context.ReadVarUInt();
|
||||||
|
var propInfo = metadata.GetPropertyByIndex(propIndex);
|
||||||
|
|
||||||
|
if (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Populate List Methods
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// ChainMode: Get IId info for element type
|
||||||
|
var isIId = false;
|
||||||
|
Type? idType = null;
|
||||||
|
Func<object, object?>? idGetter = null;
|
||||||
|
|
||||||
|
if (context.IsChainMode)
|
||||||
|
{
|
||||||
|
var idInfo = GetIdInfo(elementType);
|
||||||
|
isIId = idInfo.IsId;
|
||||||
|
idType = idInfo.IdType;
|
||||||
|
if (isIId && idType != null)
|
||||||
|
{
|
||||||
|
var idProp = elementType.GetProperty("Id");
|
||||||
|
if (idProp != null)
|
||||||
|
idGetter = AcSerializerCommon.CreateCompiledGetter(elementType, idProp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var value = ReadValue(ref context, elementType, nextDepth);
|
||||||
|
|
||||||
|
// ChainMode: Check if we already have this IId object
|
||||||
|
if (context.IsChainMode && value != null && idGetter != null && idType != null)
|
||||||
|
{
|
||||||
|
var id = idGetter(value);
|
||||||
|
if (id != null && !IsDefaultValue(id, idType))
|
||||||
|
{
|
||||||
|
if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
|
||||||
|
{
|
||||||
|
// Use existing object instead of new one
|
||||||
|
targetList.Add(existingObj);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optimized list populate that reuses existing items when possible.
|
||||||
|
/// </summary>
|
||||||
|
private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
|
||||||
|
{
|
||||||
|
var elementType = propInfo.ElementType ?? typeof(object);
|
||||||
|
var count = (int)context.ReadVarUInt();
|
||||||
|
var nextDepth = depth + 1;
|
||||||
|
|
||||||
|
var acObservable = existingList as IAcObservableCollection;
|
||||||
|
acObservable?.BeginUpdate();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existingCount = existingList.Count;
|
||||||
|
var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var peekCode = context.PeekByte();
|
||||||
|
|
||||||
|
// If we have an existing item at this index and the incoming is an object, reuse it
|
||||||
|
if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
|
||||||
|
{
|
||||||
|
var existingItem = existingList[i];
|
||||||
|
if (existingItem != null)
|
||||||
|
{
|
||||||
|
context.ReadByte(); // consume Object marker
|
||||||
|
|
||||||
|
// Handle ref ID if present
|
||||||
|
if (context.HasReferenceHandling)
|
||||||
|
{
|
||||||
|
var refId = context.ReadVarInt();
|
||||||
|
if (refId > 0)
|
||||||
|
{
|
||||||
|
context.RegisterObject(refId, existingItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PopulateObject(ref context, existingItem, elementMetadata, nextDepth);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read new value
|
||||||
|
var value = ReadValue(ref context, elementType, nextDepth);
|
||||||
|
|
||||||
|
if (i < existingCount)
|
||||||
|
{
|
||||||
|
// Replace existing item
|
||||||
|
existingList[i] = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Add new item
|
||||||
|
existingList.Add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove extra items if new list is shorter
|
||||||
|
while (existingList.Count > count)
|
||||||
|
{
|
||||||
|
existingList.RemoveAt(existingList.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
acObservable?.EndUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Merge Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optimized IId collection merge with capacity hints and reduced boxing.
|
||||||
|
/// </summary>
|
||||||
|
private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
|
||||||
|
{
|
||||||
|
var elementType = propInfo.ElementType!;
|
||||||
|
var idGetter = propInfo.ElementIdGetter!;
|
||||||
|
var idType = propInfo.ElementIdType!;
|
||||||
|
|
||||||
|
var count = existingList.Count;
|
||||||
|
var acObservable = existingList as IAcObservableCollection;
|
||||||
|
acObservable?.BeginUpdate();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Build lookup dictionary with capacity hint
|
||||||
|
Dictionary<object, object>? existingById = null;
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
existingById = new Dictionary<object, object>(count);
|
||||||
|
for (var idx = 0; idx < count; idx++)
|
||||||
|
{
|
||||||
|
var item = existingList[idx];
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
var id = idGetter(item);
|
||||||
|
if (id != null && !IsDefaultValue(id, idType))
|
||||||
|
existingById[id] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var arrayCount = (int)context.ReadVarUInt();
|
||||||
|
var nextDepth = depth + 1;
|
||||||
|
var elementMetadata = GetTypeMetadata(elementType);
|
||||||
|
|
||||||
|
// Track which IDs we see in source (for orphan removal)
|
||||||
|
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
|
||||||
|
? new HashSet<object>(arrayCount)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
// Track this ID as seen in source
|
||||||
|
sourceIds?.Add(itemId);
|
||||||
|
|
||||||
|
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
||||||
|
{
|
||||||
|
// Copy properties to existing item
|
||||||
|
CopyProperties(newItem, existingItem, elementMetadata);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existingList.Add(newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove orphaned items (items in destination but not in source)
|
||||||
|
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
||||||
|
{
|
||||||
|
// Find items to remove (those not in sourceIds)
|
||||||
|
var itemsToRemove = new List<object>();
|
||||||
|
foreach (var kvp in existingById)
|
||||||
|
{
|
||||||
|
if (!sourceIds.Contains(kvp.Key))
|
||||||
|
{
|
||||||
|
itemsToRemove.Add(kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove orphaned items
|
||||||
|
foreach (var item in itemsToRemove)
|
||||||
|
{
|
||||||
|
existingList.Remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
acObservable?.EndUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
|
||||||
|
{
|
||||||
|
var props = metadata.PropertiesArray;
|
||||||
|
for (var i = 0; i < props.Length; i++)
|
||||||
|
{
|
||||||
|
var prop = props[i];
|
||||||
|
var value = prop.GetValue(source);
|
||||||
|
if (value != null)
|
||||||
|
prop.SetValue(target, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a type is a complex type (not primitive, string, or simple value type).
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsComplexType(Type type)
|
||||||
|
{
|
||||||
|
if (type.IsPrimitive) return false;
|
||||||
|
if (ReferenceEquals(type, StringType)) return false;
|
||||||
|
if (type.IsEnum) return false;
|
||||||
|
if (ReferenceEquals(type, GuidType)) return false;
|
||||||
|
if (ReferenceEquals(type, DateTimeType)) return false;
|
||||||
|
if (ReferenceEquals(type, DecimalType)) return false;
|
||||||
|
if (ReferenceEquals(type, TimeSpanType)) return false;
|
||||||
|
if (ReferenceEquals(type, DateTimeOffsetType)) return false;
|
||||||
|
if (Nullable.GetUnderlyingType(type) != null) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,28 @@ public static partial class AcBinaryDeserializer
|
||||||
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
||||||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ThreadLocal cache for hot path type metadata lookup.
|
||||||
|
/// Avoids ConcurrentDictionary overhead for frequently accessed types.
|
||||||
|
/// </summary>
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Dictionary<Type, BinaryDeserializeTypeMetadata>? t_deserializeTypeMetadataLocalCache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ThreadLocal cache for type conversion info.
|
||||||
|
/// </summary>
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum local cache size before clearing.
|
||||||
|
/// Clear() is preferred over new Dictionary() because:
|
||||||
|
/// 1. Reuses already allocated internal array (no new allocation)
|
||||||
|
/// 2. Reduces GC pressure
|
||||||
|
/// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxLocalCacheSize = 64;
|
||||||
|
|
||||||
// Type dispatch table for fast ReadValue
|
// Type dispatch table for fast ReadValue
|
||||||
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
|
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
|
||||||
|
|
||||||
|
|
@ -819,14 +841,45 @@ public static partial class AcBinaryDeserializer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets type conversion info with ThreadLocal caching.
|
||||||
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static TypeConversionInfo GetConversionInfo(Type targetType)
|
private static TypeConversionInfo GetConversionInfo(Type targetType)
|
||||||
=> TypeConversionCache.GetOrAdd(targetType, static type =>
|
{
|
||||||
|
// Fast path: check ThreadLocal cache first
|
||||||
|
var localCache = t_typeConversionLocalCache;
|
||||||
|
if (localCache != null && localCache.TryGetValue(targetType, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path
|
||||||
|
return GetConversionInfoSlow(targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static TypeConversionInfo GetConversionInfoSlow(Type targetType)
|
||||||
|
{
|
||||||
|
var info = TypeConversionCache.GetOrAdd(targetType, static type =>
|
||||||
{
|
{
|
||||||
var underlying = Nullable.GetUnderlyingType(type) ?? type;
|
var underlying = Nullable.GetUnderlyingType(type) ?? type;
|
||||||
return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
|
return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get or create local cache
|
||||||
|
var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();
|
||||||
|
|
||||||
|
// Clear when full - reuses internal array, better than new Dictionary()
|
||||||
|
if (localCache.Count >= MaxLocalCacheSize)
|
||||||
|
{
|
||||||
|
localCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
localCache[targetType] = info;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
|
private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
|
||||||
{
|
{
|
||||||
|
|
@ -919,463 +972,6 @@ public static partial class AcBinaryDeserializer
|
||||||
return instance;
|
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;
|
|
||||||
var targetType = target.GetType();
|
|
||||||
var targetTypeName = targetType.Name;
|
|
||||||
|
|
||||||
for (int i = 0; i < propertyCount; i++)
|
|
||||||
{
|
|
||||||
var propertyNameStartPosition = context.Position;
|
|
||||||
string propertyName;
|
|
||||||
int propIndex = -1;
|
|
||||||
if (context.HasMetadata)
|
|
||||||
{
|
|
||||||
propIndex = (int)context.ReadVarUInt();
|
|
||||||
propertyName = context.GetPropertyName(propIndex);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var typeCode = context.ReadByte();
|
|
||||||
if (typeCode == BinaryTypeCode.String)
|
|
||||||
{
|
|
||||||
propertyName = ReadAndInternString(ref context);
|
|
||||||
}
|
|
||||||
else if (typeCode == BinaryTypeCode.StringInterned)
|
|
||||||
{
|
|
||||||
propertyName = context.GetInternedString((int)context.ReadVarUInt());
|
|
||||||
}
|
|
||||||
else if (typeCode == BinaryTypeCode.StringEmpty)
|
|
||||||
{
|
|
||||||
propertyName = string.Empty;
|
|
||||||
}
|
|
||||||
else if (typeCode == BinaryTypeCode.StringInternNew)
|
|
||||||
{
|
|
||||||
propertyName = ReadAndRegisterInternedString(ref context);
|
|
||||||
}
|
|
||||||
else if (BinaryTypeCode.IsFixStr(typeCode))
|
|
||||||
{
|
|
||||||
// FixStr: short string with length encoded in type code
|
|
||||||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
|
||||||
propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new AcBinaryDeserializationException(
|
|
||||||
$"Expected string for property name, got: {typeCode} (0x{typeCode:X2}) at position {propertyNameStartPosition}. " +
|
|
||||||
$"Target: {targetTypeName}, PropertyIndex: {i}/{propertyCount}, Depth: {depth}",
|
|
||||||
context.Position, targetType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
|
|
||||||
{
|
|
||||||
// Skip unknown property
|
|
||||||
SkipValue(ref context);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OPTIMIZATION: Reuse existing nested objects instead of creating new ones
|
|
||||||
var peekCode = context.PeekByte();
|
|
||||||
|
|
||||||
// Handle nested complex objects - reuse existing if available
|
|
||||||
if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
|
|
||||||
{
|
|
||||||
var existingObj = propInfo.GetValue(target);
|
|
||||||
if (existingObj != null)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Object marker
|
|
||||||
PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle collections - reuse existing collection and populate items
|
|
||||||
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
|
||||||
{
|
|
||||||
var existingCollection = propInfo.GetValue(target);
|
|
||||||
if (existingCollection is IList existingList)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Array marker
|
|
||||||
PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: read value and set (for primitives, strings, null cases)
|
|
||||||
var positionBeforeRead = context.Position;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// OPTIMIZATION: Use typed setters for primitives to avoid boxing
|
|
||||||
if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
|
||||||
propInfo.SetValue(target, value);
|
|
||||||
}
|
|
||||||
catch (InvalidCastException ex)
|
|
||||||
{
|
|
||||||
// Add context about which property and what byte code was at the read position
|
|
||||||
throw new AcBinaryDeserializationException(
|
|
||||||
$"Type mismatch for property '{propertyName}' (index {i}/{propertyCount}, headerIndex={propIndex}) on '{targetTypeName}'. " +
|
|
||||||
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
|
|
||||||
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
|
||||||
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
|
||||||
$"Depth: {depth}. " +
|
|
||||||
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
|
|
||||||
$"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
|
|
||||||
$"Error: {ex.Message}",
|
|
||||||
positionBeforeRead,
|
|
||||||
propInfo.PropertyType,
|
|
||||||
ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Populate nested object, reusing existing object and recursively updating properties.
|
|
||||||
/// </summary>
|
|
||||||
private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
|
||||||
{
|
|
||||||
var metadata = GetTypeMetadata(targetType);
|
|
||||||
|
|
||||||
// Handle 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 PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
|
||||||
{
|
|
||||||
var metadata = GetTypeMetadata(targetType);
|
|
||||||
|
|
||||||
// Skip ref ID if present
|
|
||||||
if (context.HasReferenceHandling)
|
|
||||||
{
|
|
||||||
var refId = context.ReadVarInt();
|
|
||||||
if (refId > 0)
|
|
||||||
{
|
|
||||||
context.RegisterObject(refId, target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var propertyCount = (int)context.ReadVarUInt();
|
|
||||||
var nextDepth = depth + 1;
|
|
||||||
|
|
||||||
for (int i = 0; i < propertyCount; i++)
|
|
||||||
{
|
|
||||||
string propertyName;
|
|
||||||
if (context.HasMetadata)
|
|
||||||
{
|
|
||||||
var propIndex = (int)context.ReadVarUInt();
|
|
||||||
propertyName = context.GetPropertyName(propIndex);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var typeCode = context.ReadByte();
|
|
||||||
if (typeCode == BinaryTypeCode.String)
|
|
||||||
{
|
|
||||||
propertyName = ReadAndInternString(ref context);
|
|
||||||
}
|
|
||||||
else if (typeCode == BinaryTypeCode.StringInterned)
|
|
||||||
{
|
|
||||||
propertyName = context.GetInternedString((int)context.ReadVarUInt());
|
|
||||||
}
|
|
||||||
else if (typeCode == BinaryTypeCode.StringEmpty)
|
|
||||||
{
|
|
||||||
propertyName = string.Empty;
|
|
||||||
}
|
|
||||||
else if (typeCode == BinaryTypeCode.StringInternNew)
|
|
||||||
{
|
|
||||||
propertyName = ReadAndRegisterInternedString(ref context);
|
|
||||||
}
|
|
||||||
else if (BinaryTypeCode.IsFixStr(typeCode))
|
|
||||||
{
|
|
||||||
// FixStr: short string with length encoded in type code
|
|
||||||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
|
||||||
propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new AcBinaryDeserializationException(
|
|
||||||
$"Expected string for property name, got: {typeCode} (0x{typeCode:X2})",
|
|
||||||
context.Position, targetType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
|
|
||||||
{
|
|
||||||
SkipValue(ref context);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var peekCode = context.PeekByte();
|
|
||||||
|
|
||||||
// Handle IId collection merge
|
|
||||||
if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array)
|
|
||||||
{
|
|
||||||
var existingCollection = propInfo.GetValue(target);
|
|
||||||
if (existingCollection is IList existingList)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Array marker
|
|
||||||
MergeIIdCollection(ref context, existingList, propInfo, depth);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle nested object merge
|
|
||||||
if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
|
|
||||||
{
|
|
||||||
var existingObj = propInfo.GetValue(target);
|
|
||||||
if (existingObj != null)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Object marker
|
|
||||||
PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
|
||||||
propInfo.SetValue(target, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optimized IId collection merge with capacity hints and reduced boxing.
|
|
||||||
/// </summary>
|
|
||||||
private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
|
|
||||||
{
|
|
||||||
var elementType = propInfo.ElementType!;
|
|
||||||
var idGetter = propInfo.ElementIdGetter!;
|
|
||||||
var idType = propInfo.ElementIdType!;
|
|
||||||
|
|
||||||
var count = existingList.Count;
|
|
||||||
var acObservable = existingList as IAcObservableCollection;
|
|
||||||
acObservable?.BeginUpdate();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Build lookup dictionary with capacity hint
|
|
||||||
Dictionary<object, object>? existingById = null;
|
|
||||||
if (count > 0)
|
|
||||||
{
|
|
||||||
existingById = new Dictionary<object, object>(count);
|
|
||||||
for (var idx = 0; idx < count; idx++)
|
|
||||||
{
|
|
||||||
var item = existingList[idx];
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
var id = idGetter(item);
|
|
||||||
if (id != null && !IsDefaultValue(id, idType))
|
|
||||||
existingById[id] = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var arrayCount = (int)context.ReadVarUInt();
|
|
||||||
var nextDepth = depth + 1;
|
|
||||||
var elementMetadata = GetTypeMetadata(elementType);
|
|
||||||
|
|
||||||
// Track which IDs we see in source (for orphan removal)
|
|
||||||
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
|
|
||||||
? new HashSet<object>(arrayCount)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
// Track this ID as seen in source
|
|
||||||
sourceIds?.Add(itemId);
|
|
||||||
|
|
||||||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
|
||||||
{
|
|
||||||
// Copy properties to existing item
|
|
||||||
CopyProperties(newItem, existingItem, elementMetadata);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
existingList.Add(newItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove orphaned items (items in destination but not in source)
|
|
||||||
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
|
||||||
{
|
|
||||||
// Find items to remove (those not in sourceIds)
|
|
||||||
var itemsToRemove = new List<object>();
|
|
||||||
foreach (var kvp in existingById)
|
|
||||||
{
|
|
||||||
if (!sourceIds.Contains(kvp.Key))
|
|
||||||
{
|
|
||||||
itemsToRemove.Add(kvp.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove orphaned items
|
|
||||||
foreach (var item in itemsToRemove)
|
|
||||||
{
|
|
||||||
existingList.Remove(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
acObservable?.EndUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
|
|
||||||
{
|
|
||||||
var props = metadata.PropertiesArray;
|
|
||||||
for (var i = 0; i < props.Length; i++)
|
|
||||||
{
|
|
||||||
var prop = props[i];
|
|
||||||
var value = prop.GetValue(source);
|
|
||||||
if (value != null)
|
|
||||||
prop.SetValue(target, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if a type is a complex type (not primitive, string, or simple value type).
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static bool IsComplexType(Type type)
|
|
||||||
{
|
|
||||||
if (type.IsPrimitive) return false;
|
|
||||||
if (ReferenceEquals(type, StringType)) return false;
|
|
||||||
if (type.IsEnum) return false;
|
|
||||||
if (ReferenceEquals(type, GuidType)) return false;
|
|
||||||
if (ReferenceEquals(type, DateTimeType)) return false;
|
|
||||||
if (ReferenceEquals(type, DecimalType)) return false;
|
|
||||||
if (ReferenceEquals(type, TimeSpanType)) return false;
|
|
||||||
if (ReferenceEquals(type, DateTimeOffsetType)) return false;
|
|
||||||
if (Nullable.GetUnderlyingType(type) != null) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optimized list populate that reuses existing items when possible.
|
|
||||||
/// </summary>
|
|
||||||
private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
|
|
||||||
{
|
|
||||||
var elementType = propInfo.ElementType ?? typeof(object);
|
|
||||||
var count = (int)context.ReadVarUInt();
|
|
||||||
var nextDepth = depth + 1;
|
|
||||||
|
|
||||||
var acObservable = existingList as IAcObservableCollection;
|
|
||||||
acObservable?.BeginUpdate();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var existingCount = existingList.Count;
|
|
||||||
var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null;
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var peekCode = context.PeekByte();
|
|
||||||
|
|
||||||
// If we have an existing item at this index and the incoming is an object, reuse it
|
|
||||||
if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
|
|
||||||
{
|
|
||||||
var existingItem = existingList[i];
|
|
||||||
if (existingItem != null)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Object marker
|
|
||||||
|
|
||||||
// Handle ref ID if present
|
|
||||||
if (context.HasReferenceHandling)
|
|
||||||
{
|
|
||||||
var refId = context.ReadVarInt();
|
|
||||||
if (refId > 0)
|
|
||||||
{
|
|
||||||
context.RegisterObject(refId, existingItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PopulateObject(ref context, existingItem, elementMetadata, nextDepth);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read new value
|
|
||||||
var value = ReadValue(ref context, elementType, nextDepth);
|
|
||||||
|
|
||||||
if (i < existingCount)
|
|
||||||
{
|
|
||||||
// Replace existing item
|
|
||||||
existingList[i] = value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Add new item
|
|
||||||
existingList.Add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove extra items if new list is shorter
|
|
||||||
while (existingList.Count > count)
|
|
||||||
{
|
|
||||||
existingList.RemoveAt(existingList.Count - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
acObservable?.EndUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Array Reading
|
#region Array Reading
|
||||||
|
|
@ -1557,93 +1153,6 @@ public static partial class AcBinaryDeserializer
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
|
|
||||||
{
|
|
||||||
var elementType = GetCollectionElementType(listType) ?? typeof(object);
|
|
||||||
|
|
||||||
var acObservable = targetList as IAcObservableCollection;
|
|
||||||
acObservable?.BeginUpdate();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
targetList.Clear();
|
|
||||||
|
|
||||||
var count = (int)context.ReadVarUInt();
|
|
||||||
var nextDepth = depth + 1;
|
|
||||||
|
|
||||||
// ChainMode: Get IId info for element type
|
|
||||||
var isIId = false;
|
|
||||||
Type? idType = null;
|
|
||||||
Func<object, object?>? idGetter = null;
|
|
||||||
|
|
||||||
if (context.IsChainMode)
|
|
||||||
{
|
|
||||||
var idInfo = GetIdInfo(elementType);
|
|
||||||
isIId = idInfo.IsId;
|
|
||||||
idType = idInfo.IdType;
|
|
||||||
if (isIId && idType != null)
|
|
||||||
{
|
|
||||||
var idProp = elementType.GetProperty("Id");
|
|
||||||
if (idProp != null)
|
|
||||||
idGetter = AcSerializerCommon.CreateCompiledGetter(elementType, idProp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var value = ReadValue(ref context, elementType, nextDepth);
|
|
||||||
|
|
||||||
// ChainMode: Check if we already have this IId object
|
|
||||||
if (context.IsChainMode && value != null && idGetter != null && idType != null)
|
|
||||||
{
|
|
||||||
var id = idGetter(value);
|
|
||||||
if (id != null && !IsDefaultValue(id, idType))
|
|
||||||
{
|
|
||||||
if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
|
|
||||||
{
|
|
||||||
// Use existing object instead of new one
|
|
||||||
targetList.Add(existingObj);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
#endregion
|
||||||
|
|
||||||
#region Dictionary Reading
|
#region Dictionary Reading
|
||||||
|
|
@ -1830,38 +1339,8 @@ public static partial class AcBinaryDeserializer
|
||||||
var propCount = (int)context.ReadVarUInt();
|
var propCount = (int)context.ReadVarUInt();
|
||||||
for (int i = 0; i < propCount; i++)
|
for (int i = 0; i < propCount; i++)
|
||||||
{
|
{
|
||||||
// Skip property name - but must register in intern table!
|
// Skip property index (VarUInt)
|
||||||
if (context.HasMetadata)
|
context.ReadVarUInt();
|
||||||
{
|
|
||||||
context.ReadVarUInt();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var nameCode = context.ReadByte();
|
|
||||||
if (nameCode == BinaryTypeCode.String)
|
|
||||||
{
|
|
||||||
// CRITICAL FIX: Must register property name in intern table even when skipping!
|
|
||||||
SkipAndInternString(ref context);
|
|
||||||
}
|
|
||||||
else if (nameCode == BinaryTypeCode.StringInterned)
|
|
||||||
{
|
|
||||||
// Just read the index, no registration needed
|
|
||||||
context.ReadVarUInt();
|
|
||||||
}
|
|
||||||
else if (nameCode == BinaryTypeCode.StringInternNew)
|
|
||||||
{
|
|
||||||
// New interned string - must register even when skipping!
|
|
||||||
SkipAndRegisterInternedString(ref context);
|
|
||||||
}
|
|
||||||
else if (BinaryTypeCode.IsFixStr(nameCode))
|
|
||||||
{
|
|
||||||
// FixStr: short string, just skip the bytes
|
|
||||||
var length = BinaryTypeCode.DecodeFixStrLength(nameCode);
|
|
||||||
if (length > 0)
|
|
||||||
context.Skip(length);
|
|
||||||
}
|
|
||||||
// StringEmpty doesn't need any action
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip value
|
// Skip value
|
||||||
SkipValue(ref context);
|
SkipValue(ref context);
|
||||||
|
|
@ -1891,9 +1370,40 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
#region Type Metadata
|
#region Type Metadata
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets type metadata with ThreadLocal caching for hot path optimization.
|
||||||
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
|
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
|
||||||
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
|
{
|
||||||
|
// Fast path: check ThreadLocal cache first
|
||||||
|
var localCache = t_deserializeTypeMetadataLocalCache;
|
||||||
|
if (localCache != null && localCache.TryGetValue(type, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path: get from global cache and populate local cache
|
||||||
|
return GetTypeMetadataSlow(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static BinaryDeserializeTypeMetadata GetTypeMetadataSlow(Type type)
|
||||||
|
{
|
||||||
|
var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
|
||||||
|
|
||||||
|
// Get or create local cache
|
||||||
|
var localCache = t_deserializeTypeMetadataLocalCache ??= new Dictionary<Type, BinaryDeserializeTypeMetadata>();
|
||||||
|
|
||||||
|
// Clear when full - reuses internal array, better than new Dictionary()
|
||||||
|
if (localCache.Count >= MaxLocalCacheSize)
|
||||||
|
{
|
||||||
|
localCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
localCache[type] = metadata;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
|
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
_propertyNameList?.Clear();
|
_propertyNameList?.Clear();
|
||||||
_internedStringList?.Clear();
|
_internedStringList?.Clear();
|
||||||
|
_internedStringUtf8?.Clear();
|
||||||
|
|
||||||
// Reset cached property indices
|
// Reset cached property indices
|
||||||
ResetCachedPropertyIndices();
|
ResetCachedPropertyIndices();
|
||||||
|
|
@ -186,11 +187,17 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#region String Interning
|
#region String Interning
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached UTF8 bytes for interned strings to avoid re-encoding in FinalizeHeaderSections.
|
||||||
|
/// </summary>
|
||||||
|
private List<byte[]>? _internedStringUtf8;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public int RegisterInternedString(string value)
|
public int RegisterInternedString(string value)
|
||||||
{
|
{
|
||||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||||
|
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
|
||||||
|
|
||||||
// Fast path: check bloom filter first
|
// Fast path: check bloom filter first
|
||||||
var hash = GetStringHash(value);
|
var hash = GetStringHash(value);
|
||||||
|
|
@ -200,6 +207,8 @@ public static partial class AcBinarySerializer
|
||||||
var newIndex = _internedStringList.Count;
|
var newIndex = _internedStringList.Count;
|
||||||
_internedStrings[value] = newIndex;
|
_internedStrings[value] = newIndex;
|
||||||
_internedStringList.Add(value);
|
_internedStringList.Add(value);
|
||||||
|
// Cache UTF8 bytes immediately
|
||||||
|
_internedStringUtf8.Add(GetUtf8BytesCached(value));
|
||||||
BloomFilterAdd(hash);
|
BloomFilterAdd(hash);
|
||||||
return newIndex;
|
return newIndex;
|
||||||
}
|
}
|
||||||
|
|
@ -213,10 +222,29 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
index = _internedStringList.Count;
|
index = _internedStringList.Count;
|
||||||
_internedStringList.Add(value);
|
_internedStringList.Add(value);
|
||||||
|
// Cache UTF8 bytes immediately
|
||||||
|
_internedStringUtf8.Add(GetUtf8BytesCached(value));
|
||||||
BloomFilterAdd(hash);
|
BloomFilterAdd(hash);
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get UTF8 bytes for a string, optimized for ASCII strings.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static byte[] GetUtf8BytesCached(string value)
|
||||||
|
{
|
||||||
|
// Fast path for ASCII strings - direct char to byte conversion
|
||||||
|
if (System.Text.Ascii.IsValid(value))
|
||||||
|
{
|
||||||
|
var bytes = new byte[value.Length];
|
||||||
|
System.Text.Ascii.FromUtf16(value.AsSpan(), bytes, out _);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
// Standard path for multi-byte UTF8
|
||||||
|
return Utf8NoBom.GetBytes(value);
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static int GetStringHash(string value)
|
private static int GetStringHash(string value)
|
||||||
{
|
{
|
||||||
|
|
@ -989,14 +1017,27 @@ public static partial class AcBinarySerializer
|
||||||
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
|
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
|
||||||
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
|
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
|
||||||
|
|
||||||
// Calculate actual header size first
|
// Fast path: no header payload needed
|
||||||
|
if (!hasPropertyNames && !hasInternTable)
|
||||||
|
{
|
||||||
|
// Write header flags only
|
||||||
|
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
||||||
|
if (UseReferenceHandling)
|
||||||
|
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
||||||
|
|
||||||
|
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||||
|
_buffer[_headerPosition + 1] = flags;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual header size using cached UTF8 bytes
|
||||||
var actualSize = 0;
|
var actualSize = 0;
|
||||||
if (hasPropertyNames)
|
if (hasPropertyNames)
|
||||||
{
|
{
|
||||||
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
|
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
|
||||||
foreach (var name in _propertyNameList)
|
foreach (var name in _propertyNameList)
|
||||||
{
|
{
|
||||||
var byteCount = Utf8NoBom.GetByteCount(name);
|
var byteCount = System.Text.Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
|
||||||
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1004,10 +1045,11 @@ public static partial class AcBinarySerializer
|
||||||
if (hasInternTable)
|
if (hasInternTable)
|
||||||
{
|
{
|
||||||
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
|
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
|
||||||
foreach (var value in _internedStringList)
|
// Use cached UTF8 byte lengths
|
||||||
|
for (var i = 0; i < _internedStringUtf8!.Count; i++)
|
||||||
{
|
{
|
||||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
var utf8Bytes = _internedStringUtf8[i];
|
||||||
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
actualSize += GetVarUIntSize((uint)utf8Bytes.Length) + utf8Bytes.Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1031,7 +1073,7 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write header payload directly to buffer (no ArrayBufferWriter allocation)
|
// Write header payload directly to buffer using cached UTF8 bytes
|
||||||
var headerPos = _headerPosition + 2;
|
var headerPos = _headerPosition + 2;
|
||||||
|
|
||||||
if (hasPropertyNames)
|
if (hasPropertyNames)
|
||||||
|
|
@ -1039,30 +1081,54 @@ public static partial class AcBinarySerializer
|
||||||
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
|
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
|
||||||
foreach (var name in _propertyNameList)
|
foreach (var name in _propertyNameList)
|
||||||
{
|
{
|
||||||
headerPos = WriteStringAt(headerPos, name);
|
headerPos = WriteStringAtOptimized(headerPos, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasInternTable)
|
if (hasInternTable)
|
||||||
{
|
{
|
||||||
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
|
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
|
||||||
foreach (var value in _internedStringList)
|
// Use cached UTF8 bytes - no re-encoding needed!
|
||||||
|
for (var i = 0; i < _internedStringUtf8!.Count; i++)
|
||||||
{
|
{
|
||||||
headerPos = WriteStringAt(headerPos, value);
|
var utf8Bytes = _internedStringUtf8[i];
|
||||||
|
headerPos = WriteVarUIntAt(headerPos, (uint)utf8Bytes.Length);
|
||||||
|
utf8Bytes.CopyTo(_buffer.AsSpan(headerPos));
|
||||||
|
headerPos += utf8Bytes.Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write header flags
|
// Write header flags
|
||||||
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
byte flags2 = BinaryTypeCode.HeaderFlagsBase;
|
||||||
if (hasPropertyNames)
|
if (hasPropertyNames)
|
||||||
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
flags2 |= BinaryTypeCode.HeaderFlag_Metadata;
|
||||||
if (UseReferenceHandling)
|
if (UseReferenceHandling)
|
||||||
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
flags2 |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
||||||
if (hasInternTable)
|
if (hasInternTable)
|
||||||
flags |= BinaryTypeCode.HeaderFlag_StringInternTable;
|
flags2 |= BinaryTypeCode.HeaderFlag_StringInternTable;
|
||||||
|
|
||||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||||
_buffer[_headerPosition + 1] = flags;
|
_buffer[_headerPosition + 1] = flags2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes UTF8 string at specific position, optimized for ASCII strings.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private int WriteStringAtOptimized(int pos, string value)
|
||||||
|
{
|
||||||
|
// Fast path for ASCII strings
|
||||||
|
if (System.Text.Ascii.IsValid(value))
|
||||||
|
{
|
||||||
|
pos = WriteVarUIntAt(pos, (uint)value.Length);
|
||||||
|
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _);
|
||||||
|
return pos + value.Length;
|
||||||
|
}
|
||||||
|
// Standard path for multi-byte UTF8
|
||||||
|
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||||
|
pos = WriteVarUIntAt(pos, (uint)byteCount);
|
||||||
|
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount));
|
||||||
|
return pos + byteCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1080,20 +1146,6 @@ public static partial class AcBinarySerializer
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes UTF8 string at specific position (length-prefixed) and returns new position.
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private int WriteStringAt(int pos, string value)
|
|
||||||
{
|
|
||||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
|
||||||
pos = WriteVarUIntAt(pos, (uint)byteCount);
|
|
||||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount));
|
|
||||||
return pos + byteCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove old methods: WriteHeaderVarUInt, WriteHeaderString (no longer needed)
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Reference Handling
|
#region Reference Handling
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,27 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using AyCode.Core.Serializers;
|
||||||
using System.Text;
|
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
public static partial class AcBinarySerializer
|
public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
internal sealed class BinaryTypeMetadata : TypeMetadataBase
|
internal sealed class BinaryTypeMetadata : BinaryTypeMetadataBase
|
||||||
{
|
{
|
||||||
public BinaryPropertyAccessor[] Properties { get; }
|
public BinaryPropertyAccessor[] Properties { get; }
|
||||||
|
|
||||||
public BinaryTypeMetadata(Type type) : base(type)
|
public BinaryTypeMetadata(Type type) : base(type)
|
||||||
{
|
{
|
||||||
Properties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false)
|
var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false);
|
||||||
.Select(p => new BinaryPropertyAccessor(p, type))
|
|
||||||
.ToArray();
|
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
||||||
|
for (var i = 0; i < orderedProperties.Length; i++)
|
||||||
|
{
|
||||||
|
var accessor = new BinaryPropertyAccessor(orderedProperties[i], type);
|
||||||
|
accessor.PropertyIndex = i;
|
||||||
|
Properties[i] = accessor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,22 @@ public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> TypeMetadataCache = new();
|
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> TypeMetadataCache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ThreadLocal cache for hot path type metadata lookup.
|
||||||
|
/// Avoids ConcurrentDictionary overhead for frequently accessed types.
|
||||||
|
/// </summary>
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Dictionary<Type, BinaryTypeMetadata>? t_typeMetadataLocalCache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum local cache size before clearing.
|
||||||
|
/// Clear() is preferred over new Dictionary() because:
|
||||||
|
/// 1. Reuses already allocated internal array (no new allocation)
|
||||||
|
/// 2. Reduces GC pressure
|
||||||
|
/// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxLocalCacheSize = 64;
|
||||||
|
|
||||||
// Pre-computed UTF8 encoder for string operations
|
// Pre-computed UTF8 encoder for string operations
|
||||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||||
private static readonly Type StringType = typeof(string);
|
private static readonly Type StringType = typeof(string);
|
||||||
|
|
@ -190,10 +206,8 @@ public static partial class AcBinarySerializer
|
||||||
ScanReferences(value, context, 0);
|
ScanReferences(value, context, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType))
|
// Property index-based serialization - no metadata registration needed!
|
||||||
{
|
// PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms
|
||||||
RegisterMetadataForType(runtimeType, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Estimate and reserve header space to avoid body shift later
|
// Estimate and reserve header space to avoid body shift later
|
||||||
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
|
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
|
||||||
|
|
@ -387,7 +401,7 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optimized primitive writer using TypeCode dispatch.
|
/// Optimized primitive writer using TypeCode dispatch.
|
||||||
/// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata.
|
/// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context)
|
private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context)
|
||||||
|
|
@ -446,13 +460,6 @@ public static partial class AcBinarySerializer
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle nullable types
|
|
||||||
var underlyingType = Nullable.GetUnderlyingType(type);
|
|
||||||
if (underlyingType != null)
|
|
||||||
{
|
|
||||||
return TryWritePrimitive(value, underlyingType, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle special types by reference comparison (faster than type equality)
|
// Handle special types by reference comparison (faster than type equality)
|
||||||
if (ReferenceEquals(type, GuidType))
|
if (ReferenceEquals(type, GuidType))
|
||||||
{
|
{
|
||||||
|
|
@ -475,6 +482,15 @@ public static partial class AcBinarySerializer
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,10 +623,10 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void WriteEnum(object value, BinarySerializationContext context
|
private static void WriteEnum(object value, BinarySerializationContext context)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var intValue = Convert.ToInt32(value);
|
// Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead
|
||||||
|
var intValue = GetEnumAsInt32Fast(value);
|
||||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||||
{
|
{
|
||||||
context.WriteByte(BinaryTypeCode.Enum);
|
context.WriteByte(BinaryTypeCode.Enum);
|
||||||
|
|
@ -622,6 +638,33 @@ public static partial class AcBinarySerializer
|
||||||
context.WriteVarInt(intValue);
|
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>
|
/// <summary>
|
||||||
/// Optimized string writer with FixStr for short strings.
|
/// Optimized string writer with FixStr for short strings.
|
||||||
/// Uses stackalloc for small strings to avoid allocations.
|
/// Uses stackalloc for small strings to avoid allocations.
|
||||||
|
|
@ -706,18 +749,10 @@ public static partial class AcBinarySerializer
|
||||||
if (IsPropertyDefaultOrNull(value, prop))
|
if (IsPropertyDefaultOrNull(value, prop))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Write property name/index
|
// Write property index directly - no dictionary lookup needed!
|
||||||
if (context.UseMetadata)
|
// PropertyIndex is deterministic (based on alphabetical ordering)
|
||||||
{
|
// and consistent across all platforms (WASM, iOS, Android, Windows)
|
||||||
var propIndex = prop.CachedPropertyNameIndex >= 0
|
context.WriteVarUInt((uint)prop.PropertyIndex);
|
||||||
? prop.CachedPropertyNameIndex
|
|
||||||
: context.GetPropertyNameIndex(prop.Name);
|
|
||||||
context.WriteVarUInt((uint)propIndex);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.WritePreencodedPropertyName(prop.NameUtf8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write property value
|
// Write property value
|
||||||
WritePropertyValue(value, prop, context, nextDepth);
|
WritePropertyValue(value, prop, context, nextDepth);
|
||||||
|
|
@ -1059,9 +1094,41 @@ public static partial class AcBinarySerializer
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets type metadata with ThreadLocal caching for hot path optimization.
|
||||||
|
/// Falls back to ConcurrentDictionary for cache misses.
|
||||||
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static BinaryTypeMetadata GetTypeMetadata(Type type)
|
private static BinaryTypeMetadata GetTypeMetadata(Type type)
|
||||||
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
|
{
|
||||||
|
// Fast path: check ThreadLocal cache first
|
||||||
|
var localCache = t_typeMetadataLocalCache;
|
||||||
|
if (localCache != null && localCache.TryGetValue(type, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path: get from global cache and populate local cache
|
||||||
|
return GetTypeMetadataSlow(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static BinaryTypeMetadata GetTypeMetadataSlow(Type type)
|
||||||
|
{
|
||||||
|
var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
|
||||||
|
|
||||||
|
// Get or create local cache
|
||||||
|
var localCache = t_typeMetadataLocalCache ??= new Dictionary<Type, BinaryTypeMetadata>();
|
||||||
|
|
||||||
|
// Clear when full - reuses internal array, better than new Dictionary()
|
||||||
|
if (localCache.Count >= MaxLocalCacheSize)
|
||||||
|
{
|
||||||
|
localCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
localCache[type] = metadata;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
// Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs
|
// Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal int CachedPropertyNameIndex = -1;
|
internal int CachedPropertyNameIndex = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deterministic property index based on alphabetical ordering of property names.
|
||||||
|
/// This is computed once during metadata creation and is consistent across all platforms.
|
||||||
|
/// Used for fast serialization without dictionary lookup.
|
||||||
|
/// </summary>
|
||||||
|
public int PropertyIndex { get; internal set; } = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The accessor type for fast typed getter dispatch.
|
/// The accessor type for fast typed getter dispatch.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for binary serializer/deserializer type metadata.
|
||||||
|
/// Provides common functionality for IId detection and property ordering.
|
||||||
|
/// Properties are ordered alphabetically by name for deterministic serialization across platforms.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BinaryTypeMetadataBase : TypeMetadataBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this type implements IId interface.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsIId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Id property type if IsIId is true.
|
||||||
|
/// </summary>
|
||||||
|
public Type? IdType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compiled getter for the Id property (if IsIId is true).
|
||||||
|
/// </summary>
|
||||||
|
public Func<object, object?>? IdGetter { get; }
|
||||||
|
|
||||||
|
protected BinaryTypeMetadataBase(Type type) : base(type)
|
||||||
|
{
|
||||||
|
var (isIId, idType) = GetIdInfo(type);
|
||||||
|
IsIId = isIId;
|
||||||
|
IdType = idType;
|
||||||
|
|
||||||
|
if (isIId)
|
||||||
|
{
|
||||||
|
var idProp = type.GetProperty("Id");
|
||||||
|
if (idProp != null)
|
||||||
|
IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
|
|
@ -9,6 +10,13 @@ namespace AyCode.Core.Serializers;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class TypeMetadataBase
|
public abstract class TypeMetadataBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cache for serializable properties per type.
|
||||||
|
/// Key: (Type, requiresWrite) - since requiresRead is always true in practice.
|
||||||
|
/// Value: PropertyInfo[] ordered alphabetically by name for deterministic serialization.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ConcurrentDictionary<(Type, bool), PropertyInfo[]> OrderedPropertiesCache = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compiled parameterless constructor for the type.
|
/// Compiled parameterless constructor for the type.
|
||||||
/// Null if the type is abstract or has no parameterless constructor.
|
/// Null if the type is abstract or has no parameterless constructor.
|
||||||
|
|
@ -21,21 +29,29 @@ public abstract class TypeMetadataBase
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the properties that should be serialized for a type.
|
/// Gets serializable properties ordered alphabetically by name.
|
||||||
|
/// Results are cached per type and requiresWrite combination.
|
||||||
|
/// Ordering ensures deterministic serialization across all platforms (Windows, WASM, iOS, Android).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The type to analyze.</param>
|
/// <param name="type">The type to analyze.</param>
|
||||||
/// <param name="requiresRead">Whether the property must be readable.</param>
|
/// <param name="requiresRead">Whether the property must be readable (always true in practice).</param>
|
||||||
/// <param name="requiresWrite">Whether the property must be writable.</param>
|
/// <param name="requiresWrite">Whether the property must be writable (true for deserialization).</param>
|
||||||
/// <returns>Enumerable of properties that meet the criteria.</returns>
|
/// <returns>Array of properties ordered by name (cached).</returns>
|
||||||
protected static IEnumerable<PropertyInfo> GetSerializableProperties(
|
protected static PropertyInfo[] GetSerializableProperties(
|
||||||
Type type,
|
Type type,
|
||||||
bool requiresRead = true,
|
bool requiresRead = true,
|
||||||
bool requiresWrite = false)
|
bool requiresWrite = false)
|
||||||
{
|
{
|
||||||
return type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
return OrderedPropertiesCache.GetOrAdd((type, requiresWrite), static key =>
|
||||||
.Where(p => (!requiresRead || p.CanRead) &&
|
{
|
||||||
(!requiresWrite || p.CanWrite) &&
|
var (t, needsWrite) = key;
|
||||||
p.GetIndexParameters().Length == 0 &&
|
return t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
!HasJsonIgnoreAttribute(p));
|
.Where(p => p.CanRead &&
|
||||||
|
(!needsWrite || p.CanWrite) &&
|
||||||
|
p.GetIndexParameters().Length == 0 &&
|
||||||
|
!HasJsonIgnoreAttribute(p))
|
||||||
|
.OrderBy(static p => p.Name, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue