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
|
// FIXED: IsId should be true if IId<T> interface is found, not idType.IsValueType
|
||||||
return new IdTypeInfo(true, idType);
|
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>
|
/// </summary>
|
||||||
public enum IdAccessorType : byte
|
public enum IdAccessorType : byte
|
||||||
{
|
{
|
||||||
/// <summary>Type does not implement IId.</summary>
|
|
||||||
None = 0,
|
|
||||||
/// <summary>Id is int (most common).</summary>
|
/// <summary>Id is int (most common).</summary>
|
||||||
Int32 = 1,
|
Int32 = 1,
|
||||||
/// <summary>Id is long.</summary>
|
/// <summary>Id is long.</summary>
|
||||||
Int64 = 2,
|
Int64 = 2,
|
||||||
/// <summary>Id is Guid.</summary>
|
/// <summary>Id is Guid.</summary>
|
||||||
Guid = 3,
|
Guid = 3,
|
||||||
/// <summary>Id is an exotic type (uses boxing fallback).</summary>
|
|
||||||
Object = 255
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -688,7 +684,6 @@ public static class AcSerializerCommon
|
||||||
IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject),
|
IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject),
|
||||||
IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject),
|
IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject),
|
||||||
IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject),
|
IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject),
|
||||||
IdAccessorType.Object => TryGetObjectOriginal(obj, metadata, out originalObject),
|
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -768,9 +763,6 @@ public static class AcSerializerCommon
|
||||||
case IdAccessorType.Guid:
|
case IdAccessorType.Guid:
|
||||||
RegisterGuid(obj, metadata);
|
RegisterGuid(obj, metadata);
|
||||||
break;
|
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.Int32 => metadata.GetIdInt32(instance),
|
||||||
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance),
|
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance),
|
||||||
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance),
|
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance),
|
||||||
AcSerializerCommon.IdAccessorType.Object when metadata.IdGetter != null => metadata.IdGetter(instance),
|
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
_refTracker.EnsureInitialized();
|
_refTracker.EnsureInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset wrapper tracking state from base class (IId tracking)
|
||||||
|
base.Reset();
|
||||||
|
|
||||||
if (_buffer.Length < _initialBufferSize)
|
if (_buffer.Length < _initialBufferSize)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -175,13 +175,8 @@ public static partial class AcBinarySerializer
|
||||||
var context = BinarySerializationContextPool.Get(options);
|
var context = BinarySerializationContextPool.Get(options);
|
||||||
context.WriteHeaderPlaceholder();
|
context.WriteHeaderPlaceholder();
|
||||||
|
|
||||||
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType))
|
// Single-pass serialization - no scan phase needed!
|
||||||
{
|
// Reference tracking happens inline via TryTrack during WriteObject
|
||||||
ScanReferences(value, context, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Property index-based serialization - no metadata registration needed!
|
|
||||||
// PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms
|
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
@ -194,73 +189,6 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#endregion
|
#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
|
#region Property Metadata Registration
|
||||||
|
|
||||||
private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null)
|
private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null)
|
||||||
|
|
@ -365,15 +293,7 @@ public static partial class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for object reference
|
// Handle byte arrays specially (value-like, no reference tracking)
|
||||||
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
|
|
||||||
{
|
|
||||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
|
||||||
context.WriteVarInt(refId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle byte arrays specially
|
|
||||||
if (value is byte[] byteArray)
|
if (value is byte[] byteArray)
|
||||||
{
|
{
|
||||||
WriteByteArray(byteArray, context);
|
WriteByteArray(byteArray, context);
|
||||||
|
|
@ -394,7 +314,7 @@ public static partial class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle complex objects
|
// Handle complex objects with single-pass reference tracking
|
||||||
WriteObject(value, type, context, depth);
|
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)
|
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 wrapper = context.GetWrapper(type);
|
||||||
var metadata = wrapper.Metadata;
|
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 nextDepth = depth + 1;
|
||||||
var properties = metadata.Properties;
|
var properties = metadata.Properties;
|
||||||
var propCount = properties.Length;
|
var propCount = properties.Length;
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ public abstract class TypeMetadataBase
|
||||||
SourceType = type;
|
SourceType = type;
|
||||||
_ignorePropertyFilter = ignorePropertyFilter;
|
_ignorePropertyFilter = ignorePropertyFilter;
|
||||||
CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type);
|
CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type);
|
||||||
|
|
||||||
// Pre-compute property arrays - no dictionary lookup needed later!
|
// Pre-compute property arrays - no dictionary lookup needed later!
|
||||||
// Uses static global cache for unfiltered properties, then applies filter once
|
// Uses static global cache for unfiltered properties, then applies filter once
|
||||||
var allReadable = GetUnfilteredProperties(type, requiresWrite: false)
|
var allReadable = GetUnfilteredProperties(type, requiresWrite: false)
|
||||||
|
|
@ -152,40 +152,44 @@ public abstract class TypeMetadataBase
|
||||||
.ToArray();
|
.ToArray();
|
||||||
ReadableProperties = allReadable;
|
ReadableProperties = allReadable;
|
||||||
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
|
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
|
||||||
|
|
||||||
// Cache IId info at construction time - no runtime reflection needed later!
|
// Cache IId info at construction time - no runtime reflection needed later!
|
||||||
var idInfo = GetIdInfo(type);
|
var idInfo = GetIdInfo(type);
|
||||||
IsIId = idInfo.IsId;
|
IsIId = idInfo.IsId;
|
||||||
IdType = idInfo.IdType;
|
IdType = idInfo.IdType;
|
||||||
|
|
||||||
if (IsIId && IdType != null)
|
if (IsIId)
|
||||||
{
|
{
|
||||||
var idProp = type.GetProperty("Id");
|
var idProp = type.GetProperty("Id");
|
||||||
IdPropertyInfo = idProp; // Store for TypeMetadataWrapper
|
IdPropertyInfo = idProp; // Store for TypeMetadataWrapper
|
||||||
if (idProp != null)
|
|
||||||
|
// 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
|
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
|
||||||
if (ReferenceEquals(IdType, IntType))
|
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
|
||||||
{
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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