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.
This commit is contained in:
Loretta 2026-02-25 09:13:56 +01:00
parent b5680bc0e4
commit 8eeaa6725e
5 changed files with 54 additions and 14 deletions

View File

@ -365,7 +365,7 @@ public static class AcSerializerCommon
/// Resolves a type from its name. /// Resolves a type from its name.
/// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback. /// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback.
/// </summary> /// </summary>
private static Type? ResolveTypeName(string typeName) internal static Type? ResolveTypeName(string typeName)
{ {
// Try direct resolution first (works for AssemblyQualifiedName) // Try direct resolution first (works for AssemblyQualifiedName)
var type = Type.GetType(typeName); var type = Type.GetType(typeName);

View File

@ -101,6 +101,7 @@ public static partial class AcBinaryDeserializer
readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata; readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata;
readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst; readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
readers[BinaryTypeCode.ObjectRef] = ReadObjectRef; readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
readers[BinaryTypeCode.Array] = ReadArray; readers[BinaryTypeCode.Array] = ReadArray;
readers[BinaryTypeCode.Dictionary] = ReadDictionary; readers[BinaryTypeCode.Dictionary] = ReadDictionary;
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx); readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
@ -1145,6 +1146,24 @@ public static partial class AcBinaryDeserializer
return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex); return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex);
} }
/// <summary>
/// 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.
/// </summary>
private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> 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);
}
/// <summary> /// <summary>
/// Object olvasás core implementáció. /// Object olvasás core implementáció.
/// </summary> /// </summary>

View File

@ -1,18 +1,13 @@
using AyCode.Core.Compression; using AyCode.Core.Compression;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Expressions; using AyCode.Core.Serializers.Expressions;
using System.Buffers; using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading;
using System.Diagnostics; using System.Diagnostics;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Serializers.Binaries;
@ -537,7 +532,7 @@ public static partial class AcBinarySerializer
} }
var wrapper = context.GetWrapper(type); var wrapper = context.GetWrapper(type);
WriteObject(value, wrapper, context, depth, isNested: depth > 0); WriteObject(value, wrapper, context, depth);
} }
/// <summary> /// <summary>
@ -556,7 +551,7 @@ public static partial class AcBinarySerializer
} }
var wrapper = context.GetWrapperBySlot(wrapperSlot, type); var wrapper = context.GetWrapperBySlot(wrapperSlot, type);
WriteObject(value, wrapper, context, depth, isNested: depth > 0); WriteObject(value, wrapper, context, depth);
} }
#endregion #endregion
@ -626,7 +621,7 @@ public static partial class AcBinarySerializer
} }
// Handle complex objects with single-pass reference tracking // Handle complex objects with single-pass reference tracking
WriteObject(value, wrapper, context, depth, isNested: depth > 0); WriteObject(value, wrapper, context, depth);
} }
/// <summary> /// <summary>
@ -673,7 +668,7 @@ public static partial class AcBinarySerializer
} }
// Handle complex objects with single-pass reference tracking // Handle complex objects with single-pass reference tracking
WriteObject(value, wrapper, context, depth, isNested: depth > 0); WriteObject(value, wrapper, context, depth);
} }
/// <summary> /// <summary>
@ -1039,7 +1034,7 @@ public static partial class AcBinarySerializer
#region Complex Type Writers #region Complex Type Writers
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, bool isNested = false) private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase where TOutput : struct, IBinaryOutputBase
{ {
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
@ -1480,6 +1475,16 @@ public static partial class AcBinarySerializer
else else
{ {
var runtimeType = value.GetType(); 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; var complexIdx = prop.ComplexPropertyIndex;
if (complexIdx >= 0) if (complexIdx >= 0)
{ {

View File

@ -27,11 +27,18 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
public int ComplexPropertyIndex { get; internal set; } = -1; public int ComplexPropertyIndex { get; internal set; } = -1;
public bool IsStringCollectionProperty { get; } public bool IsStringCollectionProperty { get; }
/// <summary>
/// True when declared property type is System.Object.
/// Used to trigger ObjectWithTypeName marker (68) so the deserializer
/// can resolve the concrete runtime type.
/// </summary>
public bool IsObjectDeclaredType { get; }
/// <summary> /// <summary>
/// Cached [AcStringIntern] attribute value for this property. /// Cached [AcStringIntern] attribute value for this property.
/// null = no attribute (follow global StringInterningMode) /// null = no attribute (follow global StringInterningMode)
/// true = [AcStringIntern(true)] — force intern /// true = [AcStringIntern(true)] <EFBFBD> force intern
/// false = [AcStringIntern(false)] — force skip /// false = [AcStringIntern(false)] <EFBFBD> force skip
/// </summary> /// </summary>
private readonly byte _interningFlags; private readonly byte _interningFlags;
@ -58,6 +65,7 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
: base(prop, declaringType) : base(prop, declaringType)
{ {
IsStringCollectionProperty = IsStringCollection(prop.PropertyType); IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
IsObjectDeclaredType = prop.PropertyType == typeof(object);
// All typed getters are initialized in PropertyAccessorBase // All typed getters are initialized in PropertyAccessorBase
if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty)) if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty))
@ -113,6 +121,6 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64, PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
PropertyAccessorType.Boolean => BinaryTypeCode.True, PropertyAccessorType.Boolean => BinaryTypeCode.True,
PropertyAccessorType.Enum => BinaryTypeCode.Enum, PropertyAccessorType.Enum => BinaryTypeCode.Enum,
_ => null // String, Object always write marker to stream _ => null // String, Object <EFBFBD> always write marker to stream
}; };
} }

View File

@ -55,6 +55,14 @@ internal static class BinaryTypeCode
public const byte ObjectRefFirst = 66; // First occurrence of tracked object (ref handling enabled) 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 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) // Special markers (32+, for header/meta)
// Header flags byte structure (for values >= 64): // Header flags byte structure (for values >= 64):
// Bit 0 (0x01): HasMetadata // Bit 0 (0x01): HasMetadata