diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 29bfb55..5392ad3 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -365,7 +365,7 @@ public static class AcSerializerCommon /// Resolves a type from its name. /// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback. /// - private static Type? ResolveTypeName(string typeName) + internal static Type? ResolveTypeName(string typeName) { // Try direct resolution first (works for AssemblyQualifiedName) var type = Type.GetType(typeName); diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 53dc7b2..024c7ef 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -101,6 +101,7 @@ public static partial class AcBinaryDeserializer readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata; readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst; readers[BinaryTypeCode.ObjectRef] = ReadObjectRef; + readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName; readers[BinaryTypeCode.Array] = ReadArray; readers[BinaryTypeCode.Dictionary] = ReadDictionary; readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx); @@ -1145,6 +1146,24 @@ public static partial class AcBinaryDeserializer return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex); } + /// + /// Polymorphic object prefix: declared property type is System.Object. + /// Wire format: [ObjectWithTypeName (68)] [TypeName string] [Object (25) or ObjectRefFirst (66) ...] [props...] + /// Reads the runtime type name, resolves it, then delegates to ReadValue with the resolved type + /// so the next marker (Object/ObjectRefFirst/etc.) is processed normally. + /// + private static object? ReadObjectWithTypeName(BinaryDeserializationContext context, Type targetType, int depth) + where TInput : struct, IBinaryInputBase + { + var typeName = ReadPlainString(context); + var resolvedType = AcSerializerCommon.ResolveTypeName(typeName) + ?? throw new AcBinaryDeserializationException( + $"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.", + context.Position, null); + // Next byte is the actual object marker (Object/ObjectRefFirst/etc.) — read it via ReadValue + return ReadValue(context, resolvedType, depth); + } + /// /// Object olvasás core implementáció. /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index d600a2f..938b710 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1,18 +1,13 @@ using AyCode.Core.Compression; -using AyCode.Core.Helpers; using AyCode.Core.Serializers.Expressions; using System.Buffers; using System.Collections; using System.Collections.Concurrent; -using System.Threading; using System.Diagnostics; using System.Linq.Expressions; -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using AyCode.Core.Serializers.Jsons; -using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; @@ -537,7 +532,7 @@ public static partial class AcBinarySerializer } var wrapper = context.GetWrapper(type); - WriteObject(value, wrapper, context, depth, isNested: depth > 0); + WriteObject(value, wrapper, context, depth); } /// @@ -556,7 +551,7 @@ public static partial class AcBinarySerializer } var wrapper = context.GetWrapperBySlot(wrapperSlot, type); - WriteObject(value, wrapper, context, depth, isNested: depth > 0); + WriteObject(value, wrapper, context, depth); } #endregion @@ -626,7 +621,7 @@ public static partial class AcBinarySerializer } // Handle complex objects with single-pass reference tracking - WriteObject(value, wrapper, context, depth, isNested: depth > 0); + WriteObject(value, wrapper, context, depth); } /// @@ -673,7 +668,7 @@ public static partial class AcBinarySerializer } // Handle complex objects with single-pass reference tracking - WriteObject(value, wrapper, context, depth, isNested: depth > 0); + WriteObject(value, wrapper, context, depth); } /// @@ -1039,7 +1034,7 @@ public static partial class AcBinarySerializer #region Complex Type Writers - private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, bool isNested = false) + private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase { var metadata = wrapper.Metadata; @@ -1480,6 +1475,16 @@ public static partial class AcBinarySerializer else { var runtimeType = value.GetType(); + + // System.Object declared property → prefix with ObjectWithTypeName marker + TypeName + // so the deserializer can resolve the concrete runtime type. + // The normal Object/ObjectRefFirst marker follows as usual. + if (prop.IsObjectDeclaredType && !context.UseMetadata) + { + context.WriteByte(BinaryTypeCode.ObjectWithTypeName); + context.WriteStringUtf8(runtimeType.AssemblyQualifiedName!); + } + var complexIdx = prop.ComplexPropertyIndex; if (complexIdx >= 0) { diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs index 1588b51..a25bfd9 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs @@ -27,11 +27,18 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase public int ComplexPropertyIndex { get; internal set; } = -1; public bool IsStringCollectionProperty { get; } + /// + /// True when declared property type is System.Object. + /// Used to trigger ObjectWithTypeName marker (68) so the deserializer + /// can resolve the concrete runtime type. + /// + public bool IsObjectDeclaredType { get; } + /// /// Cached [AcStringIntern] attribute value for this property. /// null = no attribute (follow global StringInterningMode) - /// true = [AcStringIntern(true)] — force intern - /// false = [AcStringIntern(false)] — force skip + /// true = [AcStringIntern(true)] � force intern + /// false = [AcStringIntern(false)] � force skip /// private readonly byte _interningFlags; @@ -58,6 +65,7 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase : base(prop, declaringType) { IsStringCollectionProperty = IsStringCollection(prop.PropertyType); + IsObjectDeclaredType = prop.PropertyType == typeof(object); // All typed getters are initialized in PropertyAccessorBase if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty)) @@ -113,6 +121,6 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64, PropertyAccessorType.Boolean => BinaryTypeCode.True, PropertyAccessorType.Enum => BinaryTypeCode.Enum, - _ => null // String, Object — always write marker to stream + _ => null // String, Object � always write marker to stream }; } diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs index 6ea8313..7514f5c 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs @@ -55,6 +55,14 @@ internal static class BinaryTypeCode public const byte ObjectRefFirst = 66; // First occurrence of tracked object (ref handling enabled) public const byte ObjectWithMetadataRefFirst = 67; // First occurrence of tracked object with metadata + // Polymorphic object markers (68-69): self-describing object for polymorphic properties. + // Used when declared property type ≠ runtime type AND UseMetadata=false. + // Serializer writes runtime type name inline so deserializer can resolve the concrete type. + // Format: [ObjectWithTypeName (68)] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd] + // Format: [ObjectWithTypeNameRefFirst (69)] [VarUInt cacheIndex] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd] + public const byte ObjectWithTypeName = 68; + public const byte ObjectWithTypeNameRefFirst = 69; + // Special markers (32+, for header/meta) // Header flags byte structure (for values >= 64): // Bit 0 (0x01): HasMetadata