Add IId-aware reference tracking to serializer/deserializer

Enhances both binary deserialization and JSON serialization to support IId-based reference tracking and handling. Refactors object reference reading in AcBinaryDeserializer to use a unified TryGetValue approach. Adds IId-aware tracking, ID writing, and reference lookup methods to AcJsonSerializer and updates ScanReferences and WriteObject to use them. Improves support for custom identity types and robust reference management.
This commit is contained in:
Loretta 2026-01-20 08:58:07 +01:00
parent 6dbe4d76c1
commit 7d133a4b24
3 changed files with 59 additions and 26 deletions

View File

@ -913,18 +913,20 @@ public static partial class AcBinaryDeserializer
private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth)
{
var wrapper = context.ContextClass.GetWrapper(targetType);
return ReadObjectRef(ref context, ref wrapper, depth);
}
private static object? ReadObjectRef(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int depth)
{
var metadata = wrapper.Metadata;
if (metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None)
{
var map = wrapper.IdentityMap;
// Read ID based on type and lookup in IdentityMap
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, map),
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, map),
AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, map),
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, ref wrapper),
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, ref wrapper),
AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, ref wrapper),
_ => throw new Exception("metadata.IdAccessorType not valid")
};
}
@ -933,30 +935,24 @@ public static partial class AcBinaryDeserializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map)
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var id = context.ReadVarInt();
if (map is AcSerializerCommon.IdentityMap<int> intMap && intMap.TryGetValue(id, out var obj))
return obj;
return null;
return context.ContextClass.TryGetValue(wrapper, id, out var instance) ? instance : throw new Exception("ReadObjectRefInt32");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map)
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var id = context.ReadVarLong(); // VarLong, not VarInt!
if (map is AcSerializerCommon.IdentityMap<long> longMap && longMap.TryGetValue(id, out var obj))
return obj;
return null; // Long IIds don't use flat reference tracking
var id = context.ReadVarLong();
return context.ContextClass.TryGetValue(wrapper, id, out var instance) ? instance : throw new Exception("ReadObjectRefInt64");;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map)
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var id = context.ReadGuidUnsafe(); // 16 bytes raw!
if (map is AcSerializerCommon.IdentityMap<Guid> guidMap && guidMap.TryGetValue(id, out var obj))
return obj;
return null; // Guid IIds don't use flat reference tracking
return context.ContextClass.TryGetValue(wrapper, id, out var instance) ? instance : throw new Exception("ReadObjectRefGuid");;
}
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth)

View File

@ -86,15 +86,47 @@ public static partial class AcJsonSerializer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanning(object obj) => _refTracker.TrackForScanning(obj);
/// <summary>
/// IId-aware tracking for reference scanning.
/// Uses IId.Id for IId types, RuntimeHelpers.GetHashCode for others.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanningWithIId(object obj, JsonSerializeTypeMetadata metadata)
{
int existingRefId;
return _refTracker.TrackForScanningWithIId(obj, metadata, out existingRefId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteId(object obj, out int id) => _refTracker.ShouldWriteId(obj, out id);
/// <summary>
/// IId-aware check for reference ID writing.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteIdForIId(object obj, JsonSerializeTypeMetadata metadata, out int id)
=> _refTracker.ShouldWriteIdForIId(obj, metadata, out id);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, int id) => _refTracker.MarkAsWritten(obj, id);
/// <summary>
/// IId-aware mark as written.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWrittenForIId(object obj, JsonSerializeTypeMetadata metadata, int id)
=> _refTracker.MarkAsWrittenForIId(obj, metadata, id);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRef(object obj, out int refId) => _refTracker.TryGetExistingRef(obj, out refId);
/// <summary>
/// IId-aware existing ref lookup.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRefForIId(object obj, JsonSerializeTypeMetadata metadata, out int refId)
=> _refTracker.TryGetExistingRefForIId(obj, metadata, out refId);
public string GetResult()
{

View File

@ -130,7 +130,13 @@ public static partial class AcJsonSerializer
var type = value.GetType();
if (IsPrimitiveOrStringFast(type)) return;
if (!context.TrackForScanning(value)) return;
// Get wrapper early to use IId-aware tracking
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata;
// Use IId-aware scanning if available
if (!context.TrackForScanningWithIId(value, metadata)) return;
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{
@ -139,8 +145,6 @@ public static partial class AcJsonSerializer
return;
}
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata;
var props = metadata.Properties;
var propCount = props.Length;
for (var i = 0; i < propCount; i++)
@ -174,8 +178,11 @@ public static partial class AcJsonSerializer
private static void WriteObject(object value, in Type type, JsonSerializationContext context, int depth)
{
var writer = context.Writer;
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata;
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
// Use IId-aware reference handling
if (context.UseReferenceHandling && context.TryGetExistingRefForIId(value, metadata, out var refId))
{
writer.WriteStartObject();
writer.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
@ -185,14 +192,12 @@ public static partial class AcJsonSerializer
writer.WriteStartObject();
if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id))
if (context.UseReferenceHandling && context.ShouldWriteIdForIId(value, metadata, out var id))
{
writer.WriteString(IdPropertyEncoded, id.ToString(CultureInfo.InvariantCulture));
context.MarkAsWritten(value, id);
context.MarkAsWrittenForIId(value, metadata, id);
}
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata;
var props = metadata.Properties;
var propCount = props.Length;
var nextDepth = depth + 1;