diff --git a/AyCode.Core/Helpers/JsonUtilities.cs b/AyCode.Core/Helpers/JsonUtilities.cs index 58830e3..1501359 100644 --- a/AyCode.Core/Helpers/JsonUtilities.cs +++ b/AyCode.Core/Helpers/JsonUtilities.cs @@ -511,7 +511,7 @@ public static class JsonUtilities // FIXED: IsId should be true if IId interface is found, not idType.IsValueType return new IdTypeInfo(true, idType); } - return new IdTypeInfo(false, null); + return new IdTypeInfo(false, typeof(int)); }); } diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 87ca528..6e59f49 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -597,16 +597,12 @@ public static class AcSerializerCommon /// public enum IdAccessorType : byte { - /// Type does not implement IId. - None = 0, /// Id is int (most common). Int32 = 1, /// Id is long. Int64 = 2, /// Id is Guid. Guid = 3, - /// Id is an exotic type (uses boxing fallback). - Object = 255 } /// @@ -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; } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index ca0f687..2b93581 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -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 }; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index a93563f..2dfa6ca 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -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) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 14b1757..8f68b0d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -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? 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; diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index e3fc007..31b6ac1 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -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(type, idProp); - } - else if (ReferenceEquals(IdType, LongType)) - { - IdAccessorType = AcSerializerCommon.IdAccessorType.Int64; - _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); - } - else if (ReferenceEquals(IdType, GuidType)) - { - IdAccessorType = AcSerializerCommon.IdAccessorType.Guid; - _typedIdGetter = AcSerializerCommon.CreateTypedGetter(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(type, idProp); } + else if (ReferenceEquals(IdType, LongType)) + { + IdAccessorType = AcSerializerCommon.IdAccessorType.Int64; + _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); + } + else if (ReferenceEquals(IdType, GuidType)) + { + IdAccessorType = AcSerializerCommon.IdAccessorType.Guid; + _typedIdGetter = AcSerializerCommon.CreateTypedGetter(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 } }