Markerless serialization for value types (UseMetadata=false)
Introduced markerless serialization/deserialization for non-nullable value type properties when UseMetadata is false, eliminating type marker bytes for int, long, double, etc. Added ExpectedTypeCode to property accessors/setters to enable this optimization. Refactored property loops in serializer/deserializer for performance and clarity. Default UseMetadata is now false. Improves speed and reduces stream size for common value types while maintaining compatibility for complex types.
This commit is contained in:
parent
b38fd480d8
commit
97b7813633
|
|
@ -77,108 +77,196 @@ public static partial class AcBinaryDeserializer
|
||||||
// Non-UseMetadata: properties.Length a target property-k száma (source == target)
|
// Non-UseMetadata: properties.Length a target property-k száma (source == target)
|
||||||
var propCount = cacheMap?.Length ?? properties.Length;
|
var propCount = cacheMap?.Length ?? properties.Length;
|
||||||
|
|
||||||
for (int i = 0; i < propCount; i++)
|
if (!context.HasMetadata)
|
||||||
{
|
{
|
||||||
var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
|
// Markerless loop: properties with ExpectedTypeCode read raw values directly.
|
||||||
|
// Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path.
|
||||||
var peekCode = context.PeekByte();
|
for (int i = 0; i < propCount; i++)
|
||||||
|
|
||||||
// Nincs megfelelő target property → skip
|
|
||||||
if (propInfo == null)
|
|
||||||
{
|
{
|
||||||
SkipValue(ref context, metadata);
|
var propInfo = properties[i];
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip marker - property has default/null value
|
if (propInfo.ExpectedTypeCode.HasValue)
|
||||||
if (peekCode == BinaryTypeCode.PropertySkip)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Skip marker
|
|
||||||
|
|
||||||
// Populate mode: overwrite with default (existing object may have non-default values)
|
|
||||||
// Deserialize mode: skip write (new object already has defaults from CreateInstance)
|
|
||||||
if (!skipDefaultWrite)
|
|
||||||
{
|
{
|
||||||
SetPropertyToDefault(target, propInfo);
|
ReadAndSetMarkerlessValue(ref context, target, propInfo);
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Null values - always set
|
|
||||||
if (peekCode == BinaryTypeCode.Null)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Null marker
|
|
||||||
propInfo.SetValue(target, null);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle collections
|
|
||||||
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
|
||||||
{
|
|
||||||
var existingCollection = propInfo.GetValue(target);
|
|
||||||
if (existingCollection is IList existingList)
|
|
||||||
{
|
|
||||||
context.ReadByte(); // consume Array marker
|
|
||||||
|
|
||||||
// Merge mode with IId collection: use merge logic
|
|
||||||
if (isMergeMode && propInfo.IsIIdCollection)
|
|
||||||
{
|
|
||||||
MergeIIdCollection(ref context, existingList, propInfo, nextDepth);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Normal populate: replace collection contents
|
|
||||||
PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Handle nested complex objects - reuse existing if available
|
// Non-markerless properties: standard marker-based read
|
||||||
if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType)
|
PopulatePropertyWithMarker(ref context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// UseMetadata=true loop — UNCHANGED, zero extra overhead
|
||||||
|
for (int i = 0; i < propCount; i++)
|
||||||
{
|
{
|
||||||
var existingObj = propInfo.GetValue(target);
|
var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
|
||||||
if (existingObj != null)
|
|
||||||
|
PopulatePropertyWithMarker(ref context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standard marker-based property read. Extracted to avoid duplicating logic in both loops.
|
||||||
|
/// </summary>
|
||||||
|
private static void PopulatePropertyWithMarker(
|
||||||
|
ref BinaryDeserializationContext context,
|
||||||
|
object target,
|
||||||
|
BinaryPropertySetterBase? propInfo,
|
||||||
|
BinaryDeserializeTypeMetadata metadata,
|
||||||
|
int nextDepth,
|
||||||
|
bool isMergeMode,
|
||||||
|
bool skipDefaultWrite,
|
||||||
|
int propertyIndex,
|
||||||
|
int depth)
|
||||||
|
{
|
||||||
|
var peekCode = context.PeekByte();
|
||||||
|
|
||||||
|
// Nincs megfelelő target property → skip
|
||||||
|
if (propInfo == null)
|
||||||
|
{
|
||||||
|
SkipValue(ref context, metadata);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip marker - property has default/null value
|
||||||
|
if (peekCode == BinaryTypeCode.PropertySkip)
|
||||||
|
{
|
||||||
|
context.ReadByte(); // consume Skip marker
|
||||||
|
|
||||||
|
// Populate mode: overwrite with default (existing object may have non-default values)
|
||||||
|
// Deserialize mode: skip write (new object already has defaults from CreateInstance)
|
||||||
|
if (!skipDefaultWrite)
|
||||||
|
{
|
||||||
|
SetPropertyToDefault(target, propInfo);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null values - always set
|
||||||
|
if (peekCode == BinaryTypeCode.Null)
|
||||||
|
{
|
||||||
|
context.ReadByte(); // consume Null marker
|
||||||
|
propInfo.SetValue(target, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle collections
|
||||||
|
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
||||||
|
{
|
||||||
|
var existingCollection = propInfo.GetValue(target);
|
||||||
|
if (existingCollection is IList existingList)
|
||||||
|
{
|
||||||
|
context.ReadByte(); // consume Array marker
|
||||||
|
|
||||||
|
// Merge mode with IId collection: use merge logic
|
||||||
|
if (isMergeMode && propInfo.IsIIdCollection)
|
||||||
{
|
{
|
||||||
// ReadValue kezeli mindkét markert
|
MergeIIdCollection(ref context, existingList, propInfo, nextDepth);
|
||||||
var nestedValue = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
|
||||||
if (nestedValue != null)
|
|
||||||
{
|
|
||||||
var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata;
|
|
||||||
CopyProperties(nestedValue, existingObj, nestedMeta);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Normal populate: replace collection contents
|
||||||
|
PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default: read value and set (for primitives, strings, new objects)
|
// Handle nested complex objects - reuse existing if available
|
||||||
var positionBeforeRead = context.Position;
|
if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType)
|
||||||
try
|
{
|
||||||
|
var existingObj = propInfo.GetValue(target);
|
||||||
|
if (existingObj != null)
|
||||||
{
|
{
|
||||||
// Use typed setters for primitives and strings to avoid ReadValue dispatch
|
// ReadValue kezeli mindkét markert
|
||||||
if (propInfo.AccessorType != PropertyAccessorType.Object &&
|
var nestedValue = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||||||
TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
|
if (nestedValue != null)
|
||||||
continue;
|
{
|
||||||
|
var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata;
|
||||||
|
CopyProperties(nestedValue, existingObj, nestedMeta);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
// Default: read value and set (for primitives, strings, new objects)
|
||||||
propInfo.SetValue(target, value);
|
var positionBeforeRead = context.Position;
|
||||||
}
|
try
|
||||||
catch (InvalidCastException ex)
|
{
|
||||||
{
|
// Use typed setters for primitives and strings to avoid ReadValue dispatch
|
||||||
var targetType = target.GetType();
|
if (propInfo.AccessorType != PropertyAccessorType.Object &&
|
||||||
throw new AcBinaryDeserializationException(
|
TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
|
||||||
$"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetType.Name}'. " +
|
return;
|
||||||
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
|
|
||||||
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||||||
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
propInfo.SetValue(target, value);
|
||||||
$"Depth: {depth}. " +
|
}
|
||||||
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
|
catch (InvalidCastException ex)
|
||||||
$"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
|
{
|
||||||
$"Error: {ex.Message}",
|
var targetType = target.GetType();
|
||||||
positionBeforeRead,
|
throw new AcBinaryDeserializationException(
|
||||||
propInfo.PropertyType,
|
$"Type mismatch for property '{propInfo.Name}' (index {propertyIndex}) on '{targetType.Name}'. " +
|
||||||
ex);
|
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
|
||||||
}
|
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
||||||
|
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
||||||
|
$"Depth: {depth}. " +
|
||||||
|
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
|
||||||
|
$"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
|
||||||
|
$"Error: {ex.Message}",
|
||||||
|
positionBeforeRead,
|
||||||
|
propInfo.PropertyType,
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a raw value without type marker from stream (markerless mode, UseMetadata=false).
|
||||||
|
/// The property's type is known from metadata — no type code in the stream.
|
||||||
|
/// Only called for non-nullable value types with ExpectedTypeCode set.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void ReadAndSetMarkerlessValue(ref BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo)
|
||||||
|
{
|
||||||
|
switch (propInfo.AccessorType)
|
||||||
|
{
|
||||||
|
case PropertyAccessorType.Int32:
|
||||||
|
propInfo.SetInt32(target, context.ReadVarInt());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Int64:
|
||||||
|
propInfo.SetInt64(target, context.ReadVarLong());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Double:
|
||||||
|
propInfo.SetDouble(target, context.ReadDoubleUnsafe());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Single:
|
||||||
|
propInfo.SetSingle(target, context.ReadSingleUnsafe());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Decimal:
|
||||||
|
propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.DateTime:
|
||||||
|
propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Guid:
|
||||||
|
propInfo.SetGuid(target, context.ReadGuidUnsafe());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Byte:
|
||||||
|
propInfo.SetByte(target, context.ReadByte());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Int16:
|
||||||
|
propInfo.SetInt16(target, context.ReadInt16Unsafe());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.UInt16:
|
||||||
|
propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.UInt32:
|
||||||
|
propInfo.SetUInt32(target, context.ReadVarUInt());
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.UInt64:
|
||||||
|
propInfo.SetUInt64(target, context.ReadVarULong());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -944,27 +944,46 @@ public static partial class AcBinarySerializer
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
var properties = metadata.Properties;
|
var properties = metadata.Properties;
|
||||||
var propCount = properties.Length;
|
var propCount = properties.Length;
|
||||||
|
|
||||||
// Single-pass serialization with SKIP markers
|
|
||||||
// - No property count needed (fixed property order)
|
|
||||||
// - No property indices needed (sequential order)
|
|
||||||
// - Single getter call per property
|
|
||||||
// - Write value OR skip marker in one operation
|
|
||||||
var hasPropertyFilter = context.HasPropertyFilter;
|
var hasPropertyFilter = context.HasPropertyFilter;
|
||||||
|
|
||||||
for (var i = 0; i < propCount; i++)
|
if (!context.UseMetadata)
|
||||||
{
|
{
|
||||||
var prop = properties[i];
|
// Markerless loop: no extra branching per property for the common case.
|
||||||
|
// Properties with ExpectedTypeCode write raw values (no type marker, no skip).
|
||||||
// Skip if filter says no - write skip marker
|
// Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path.
|
||||||
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
for (var i = 0; i < propCount; i++)
|
||||||
{
|
{
|
||||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
var prop = properties[i];
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write property value OR skip marker (single operation, single getter call)
|
if (prop.ExpectedTypeCode.HasValue)
|
||||||
WritePropertyOrSkip(value, prop, context, nextDepth);
|
{
|
||||||
|
WritePropertyMarkerless(value, prop, context);
|
||||||
|
}
|
||||||
|
else if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
||||||
|
{
|
||||||
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WritePropertyOrSkip(value, prop, context, nextDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// UseMetadata=true loop — UNCHANGED, zero extra overhead
|
||||||
|
for (var i = 0; i < propCount; i++)
|
||||||
|
{
|
||||||
|
var prop = properties[i];
|
||||||
|
|
||||||
|
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
||||||
|
{
|
||||||
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
WritePropertyOrSkip(value, prop, context, nextDepth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1283,6 +1302,55 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a property value without type marker byte (markerless mode, UseMetadata=false).
|
||||||
|
/// All values are written including defaults — no PropertySkip markers.
|
||||||
|
/// Only called for non-nullable value types with ExpectedTypeCode set.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void WritePropertyMarkerless(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context)
|
||||||
|
{
|
||||||
|
switch (prop.AccessorType)
|
||||||
|
{
|
||||||
|
case PropertyAccessorType.Int32:
|
||||||
|
context.WriteVarInt(prop.GetInt32(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Int64:
|
||||||
|
context.WriteVarLong(prop.GetInt64(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Double:
|
||||||
|
context.WriteRaw(prop.GetDouble(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Single:
|
||||||
|
context.WriteRaw(prop.GetSingle(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Decimal:
|
||||||
|
context.WriteDecimalBits(prop.GetDecimal(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.DateTime:
|
||||||
|
context.WriteDateTimeBits(prop.GetDateTime(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Guid:
|
||||||
|
context.WriteGuidBits(prop.GetGuid(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Byte:
|
||||||
|
context.WriteByte(prop.GetByte(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Int16:
|
||||||
|
context.WriteRaw(prop.GetInt16(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.UInt16:
|
||||||
|
context.WriteRaw(prop.GetUInt16(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.UInt32:
|
||||||
|
context.WriteVarUInt(prop.GetUInt32(obj));
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.UInt64:
|
||||||
|
context.WriteVarULong(prop.GetUInt64(obj));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Specialized Array Writers
|
#region Specialized Array Writers
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
/// allowing the deserializer to match properties by name between different types.
|
/// allowing the deserializer to match properties by name between different types.
|
||||||
/// Default: false (no overhead)
|
/// Default: false (no overhead)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UseMetadata { get; init; } = true;
|
public bool UseMetadata { get; init; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<object, object?> DynamicGetter => _dynamicGetter;
|
public Func<object, object?> DynamicGetter => _dynamicGetter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-computed expected type code for markerless serialization (UseMetadata=false).
|
||||||
|
/// When set, the serializer skips writing the type marker byte and the deserializer
|
||||||
|
/// uses this value instead of reading from the stream.
|
||||||
|
/// null = marker must be written/read (bool, enum, string, object, collection, nullable value types).
|
||||||
|
/// Non-null = markerless (non-nullable value types: int, long, double, float, decimal, Guid, DateTime, byte, short, ushort, uint, ulong).
|
||||||
|
/// </summary>
|
||||||
|
public byte? ExpectedTypeCode { get; }
|
||||||
|
|
||||||
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType)
|
protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType)
|
||||||
: base(prop, declaringType)
|
: base(prop, declaringType)
|
||||||
{
|
{
|
||||||
|
|
@ -39,5 +48,28 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
// Cache string intern attribute (inherit: true to check base class properties)
|
// Cache string intern attribute (inherit: true to check base class properties)
|
||||||
var attr = prop.GetCustomAttribute<AcStringInternAttribute>(inherit: true);
|
var attr = prop.GetCustomAttribute<AcStringInternAttribute>(inherit: true);
|
||||||
IsStringInternProperty = attr?.Enabled;
|
IsStringInternProperty = attr?.Enabled;
|
||||||
|
|
||||||
|
ExpectedTypeCode = ComputeExpectedTypeCode(AccessorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps AccessorType to the BinaryTypeCode that would normally be written as marker.
|
||||||
|
/// Returns null for types that always need a stream marker (bool, enum, string, object/nullable).
|
||||||
|
/// </summary>
|
||||||
|
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
||||||
|
{
|
||||||
|
PropertyAccessorType.Int32 => BinaryTypeCode.Int32,
|
||||||
|
PropertyAccessorType.Int64 => BinaryTypeCode.Int64,
|
||||||
|
PropertyAccessorType.Double => BinaryTypeCode.Float64,
|
||||||
|
PropertyAccessorType.Single => BinaryTypeCode.Float32,
|
||||||
|
PropertyAccessorType.Decimal => BinaryTypeCode.Decimal,
|
||||||
|
PropertyAccessorType.DateTime => BinaryTypeCode.DateTime,
|
||||||
|
PropertyAccessorType.Guid => BinaryTypeCode.Guid,
|
||||||
|
PropertyAccessorType.Byte => BinaryTypeCode.UInt8,
|
||||||
|
PropertyAccessorType.Int16 => BinaryTypeCode.Int16,
|
||||||
|
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
||||||
|
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
||||||
|
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
|
||||||
|
_ => null // Bool, Enum, String, Object — always read marker from stream
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,20 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsCollection { get; }
|
public bool IsCollection { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-computed expected type code for markerless deserialization (UseMetadata=false).
|
||||||
|
/// When set, the deserializer uses this value instead of reading a marker from the stream.
|
||||||
|
/// null = marker must be read from stream (bool, enum, string, object, collection, nullable value types).
|
||||||
|
/// Non-null = markerless (non-nullable value types).
|
||||||
|
/// </summary>
|
||||||
|
public byte? ExpectedTypeCode { get; }
|
||||||
|
|
||||||
protected BinaryPropertySetterBase(PropertyInfo prop, Type declaringType)
|
protected BinaryPropertySetterBase(PropertyInfo prop, Type declaringType)
|
||||||
: base(prop, declaringType)
|
: base(prop, declaringType)
|
||||||
{
|
{
|
||||||
IsCollection = IsCollectionTypeCheck(PropertyType);
|
IsCollection = IsCollectionTypeCheck(PropertyType);
|
||||||
IsComplexType = IsComplex(PropertyType);
|
IsComplexType = IsComplex(PropertyType);
|
||||||
|
ExpectedTypeCode = ComputeExpectedTypeCode(AccessorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetValue(object target, object? value)
|
public override void SetValue(object target, object? value)
|
||||||
|
|
@ -83,4 +92,25 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase
|
||||||
if (ReferenceEquals(actualType, DateTimeOffsetType)) return false;
|
if (ReferenceEquals(actualType, DateTimeOffsetType)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps AccessorType to the BinaryTypeCode that would normally be read as marker.
|
||||||
|
/// Returns null for types that always need a stream marker (bool, enum, string, object/nullable).
|
||||||
|
/// </summary>
|
||||||
|
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
||||||
|
{
|
||||||
|
PropertyAccessorType.Int32 => BinaryTypeCode.Int32,
|
||||||
|
PropertyAccessorType.Int64 => BinaryTypeCode.Int64,
|
||||||
|
PropertyAccessorType.Double => BinaryTypeCode.Float64,
|
||||||
|
PropertyAccessorType.Single => BinaryTypeCode.Float32,
|
||||||
|
PropertyAccessorType.Decimal => BinaryTypeCode.Decimal,
|
||||||
|
PropertyAccessorType.DateTime => BinaryTypeCode.DateTime,
|
||||||
|
PropertyAccessorType.Guid => BinaryTypeCode.Guid,
|
||||||
|
PropertyAccessorType.Byte => BinaryTypeCode.UInt8,
|
||||||
|
PropertyAccessorType.Int16 => BinaryTypeCode.Int16,
|
||||||
|
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
||||||
|
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
||||||
|
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
|
||||||
|
_ => null // Bool, Enum, String, Object — always read marker from stream
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue