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:
parent
b5680bc0e4
commit
8eeaa6725e
|
|
@ -365,7 +365,7 @@ public static class AcSerializerCommon
|
|||
/// Resolves a type from its name.
|
||||
/// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback.
|
||||
/// </summary>
|
||||
private static Type? ResolveTypeName(string typeName)
|
||||
internal static Type? ResolveTypeName(string typeName)
|
||||
{
|
||||
// Try direct resolution first (works for AssemblyQualifiedName)
|
||||
var type = Type.GetType(typeName);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Object olvasás core implementáció.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1039,7 +1034,7 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#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
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,11 +27,18 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
|||
public int ComplexPropertyIndex { get; internal set; } = -1;
|
||||
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>
|
||||
/// 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)] <EFBFBD> force intern
|
||||
/// false = [AcStringIntern(false)] <EFBFBD> force skip
|
||||
/// </summary>
|
||||
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 <EFBFBD> always write marker to stream
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue