Refactor reference tracking to use per-type identity maps

Replaces flat object reference dictionary with per-type identity maps in deserializer, improving type safety and efficiency for IId types. TypeMetadataWrapper now uses cached typed delegates for reference ID access. Centralizes complex type detection and exposes IsComplexType and TypedIdGetter in metadata. Updates all registration and lookup logic to use wrappers, removes obsolete metadata cache, and ensures thread-safe, type-aware reference handling throughout. Comments out legacy code for easier review and rollback.
This commit is contained in:
Loretta 2026-01-20 07:23:02 +01:00
parent dc2526da7e
commit 6dbe4d76c1
8 changed files with 224 additions and 147 deletions

View File

@ -643,7 +643,11 @@ public static class AcSerializerCommon
{
return _tracked.TryAdd(key, null);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasKey(TId key) => _tracked.ContainsKey(key);
/// <summary>
/// Checks if key exists and returns existing value, or adds new key (deserialization).
/// Returns true if first occurrence (key was added, out = null).
@ -669,7 +673,7 @@ public static class AcSerializerCommon
slot = newValue;
return newValue;
}
/// <summary>
/// Tries to get the value for a key (ObjectRef lookup).
/// Returns true if found, false if not.

View File

@ -53,7 +53,7 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
{
// Get metadata from global cache (thread-safe)
var metadata = GlobalMetadataCache.GetOrAdd(type, MetadataFactory);
// Create wrapper with metadata + tracking state (per-context)
var wrapper = new TypeMetadataWrapper<TMetadata>(metadata);
_wrappers[type] = wrapper;
@ -64,6 +64,45 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
#region Tracking API - int
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, int refId, out object? instance)
{
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<int>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<int>();
wrapper.IdentityMap = map;
}
return map.TryGetValue(refId, out instance);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, long refId, out object? instance)
{
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<long>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<long>();
wrapper.IdentityMap = map;
}
return map.TryGetValue(refId, out instance);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, Guid refId, out object? instance)
{
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<Guid>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<Guid>();
wrapper.IdentityMap = map;
}
return map.TryGetValue(refId, out instance);
}
/// <summary>
/// Tries to track an object with int RefId.
/// Use when wrapper.Metadata.IdAccessorType == Int32.
@ -78,10 +117,10 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
refId = getter(obj);
// BitArray fast path for small positive IDs
if (refId >= 0 && refId < MaxSmallId)
{
return TryTrackSmallId(wrapper, refId);
}
//if (refId >= 0 && refId < MaxSmallId)
//{
// return TryTrackSmallId(wrapper, refId);
//}
// IdentityMap for large/negative IDs
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<int>;

View File

@ -19,7 +19,7 @@ public static partial class AcBinaryDeserializer
private int _position;
private List<string>? _internedStrings;
private List<string>? _propertyNames;
private Dictionary<int, object>? _objectReferences;
//private Dictionary<int, object>? _objectReferences;
private Dictionary<int, string>? _stringCache;
private readonly byte _minStringInternLength;
private readonly bool _useStringCaching;
@ -66,7 +66,7 @@ public static partial class AcBinaryDeserializer
_position = 0;
_internedStrings = null;
_propertyNames = null;
_objectReferences = null;
//_objectReferences = null;
_stringCache = null;
HasMetadata = false;
HasReferenceHandling = false;
@ -522,33 +522,35 @@ public static partial class AcBinaryDeserializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(int refId, object instance)
public void RegisterObject(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int refId, object instance)
{
if (refId <= 0)
{
return;
}
if (refId == 0) throw new Exception("refId == 0");
ContextClass.TryGetOrStoreInt32(wrapper, instance, refId);
//if (refId <= 0)
//{
// return;
//}
_objectReferences ??= new Dictionary<int, object>(16);
_objectReferences[refId] = instance;
//_objectReferences ??= new Dictionary<int, object>(16);
//_objectReferences[refId] = instance;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetReferencedObject(int refId)
{
if (refId <= 0)
{
return null;
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public object? GetReferencedObject(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int refId)
//{
// //if (refId <= 0)
// //{
// // return null;
// //}
if (_objectReferences == null || !_objectReferences.TryGetValue(refId, out var value))
{
throw new AcBinaryDeserializationException($"Unknown object reference id '{refId}'.", _position);
}
// //if (_objectReferences == null || !_objectReferences.TryGetValue(refId, out var value))
// //{
// // throw new AcBinaryDeserializationException($"Unknown object reference id '{refId}'.", _position);
// //}
return value;
}
// //return value;
//}
private void EnsureAvailable(int length)
{
@ -563,20 +565,20 @@ public static partial class AcBinaryDeserializer
var byteLength = (int)ReadVarUInt();
return ReadStringUtf8(byteLength);
}
#region IId Reference Cache - Delegates to ContextClass
/// <summary>
/// After PopulateObject, checks if we should reuse an existing IId object.
/// Delegates to ContextClass which uses AcSerializerContextBase infrastructure.
/// Returns the object to use (either the new one or an existing cached one).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata)
{
return ContextClass.GetOrRegisterIIdObject(newObj, metadata);
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public object GetOrRegisterIIdObject(object newObj, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
//{
// return ContextClass.GetOrRegisterIIdObject(newObj, wrapper);
//}
#endregion
}
}

View File

@ -215,23 +215,20 @@ public static partial class AcBinaryDeserializer
/// </summary>
private static object? ReadObjectWithMapping(ref BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth)
{
var metadata = GetTypeMetadata(destType);
var wrapper = context.ContextClass.GetWrapper(destType);
var metadata = wrapper.Metadata;
// Handle reference ID if present
if (context.HasReferenceHandling)
{
var refId = context.ReadVarInt();
if (refId < 0)
{
// Object reference - get existing object
return context.GetReferencedObject(-refId);
}
if (context.ContextClass.TryGetValue(wrapper, refId, out var instance)) return instance;
// New object with ID - will be registered below
var instance = CreateInstance(destType, metadata);
instance = CreateInstance(destType, metadata);
if (instance != null && refId > 0)
{
context.RegisterObject(refId, instance);
context.RegisterObject(wrapper, refId, instance);
}
PopulateObjectWithMapping(ref context, instance!, destType, indexMapping, depth);
@ -258,7 +255,8 @@ public static partial class AcBinaryDeserializer
int[] indexMapping,
int depth)
{
var metadata = GetTypeMetadata(destType);
var wrapper = context.ContextClass.GetWrapper(destType);
var metadata = wrapper.Metadata;
var propertyCount = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
@ -286,7 +284,7 @@ public static partial class AcBinaryDeserializer
}
// Reuse common populate logic
PopulatePropertyValue(ref context, target, propInfo, nextDepth, sourcePropIndex, destPropIndex, i, propertyCount, depth);
PopulatePropertyValue(ref context, target, propInfo, wrapper, nextDepth, sourcePropIndex, destPropIndex, i, propertyCount, depth);
}
}
@ -311,6 +309,7 @@ public static partial class AcBinaryDeserializer
ref BinaryDeserializationContext context,
object target,
BinaryPropertySetterInfo propInfo,
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
int nextDepth,
int sourcePropIndex,
int destPropIndex,
@ -342,11 +341,11 @@ public static partial class AcBinaryDeserializer
var refId = context.ReadVarInt();
if (refId > 0)
{
context.RegisterObject(refId, existingObj);
context.RegisterObject(wrapper, refId, existingObj);
}
}
PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false);
PopulateObjectCore(ref context, existingObj, wrapper.Metadata, nextDepth, skipDefaultWrite: false);
return;
}
}

View File

@ -12,13 +12,18 @@ public static partial class AcBinaryDeserializer
{
#region Populate Object Methods
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
=> null;//MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
/// <summary>
/// Populate object with automatic mode detection from context.
/// Uses IsMergeMode to determine merge behavior for IId collections.
/// </summary>
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{
var metadata = GetTypeMetadata(targetType);
var wrapper = context.ContextClass.GetWrapper(targetType);
var metadata = wrapper.Metadata;
// Handle ref ID if present
if (context.HasReferenceHandling)
@ -26,7 +31,7 @@ public static partial class AcBinaryDeserializer
var refId = context.ReadVarInt();
if (refId > 0)
{
context.RegisterObject(refId, target);
context.RegisterObject(wrapper, refId, target);
}
}
@ -105,6 +110,8 @@ public static partial class AcBinaryDeserializer
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
context.ReadByte(); // consume Object marker
// Handle ref ID if present
@ -113,12 +120,12 @@ public static partial class AcBinaryDeserializer
var refId = context.ReadVarInt();
if (refId > 0)
{
context.RegisterObject(refId, existingObj);
context.RegisterObject(wrapper, refId, existingObj);
}
}
// Recursively populate - existing object, don't skip defaults
PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false);
PopulateObjectCore(ref context, existingObj, wrapper.Metadata, nextDepth, skipDefaultWrite: false);
continue;
}
}
@ -207,8 +214,10 @@ public static partial class AcBinaryDeserializer
try
{
var wrapper = context.ContextClass.GetWrapper(elementType);
var existingCount = existingList.Count;
var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null;
var elementMetadata = wrapper.Metadata.IsComplexType ? wrapper.Metadata : null;
for (int i = 0; i < count; i++)
{
@ -228,7 +237,7 @@ public static partial class AcBinaryDeserializer
var refId = context.ReadVarInt();
if (refId > 0)
{
context.RegisterObject(refId, existingItem);
context.RegisterObject(wrapper, refId, existingItem);
}
}
@ -302,7 +311,8 @@ public static partial class AcBinaryDeserializer
var arrayCount = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
var elementMetadata = GetTypeMetadata(elementType);
var wrapper = context.ContextClass.GetWrapper(elementType);
var elementMetadata = wrapper.Metadata;
// Track which IDs we see in source (for orphan removal)
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
@ -329,7 +339,7 @@ public static partial class AcBinaryDeserializer
{
var refId = context.ReadVarInt();
if (refId > 0)
context.RegisterObject(refId, newItem);
context.RegisterObject(wrapper, refId, newItem);
}
// Deserialize mode: new item just created, skip writing defaults
@ -383,9 +393,10 @@ public static partial class AcBinaryDeserializer
ref BinaryDeserializationContext context,
IList existingList,
Type elementType,
BinaryDeserializeTypeMetadata elementMetadata,
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
int depth)
{
var elementMetadata = wrapper.Metadata;
var idGetter = elementMetadata.IdGetter!;
var idType = elementMetadata.IdType!;
@ -440,7 +451,7 @@ public static partial class AcBinaryDeserializer
{
var refId = context.ReadVarInt();
if (refId > 0)
context.RegisterObject(refId, newItem);
context.RegisterObject(wrapper, refId, newItem);
}
// Deserialize mode: new item just created, skip writing defaults
@ -546,7 +557,7 @@ public static partial class AcBinaryDeserializer
/// 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)
private static bool IsComplexType5(Type type)
{
if (type.IsPrimitive) return false;
if (ReferenceEquals(type, StringType)) return false;

View File

@ -325,15 +325,18 @@ public static partial class AcBinaryDeserializer
context.ReadByte();
// For top-level list merge, check if it's an IId collection
var elementType = GetCollectionElementType(targetType);
if (elementType != null && IsComplexType(elementType))
if (elementType != null)
{
var elementMetadata = GetTypeMetadata(elementType);
if (elementMetadata.IsIId && elementMetadata.IdGetter != null)
var wrapper = context.ContextClass.GetWrapper(elementType);
var elementMetadata = wrapper.Metadata;
if (elementMetadata.IsComplexType && elementMetadata.IsIId && elementMetadata.IdGetter != null)
{
MergeIIdCollectionWithMetadata(ref context, targetList, elementType, elementMetadata, 0);
MergeIIdCollectionWithMetadata(ref context, targetList, elementType, wrapper, 0);
return;
}
}
// Non-IId collection, just populate
PopulateList(ref context, targetList, targetType, 0);
}
@ -909,11 +912,11 @@ public static partial class AcBinaryDeserializer
/// </summary>
private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth)
{
var metadata = GetTypeMetadata(targetType);
var wrapper = context.ContextClass.GetWrapper(targetType);
var metadata = wrapper.Metadata;
if (metadata.IsIId)
if (metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None)
{
var wrapper = context.ContextClass.GetWrapper(targetType);
var map = wrapper.IdentityMap;
// Read ID based on type and lookup in IdentityMap
@ -922,12 +925,11 @@ public static partial class AcBinaryDeserializer
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, map),
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, map),
AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, map),
_ => context.GetReferencedObject(context.ReadVarInt())
_ => throw new Exception("metadata.IdAccessorType not valid")
};
}
// Non-IId: sequential int refId
return context.GetReferencedObject(context.ReadVarInt());
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -936,7 +938,7 @@ public static partial class AcBinaryDeserializer
var id = context.ReadVarInt();
if (map is AcSerializerCommon.IdentityMap<int> intMap && intMap.TryGetValue(id, out var obj))
return obj;
return context.GetReferencedObject(id);
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -959,36 +961,53 @@ public static partial class AcBinaryDeserializer
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth)
{
var metadata = GetTypeMetadata(targetType);
// Read reference ID based on IdAccessorType (different wire format for each type)
object? readId = null;
if (context.HasReferenceHandling)
{
readId = metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => context.ReadVarInt(),
AcSerializerCommon.IdAccessorType.Int64 => context.ReadVarLong(),
AcSerializerCommon.IdAccessorType.Guid => context.ReadGuidUnsafe(),
_ => throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}")
};
}
// Handle dictionary types
if (IsDictionaryType(targetType, out var keyType, out var valueType))
{
return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth);
}
var wrapper = context.ContextClass.GetWrapper(targetType);
var metadata = wrapper.Metadata;
// Create instance
var instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
object? instance = null;
// Register reference for flat lookup (int refId only)
if (readId is int intRefId)
if (context.HasReferenceHandling && metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None)
{
if (intRefId == 0) throw new Exception("intRefId == 0");
context.RegisterObject(intRefId, instance);
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
var intId = context.ReadVarInt();
if (context.ContextClass.TryGetValue(wrapper, intId, out instance)) return instance;
instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, intId);
break;
case AcSerializerCommon.IdAccessorType.Int64:
var longId = context.ReadVarLong();
if (context.ContextClass.TryGetValue(wrapper, longId, out instance)) return instance;
instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
context.ContextClass.TryGetOrStoreLong(wrapper, instance, longId);
break;
case AcSerializerCommon.IdAccessorType.Guid:
var guidId = context.ReadGuidUnsafe();
if (context.ContextClass.TryGetValue(wrapper, guidId, out instance)) return instance;
instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
context.ContextClass.TryGetOrStoreGuid(wrapper, instance, guidId);
break;
default:
throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}");
}
}
else
{
instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
}
// Deserialize mode: object just created, skip writing defaults (already at default)
@ -998,14 +1017,23 @@ public static partial class AcBinaryDeserializer
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
{
object? id = metadata.IdAccessorType switch
object? id;
switch (metadata.IdAccessorType)
{
AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance),
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance),
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance),
_ => null
};
case AcSerializerCommon.IdAccessorType.Int32:
id = metadata.GetIdInt32(instance);
break;
case AcSerializerCommon.IdAccessorType.Int64:
id = metadata.GetIdInt64(instance);
break;
case AcSerializerCommon.IdAccessorType.Guid:
id = metadata.GetIdGuid(instance);
break;
default:
id = null;
break;
}
if (id != null && !IsDefaultValue(id, metadata.IdType))
{
// Check if we already have this object
@ -1020,11 +1048,6 @@ public static partial class AcBinaryDeserializer
context.ChainTracker.TryRegisterIIdObject(instance);
}
}
// Normal IId cache for non-chain deserialization
else if (context.HasReferenceHandling && metadata.IsIId)
{
instance = context.GetOrRegisterIIdObject(instance, metadata);
}
return instance;
}
@ -1424,13 +1447,6 @@ public static partial class AcBinaryDeserializer
#region Type Metadata
// Temporary: own cache until ref struct is removed
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> MetadataCache = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
=> MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
{

View File

@ -102,8 +102,9 @@ public abstract class TypeMetadataBase
/// <summary>
/// Typed getter delegate for IId.Id property.
/// Type depends on IdAccessorType (Func&lt;object, int&gt;, Func&lt;object, long&gt;, or Func&lt;object, Guid&gt;).
/// Cached here to avoid creating new delegates per wrapper.
/// </summary>
private readonly Delegate? _typedIdGetter;
public Delegate? TypedIdGetter { get; }
#region Scan Optimization Flags
@ -112,7 +113,9 @@ public abstract class TypeMetadataBase
/// If false, ScanReferences can skip child property scanning entirely.
/// </summary>
public bool HasComplexProperties { get; protected set; }
public bool IsComplexType { get; init; }
/// <summary>
/// True if this type or any of its properties could potentially be shared (multi-referenced).
/// False for sealed value-like types with only primitives - these never need reference tracking.
@ -125,19 +128,19 @@ public abstract class TypeMetadataBase
/// Gets the Id as int without boxing. Only valid when IdAccessorType == Int32.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetIdInt32(object obj) => ((Func<object, int>)_typedIdGetter!)(obj);
public int GetIdInt32(object obj) => ((Func<object, int>)TypedIdGetter!)(obj);
/// <summary>
/// Gets the Id as long without boxing. Only valid when IdAccessorType == Int64.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetIdInt64(object obj) => ((Func<object, long>)_typedIdGetter!)(obj);
public long GetIdInt64(object obj) => ((Func<object, long>)TypedIdGetter!)(obj);
/// <summary>
/// Gets the Id as Guid without boxing. Only valid when IdAccessorType == Guid.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid GetIdGuid(object obj) => ((Func<object, Guid>)_typedIdGetter!)(obj);
public Guid GetIdGuid(object obj) => ((Func<object, Guid>)TypedIdGetter!)(obj);
protected TypeMetadataBase(Type type, Func<PropertyInfo, bool> ignorePropertyFilter)
{
@ -153,11 +156,14 @@ public abstract class TypeMetadataBase
ReadableProperties = allReadable;
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
IsComplexType = IsComplexType2(type);
// Cache IId info at construction time - no runtime reflection needed later!
var idInfo = GetIdInfo(type);
IsIId = idInfo.IsId;
IdType = idInfo.IdType;
if (IsIId)
{
var idProp = type.GetProperty("Id");
@ -167,17 +173,17 @@ public abstract class TypeMetadataBase
if (ReferenceEquals(IdType, IntType))
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
TypedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
}
else if (ReferenceEquals(IdType, LongType))
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Int64;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
TypedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
}
else if (ReferenceEquals(IdType, GuidType))
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Guid;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
TypedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
}
else
{
@ -189,7 +195,7 @@ public abstract class TypeMetadataBase
// Non-IId types: use RuntimeHelpers.GetHashCode (int)
// RefIdGetter is created in TypeMetadataWrapper.CreateRefIdGetter()
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
// _typedIdGetter remains null - wrapper uses GetHashCode directly
// TypedIdGetter remains null - wrapper uses GetHashCode directly
}
}
@ -241,4 +247,19 @@ public abstract class TypeMetadataBase
return allProperties;
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsComplexType2(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;
}
}

View File

@ -20,6 +20,8 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// <summary>
/// Typed getter for reference ID. Runtime type is Func&lt;object, int/long/Guid&gt;.
/// Use IdAccessorType to determine the actual type.
/// For IId types: uses metadata.TypedIdGetter (cached in metadata).
/// For non-IId types: uses RuntimeHelpers.GetHashCode.
/// </summary>
internal readonly Delegate RefIdGetter;
@ -36,39 +38,22 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
private const int BitArraySize = 1024; // 1024 * 64 = 65,536 IDs
/// <summary>
/// Static fallback delegate for non-IId types.
/// Shared across all wrappers to avoid allocation.
/// </summary>
private static readonly Func<object, int> HashCodeGetter = RuntimeHelpers.GetHashCode;
/// <summary>
/// Creates a new wrapper for the given metadata.
/// Initializes RefIdGetter based on IdAccessorType.
/// Uses metadata.TypedIdGetter for IId types (cached, no allocation).
/// </summary>
public TypeMetadataWrapper(TMetadata metadata)
{
Metadata = metadata;
// Create typed RefIdGetter based on IdAccessorType
RefIdGetter = CreateRefIdGetter(metadata);
}
private static Delegate CreateRefIdGetter(TMetadata metadata)
{
if (metadata.IsIId && metadata.IdPropertyInfo != null)
{
// IId type - create typed getter from Id property
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 =>
AcSerializerCommon.CreateTypedGetter<int>(metadata.MetadataType, metadata.IdPropertyInfo),
AcSerializerCommon.IdAccessorType.Int64 =>
AcSerializerCommon.CreateTypedGetter<long>(metadata.MetadataType, metadata.IdPropertyInfo),
AcSerializerCommon.IdAccessorType.Guid =>
AcSerializerCommon.CreateTypedGetter<Guid>(metadata.MetadataType, metadata.IdPropertyInfo),
_ => throw new NotSupportedException($"Unsupported IdAccessorType: {metadata.IdAccessorType}")
};
}
else
{
// Non-IId type - use RuntimeHelpers.GetHashCode
return new Func<object, int>(RuntimeHelpers.GetHashCode);
}
// Use cached delegate from metadata for IId types, static fallback for non-IId
RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
}
/// <summary>