Refactor: single-pass ref tracking, restrict IId<T> types
Refactored serialization reference tracking to a single-pass, inline approach, removing the previous two-pass scan. Only int, long, and Guid are now supported as IId<T> types; exotic ID types are no longer allowed. Cleaned up related enum values and code paths, defaulting non-IId types to int-based reference IDs. This simplifies the codebase and improves performance and type safety.
This commit is contained in:
parent
8161ddade4
commit
09a61539fa
|
|
@ -511,7 +511,7 @@ public static class JsonUtilities
|
|||
// FIXED: IsId should be true if IId<T> interface is found, not idType.IsValueType
|
||||
return new IdTypeInfo(true, idType);
|
||||
}
|
||||
return new IdTypeInfo(false, null);
|
||||
return new IdTypeInfo(false, typeof(int));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -597,16 +597,12 @@ public static class AcSerializerCommon
|
|||
/// </summary>
|
||||
public enum IdAccessorType : byte
|
||||
{
|
||||
/// <summary>Type does not implement IId.</summary>
|
||||
None = 0,
|
||||
/// <summary>Id is int (most common).</summary>
|
||||
Int32 = 1,
|
||||
/// <summary>Id is long.</summary>
|
||||
Int64 = 2,
|
||||
/// <summary>Id is Guid.</summary>
|
||||
Guid = 3,
|
||||
/// <summary>Id is an exotic type (uses boxing fallback).</summary>
|
||||
Object = 255
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -688,7 +684,6 @@ public static class AcSerializerCommon
|
|||
IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject),
|
||||
IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject),
|
||||
IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject),
|
||||
IdAccessorType.Object => TryGetObjectOriginal(obj, metadata, out originalObject),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
|
@ -768,9 +763,6 @@ public static class AcSerializerCommon
|
|||
case IdAccessorType.Guid:
|
||||
RegisterGuid(obj, metadata);
|
||||
break;
|
||||
case IdAccessorType.Object:
|
||||
RegisterObject(obj, metadata);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -936,7 +936,6 @@ public static partial class AcBinaryDeserializer
|
|||
AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance),
|
||||
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance),
|
||||
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance),
|
||||
AcSerializerCommon.IdAccessorType.Object when metadata.IdGetter != null => metadata.IdGetter(instance),
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@ public static partial class AcBinarySerializer
|
|||
{
|
||||
_refTracker.EnsureInitialized();
|
||||
}
|
||||
|
||||
// Reset wrapper tracking state from base class (IId tracking)
|
||||
base.Reset();
|
||||
|
||||
if (_buffer.Length < _initialBufferSize)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -175,13 +175,8 @@ public static partial class AcBinarySerializer
|
|||
var context = BinarySerializationContextPool.Get(options);
|
||||
context.WriteHeaderPlaceholder();
|
||||
|
||||
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType))
|
||||
{
|
||||
ScanReferences(value, context, 0);
|
||||
}
|
||||
|
||||
// Property index-based serialization - no metadata registration needed!
|
||||
// PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms
|
||||
// Single-pass serialization - no scan phase needed!
|
||||
// Reference tracking happens inline via TryTrack during WriteObject
|
||||
|
||||
// Estimate and reserve header space to avoid body shift later
|
||||
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
|
||||
|
|
@ -194,73 +189,6 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Reference Scanning
|
||||
|
||||
private static void ScanReferences(object? value, BinarySerializationContext context, int depth)
|
||||
{
|
||||
if (value == null || depth > context.MaxDepth) return;
|
||||
|
||||
var type = value.GetType();
|
||||
if (IsPrimitiveOrStringFast(type)) return;
|
||||
|
||||
// Get wrapper for IId-aware tracking
|
||||
var wrapper = context.GetWrapper(type);
|
||||
var metadata = wrapper.Metadata;
|
||||
|
||||
// OPTIMIZATION: Skip types that don't need reference tracking
|
||||
// (no IId, no complex properties that could be shared)
|
||||
if (!metadata.NeedsReferenceTracking) return;
|
||||
|
||||
// Use IId-aware tracking if metadata is available and UseReferenceHandling is enabled
|
||||
if (!context.TrackForScanningWithIId(value, metadata, out _)) return;
|
||||
|
||||
if (value is byte[]) return; // byte arrays are value types
|
||||
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
if (entry.Value != null)
|
||||
ScanReferences(entry.Value, context, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (item != null)
|
||||
ScanReferences(item, context, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// OPTIMIZATION: Skip if no complex properties to scan
|
||||
if (!metadata.HasComplexProperties) return;
|
||||
|
||||
// Scan only complex properties using pre-computed IsComplexType flag
|
||||
var properties = metadata.Properties;
|
||||
for (var i = 0; i < properties.Length; i++)
|
||||
{
|
||||
var prop = properties[i];
|
||||
|
||||
// Skip primitive properties - use pre-computed flag, no method call!
|
||||
if (!prop.IsComplexType) continue;
|
||||
|
||||
if (!context.ShouldSerializeProperty(value, prop))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue != null)
|
||||
ScanReferences(propValue, context, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Metadata Registration
|
||||
|
||||
private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null)
|
||||
|
|
@ -365,15 +293,7 @@ public static partial class AcBinarySerializer
|
|||
return;
|
||||
}
|
||||
|
||||
// Check for object reference
|
||||
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarInt(refId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle byte arrays specially
|
||||
// Handle byte arrays specially (value-like, no reference tracking)
|
||||
if (value is byte[] byteArray)
|
||||
{
|
||||
WriteByteArray(byteArray, context);
|
||||
|
|
@ -394,7 +314,7 @@ public static partial class AcBinarySerializer
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle complex objects
|
||||
// Handle complex objects with single-pass reference tracking
|
||||
WriteObject(value, type, context, depth);
|
||||
}
|
||||
|
||||
|
|
@ -711,21 +631,57 @@ public static partial class AcBinarySerializer
|
|||
|
||||
private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
|
||||
// Register object reference if needed
|
||||
if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
|
||||
{
|
||||
context.WriteVarInt(refId);
|
||||
context.MarkAsWritten(value, refId);
|
||||
}
|
||||
else if (context.UseReferenceHandling)
|
||||
{
|
||||
context.WriteVarInt(-1); // No ref ID
|
||||
}
|
||||
|
||||
var wrapper = context.GetWrapper(type);
|
||||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Single-pass reference tracking
|
||||
if (context.UseReferenceHandling)
|
||||
{
|
||||
switch (metadata.IdAccessorType)
|
||||
{
|
||||
case AcSerializerCommon.IdAccessorType.Int32:
|
||||
if (!context.TryTrack(wrapper, value, out int intId))
|
||||
{
|
||||
// Already seen → write reference
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarInt(intId);
|
||||
return;
|
||||
}
|
||||
// First occurrence → write object with refId
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
context.WriteVarInt(intId);
|
||||
break;
|
||||
|
||||
case AcSerializerCommon.IdAccessorType.Int64:
|
||||
if (!context.TryTrack(wrapper, value, out long longId))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarLong(longId);
|
||||
return;
|
||||
}
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
context.WriteVarLong(longId);
|
||||
break;
|
||||
|
||||
case AcSerializerCommon.IdAccessorType.Guid:
|
||||
if (!context.TryTrack(wrapper, value, out Guid guidId))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteGuidBits(guidId);
|
||||
return;
|
||||
}
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
context.WriteGuidBits(guidId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No reference handling - just write object marker
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
}
|
||||
|
||||
// Write properties
|
||||
var nextDepth = depth + 1;
|
||||
var properties = metadata.Properties;
|
||||
var propCount = properties.Length;
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ public abstract class TypeMetadataBase
|
|||
SourceType = type;
|
||||
_ignorePropertyFilter = ignorePropertyFilter;
|
||||
CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type);
|
||||
|
||||
|
||||
// Pre-compute property arrays - no dictionary lookup needed later!
|
||||
// Uses static global cache for unfiltered properties, then applies filter once
|
||||
var allReadable = GetUnfilteredProperties(type, requiresWrite: false)
|
||||
|
|
@ -152,40 +152,44 @@ public abstract class TypeMetadataBase
|
|||
.ToArray();
|
||||
ReadableProperties = allReadable;
|
||||
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
|
||||
|
||||
|
||||
// Cache IId info at construction time - no runtime reflection needed later!
|
||||
var idInfo = GetIdInfo(type);
|
||||
IsIId = idInfo.IsId;
|
||||
IdType = idInfo.IdType;
|
||||
|
||||
if (IsIId && IdType != null)
|
||||
if (IsIId)
|
||||
{
|
||||
var idProp = type.GetProperty("Id");
|
||||
IdPropertyInfo = idProp; // Store for TypeMetadataWrapper
|
||||
if (idProp != null)
|
||||
IdPropertyInfo = idProp; // Store for TypeMetadataWrapper
|
||||
|
||||
// Create typed getter for the three common Id types to avoid boxing
|
||||
if (ReferenceEquals(IdType, IntType))
|
||||
{
|
||||
// Create typed getter for the three common Id types to avoid boxing
|
||||
if (ReferenceEquals(IdType, IntType))
|
||||
{
|
||||
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
|
||||
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
|
||||
}
|
||||
else if (ReferenceEquals(IdType, LongType))
|
||||
{
|
||||
IdAccessorType = AcSerializerCommon.IdAccessorType.Int64;
|
||||
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
|
||||
}
|
||||
else if (ReferenceEquals(IdType, GuidType))
|
||||
{
|
||||
IdAccessorType = AcSerializerCommon.IdAccessorType.Guid;
|
||||
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Exotic Id types not supported - only int, long, Guid
|
||||
throw new NotSupportedException($"Unsupported IId type: {IdType.Name}. Only int, long, and Guid are supported.");
|
||||
}
|
||||
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
|
||||
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
|
||||
}
|
||||
else if (ReferenceEquals(IdType, LongType))
|
||||
{
|
||||
IdAccessorType = AcSerializerCommon.IdAccessorType.Int64;
|
||||
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
|
||||
}
|
||||
else if (ReferenceEquals(IdType, GuidType))
|
||||
{
|
||||
IdAccessorType = AcSerializerCommon.IdAccessorType.Guid;
|
||||
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Unsupported IId type: {IdType.Name}. Only int, long, and Guid are supported.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-IId types: use RuntimeHelpers.GetHashCode (int)
|
||||
// RefIdGetter is created in TypeMetadataWrapper.CreateRefIdGetter()
|
||||
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
|
||||
// _typedIdGetter remains null - wrapper uses GetHashCode directly
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue