Handle System.Object properties with runtime type dispatch

- Emit special serialization logic for properties declared as System.Object, using value.GetType() and writing type name metadata for correct polymorphic deserialization.
- Add IsObjectDeclaredType to PropInfo to support this logic.
- Update scan pass to use runtime type for object properties.
- Adjust IId reference test to ensure circular reference coverage.
- Default UseGeneratedCode to true in serializer options.
This commit is contained in:
Loretta 2026-02-25 09:28:44 +01:00
parent 8eeaa6725e
commit 03e5cd9f29
3 changed files with 31 additions and 5 deletions

View File

@ -248,6 +248,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
typeNameForTypeof,
kind,
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
p.Type.SpecialType == SpecialType.System_Object,
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
childTypeNameHash, childPropertyHashes,
@ -536,6 +537,22 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise.
if (p.HasGeneratedWriter)
EmitDirectObjectWrite(sb, p, a, i);
else if (p.IsObjectDeclaredType)
{
// System.Object property: runtime type unknown at compile time.
// Write ObjectWithTypeName prefix so deserializer can resolve the concrete type.
// Use value.GetType() for runtime type dispatch (not typeof(object)).
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} if (!context.UseMetadata)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithTypeName);");
sb.AppendLine($"{i} context.WriteStringUtf8({a}.GetType().AssemblyQualifiedName!);");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);");
sb.AppendLine($"{i}}}");
}
else if (p.IsNullable)
{
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
@ -723,13 +740,17 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
/// <summary>
/// Emits scan pass code for a Complex property without SGen writer (runtime fallback).
/// System.Object properties use value.GetType() for runtime type dispatch.
/// </summary>
private static void EmitScanComplexRuntime(StringBuilder sb, PropInfo p, string a, string i)
{
var childVar = $"sc_{p.Name}";
sb.AppendLine($"{i}var {childVar} = {a};");
sb.AppendLine($"{i}if ({childVar} != null)");
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context, depth + 1);");
if (p.IsObjectDeclaredType)
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, {childVar}.GetType(), context, depth + 1);");
else
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context, depth + 1);");
}
/// <summary>
@ -2046,6 +2067,8 @@ internal sealed class PropInfo
/// </summary>
public int InterningFlags { get; }
/// <summary>True when declared property type is System.Object. Runtime type dispatch needed.</summary>
public bool IsObjectDeclaredType { get; }
/// <summary>True if the Complex property type has [AcBinarySerializable] → has a generated writer.</summary>
public bool HasGeneratedWriter { get; }
/// <summary>True if the Complex property type implements IId&lt;T&gt; → needs ref tracking in write pass.</summary>
@ -2106,6 +2129,7 @@ internal sealed class PropInfo
public bool ElementNeedsScan => ElementNeedsIdScan || ElementNeedsAllRefScan || ElementNeedsInternScan;
public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable,
bool isObjectDeclaredType = false,
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null,
PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false,
string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null,
@ -2120,6 +2144,7 @@ internal sealed class PropInfo
TypeNameForTypeof = tnForTypeof;
TypeKind = tk;
IsNullable = nullable;
IsObjectDeclaredType = isObjectDeclaredType;
HasGeneratedWriter = hasGeneratedWriter;
IsIId = isIId;
WriterClassName = writerClassName;

View File

@ -114,9 +114,10 @@ public class AcBinarySerializerIIdReferenceTests
]
};
//order.Parent = order.Items[0];
order.Parent = userPreferences;
order.Items[0].ParentOrder = order;
if (mode != ReferenceHandlingMode.None) order.Parent = order.Items[1];
else order.Parent = userPreferences;
order.Items[1].ParentOrder = order;
var options = new AcBinarySerializerOptions { ReferenceHandling = mode };

View File

@ -84,7 +84,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// </summary>
public bool UseMetadata { get; set; } = false;
public bool UseGeneratedCode { get; set; } = false;
public bool UseGeneratedCode { get; set; } = true;
/// <summary>
/// Controls how string interning is applied during serialization.