From 8eeaa6725eb63ace829670aec87eb6912bed189b Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 25 Feb 2026 09:13:56 +0100 Subject: [PATCH] Add polymorphic support for System.Object properties Enable serialization of runtime type info for System.Object properties using new ObjectWithTypeName markers. Serializer now writes the runtime type name inline; deserializer resolves and instantiates the correct type. Added IsObjectDeclaredType property for detection, refactored WriteObject methods, and registered new deserialization logic. This ensures robust polymorphic (de)serialization even without metadata. Also includes minor cleanup of unused usings. --- AyCode.Core/Serializers/AcSerializerCommon.cs | 2 +- .../Binaries/AcBinaryDeserializer.cs | 19 ++++++++++++++ .../Binaries/AcBinarySerializer.cs | 25 +++++++++++-------- .../Binaries/BinaryPropertyAccessorBase.cs | 14 ++++++++--- .../Serializers/Binaries/BinaryTypeCode.cs | 8 ++++++ 5 files changed, 54 insertions(+), 14 deletions(-) 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