Refactor property metadata; add console perf profiler

- Introduced PropertyMetadataBase to unify property metadata and dynamic getter logic, now shared by PropertyAccessorBase and PropertySetterBase.
- Moved PropertyAccessorType enum to PropertyMetadataBase.
- Replaced all GetDynamicValue usages with GetValue for consistent property access in serializers/deserializers.
- Refactored PropertyAccessorBase and PropertySetterBase inheritance and responsibilities.
- Added MaxStringInternLength option to AcBinarySerializerOptions for configurable string interning.
- Improved collection deserialization: clear destination if source is empty.
- Added AyCode.Core.Serializers.Console project for performance profiling (with MessagePack comparison).
- Updated solution file to include new project and build configs.
- Minor code cleanups and documentation improvements.
This commit is contained in:
Loretta 2026-01-21 16:47:40 +01:00
parent 75823d593b
commit 905b1c404d
16 changed files with 433 additions and 157 deletions

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\AyCode.Core\AyCode.Core.csproj" />
<ProjectReference Include="..\AyCode.Core.Tests\AyCode.Core.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,195 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
using MessagePack;
using MessagePack.Resolvers;
namespace AyCode.Core.Serializers.Console;
/// <summary>
/// Console application for Performance Diagnostics profiling.
/// Run with: Debug > Performance Profiler in Visual Studio
///
/// Usage:
/// dotnet run -- serialize # Profile serialize only
/// dotnet run -- deserialize # Profile deserialize only
/// dotnet run -- all # Profile both (default)
/// </summary>
public static class Program
{
private const int WarmupIterations = 50;
private const int TestIterations = 5000;
// Keep references to prevent GC during profiling
private static TestOrder s_testOrder = null!;
private static byte[] s_acBinaryData = null!;
private static byte[] s_acBinaryNoRefData = null!;
private static byte[] s_msgPackData = null!;
private static AcBinarySerializerOptions s_acBinaryOptions = null!;
private static AcBinarySerializerOptions s_acBinaryNoRefOptions = null!;
private static MessagePackSerializerOptions s_msgPackOptions = null!;
public static void Main(string[] args)
{
var mode = args.Length > 0 ? args[0].ToLower() : "all";
System.Console.WriteLine("=".PadRight(60, '='));
System.Console.WriteLine($"AcBinary Performance Profiler - Mode: {mode}");
System.Console.WriteLine("=".PadRight(60, '='));
Setup();
Warmup();
System.Console.WriteLine($"\nRunning {TestIterations} iterations...\n");
var sw = Stopwatch.StartNew();
switch (mode)
{
case "serialize":
case "ser":
RunSerializeTests();
break;
case "deserialize":
case "des":
RunDeserializeTests();
break;
default:
RunSerializeTests();
RunDeserializeTests();
break;
}
sw.Stop();
System.Console.WriteLine($"\nTotal time: {sw.ElapsedMilliseconds:N0} ms");
System.Console.WriteLine("=".PadRight(60, '='));
}
private static void Setup()
{
System.Console.WriteLine("Creating test data...");
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
s_testOrder = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta);
s_acBinaryOptions = AcBinarySerializerOptions.Default;
s_acBinaryNoRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
s_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
s_acBinaryData = AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions);
s_acBinaryNoRefData = AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions);
s_msgPackData = MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions);
System.Console.WriteLine($" AcBinary (WithRef): {s_acBinaryData.Length:N0} bytes");
System.Console.WriteLine($" AcBinary (NoRef): {s_acBinaryNoRefData.Length:N0} bytes");
System.Console.WriteLine($" MessagePack: {s_msgPackData.Length:N0} bytes");
}
private static void Warmup()
{
System.Console.WriteLine($"Warming up ({WarmupIterations} iterations)...");
for (int i = 0; i < WarmupIterations; i++)
{
DoSerializeAcBinary();
DoSerializeAcBinaryNoRef();
DoSerializeMsgPack();
DoDeserializeAcBinary();
DoDeserializeAcBinaryNoRef();
DoDeserializeMsgPack();
}
}
private static void RunSerializeTests()
{
System.Console.WriteLine("--- SERIALIZE ---");
var sw = Stopwatch.StartNew();
for (int i = 0; i < TestIterations; i++)
{
DoSerializeAcBinary();
}
sw.Stop();
System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
sw.Restart();
for (int i = 0; i < TestIterations; i++)
{
DoSerializeAcBinaryNoRef();
}
sw.Stop();
System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
sw.Restart();
for (int i = 0; i < TestIterations; i++)
{
DoSerializeMsgPack();
}
sw.Stop();
System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
}
private static void RunDeserializeTests()
{
System.Console.WriteLine("--- DESERIALIZE ---");
var sw = Stopwatch.StartNew();
for (int i = 0; i < TestIterations; i++)
{
DoDeserializeAcBinary();
}
sw.Stop();
System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
sw.Restart();
for (int i = 0; i < TestIterations; i++)
{
DoDeserializeAcBinaryNoRef();
}
sw.Stop();
System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
sw.Restart();
for (int i = 0; i < TestIterations; i++)
{
DoDeserializeMsgPack();
}
sw.Stop();
System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
}
// Separate methods for better profiler visibility - NO INLINING
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte[] DoSerializeAcBinary()
=> AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions);
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte[] DoSerializeAcBinaryNoRef()
=> AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions);
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte[] DoSerializeMsgPack()
=> MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions);
[MethodImpl(MethodImplOptions.NoInlining)]
private static TestOrder? DoDeserializeAcBinary()
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryData);
[MethodImpl(MethodImplOptions.NoInlining)]
private static TestOrder? DoDeserializeAcBinaryNoRef()
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryNoRefData);
[MethodImpl(MethodImplOptions.NoInlining)]
private static TestOrder? DoDeserializeMsgPack()
=> MessagePackSerializer.Deserialize<TestOrder>(s_msgPackData, s_msgPackOptions);
}

View File

@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Services.Tests", "Ay
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core.Serializers.SourceGenerator", "AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj", "{4A817897-80A8-4F42-86C5-20447401E0AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core.Serializers.Console", "AyCode.Core.Serializers.Console\AyCode.Core.Serializers.Console.csproj", "{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -433,6 +435,24 @@ Global
{4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x64.Build.0 = Release|Any CPU
{4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x86.ActiveCfg = Release|Any CPU
{4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x86.Build.0 = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x64.ActiveCfg = Debug|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x64.Build.0 = Debug|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x86.ActiveCfg = Debug|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x86.Build.0 = Debug|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|Any CPU.ActiveCfg = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|Any CPU.Build.0 = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x64.ActiveCfg = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x64.Build.0 = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x86.ActiveCfg = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x86.Build.0 = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|Any CPU.Build.0 = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x64.ActiveCfg = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x64.Build.0 = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x86.ActiveCfg = Release|Any CPU
{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -151,7 +151,7 @@ public static partial class AcBinaryDeserializer
public new bool IsIIdCollection => _isManualConstruction ? _manualIsIIdCollection : base.IsIIdCollection;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new object? GetValue(object target) => _isManualConstruction ? _manualGetter!(target) : base.GetDynamicValue(target);
public new object? GetValue(object target) => _isManualConstruction ? _manualGetter!(target) : base.GetValue(target);
public override void SetValue(object target, object? value)
{

View File

@ -110,10 +110,10 @@ public static partial class AcBinaryDeserializer
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
context.ReadByte(); // consume Object marker
var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
// Handle ref ID if present
if (context.HasReferenceHandling)
{
@ -135,7 +135,9 @@ public static partial class AcBinaryDeserializer
try
{
// Use typed setters for primitives to avoid boxing
if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
// Skip method call for Object/String/Collection types - they can't use typed setters
if (propInfo.AccessorType != PropertyAccessorType.Object &&
TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
continue;
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
@ -214,9 +216,16 @@ public static partial class AcBinaryDeserializer
try
{
var wrapper = context.ContextClass.GetWrapper(elementType);
var existingCount = existingList.Count;
// Early exit if empty source - just clear destination
if (count == 0)
{
existingList.Clear();
return;
}
var wrapper = context.ContextClass.GetWrapper(elementType);
var elementMetadata = wrapper.Metadata.IsComplexType ? wrapper.Metadata : null;
for (int i = 0; i < count; i++)

View File

@ -89,6 +89,7 @@ public static partial class AcBinarySerializer
public bool UseMetadata { get; private set; }
public byte MaxDepth { get; private set; }
public byte MinStringInternLength { get; private set; }
public byte MaxStringInternLength { get; private set; }
public BinaryPropertyFilter? PropertyFilter { get; private set; }
public int Position => _position;
@ -114,6 +115,7 @@ public static partial class AcBinarySerializer
UseMetadata = options.UseMetadata;
MaxDepth = options.MaxDepth;
MinStringInternLength = options.MinStringInternLength;
MaxStringInternLength = options.MaxStringInternLength;
PropertyFilter = options.PropertyFilter;
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);

View File

@ -1,4 +1,4 @@
using AyCode.Core.Helpers;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Expressions;
using System.Buffers;
using System.Collections;
@ -597,7 +597,11 @@ public static partial class AcBinarySerializer
return;
}
if (context.UseStringInterning && value.Length >= context.MinStringInternLength)
// String interning: only for strings within length range
// MaxStringInternLength == 0 means no max limit
if (context.UseStringInterning
&& value.Length >= context.MinStringInternLength
&& (context.MaxStringInternLength == 0 || value.Length <= context.MaxStringInternLength))
{
var index = context.RegisterInternedString(value);
context.WriteByte(BinaryTypeCode.StringInterned);
@ -642,12 +646,12 @@ public static partial class AcBinarySerializer
case AcSerializerCommon.IdAccessorType.Int32:
if (!context.TryTrack(wrapper, value, out int intId))
{
// Already seen write reference
// Already seen write reference
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(intId);
return;
}
// First occurrence write object with refId
// First occurrence write object with refId
context.WriteByte(BinaryTypeCode.Object);
context.WriteVarInt(intId);
break;
@ -747,7 +751,7 @@ public static partial class AcBinarySerializer
return false;
default:
// Object type - use regular getter
var value = prop.GetDynamicValue(obj);
var value = prop.GetValue(obj);
if (value == null) return true;
if (prop.PropertyTypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value);
return false;
@ -818,7 +822,7 @@ public static partial class AcBinarySerializer
return;
default:
// Fallback to object getter for reference types
var value = prop.GetDynamicValue(obj);
var value = prop.GetValue(obj);
WriteValue(value, prop.PropertyType, context, depth);
return;
}
@ -975,7 +979,7 @@ public static partial class AcBinarySerializer
default:
{
// Object type - use regular getter
var value = prop.GetDynamicValue(obj);
var value = prop.GetValue(obj);
// SKIP marker only for null (reference types)
// Empty string, empty collections, etc. are valid values and must be written!
@ -1211,4 +1215,4 @@ public static partial class AcBinarySerializer
// Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs
#endregion
}
}

View File

@ -102,6 +102,14 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// </summary>
public byte MinStringInternLength { get; init; } = 4;
/// <summary>
/// Maximum string length to consider for interning.
/// Longer strings (descriptions, notes, etc.) are usually unique and not worth interning.
/// Set to 0 to disable max limit.
/// Default: 64 (strings longer than 64 chars are not interned)
/// </summary>
public byte MaxStringInternLength { get; init; } = 64;
/// <summary>
/// Initial capacity for serialization buffer.
/// Default: 4096 bytes

View File

@ -116,7 +116,7 @@ public static partial class AcJsonDeserializer
for (var i = 0; i < props.Length; i++)
{
var prop = props[i];
var value = prop.GetDynamicValue(source);
var value = prop.GetValue(source);
if (value != null)
prop.SetValue(target, value);
}
@ -176,7 +176,7 @@ public static partial class AcJsonDeserializer
// Handle IId collection merge
if (propInfo.IsIIdCollection && propValueKind == JsonValueKind.Array)
{
var existingCollection = propInfo.GetDynamicValue(target);
var existingCollection = propInfo.GetValue(target);
if (existingCollection != null)
{
MergeIIdCollection(propValue, existingCollection, propInfo, context, depth);
@ -201,7 +201,7 @@ public static partial class AcJsonDeserializer
// Merge into existing object
if (!propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
{
var existingObj = propInfo.GetDynamicValue(target);
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);

View File

@ -489,7 +489,7 @@ public static partial class AcJsonDeserializer
// Handle IId collection merge
if (propInfo.IsIIdCollection && tokenType == JsonTokenType.StartArray)
{
var existingCollection = propInfo.GetDynamicValue(target);
var existingCollection = propInfo.GetValue(target);
if (existingCollection != null)
{
MergeIIdCollectionFromReader(ref reader, existingCollection, propInfo, maxDepth, depth);
@ -500,7 +500,7 @@ public static partial class AcJsonDeserializer
// Handle nested objects - merge into existing
if (tokenType == JsonTokenType.StartObject && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
{
var existingObj = propInfo.GetDynamicValue(target);
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
@ -611,7 +611,7 @@ public static partial class AcJsonDeserializer
{
foreach (var prop in metadata.PropertySettersFrozen.Values)
{
var value = prop.GetDynamicValue(source);
var value = prop.GetValue(source);
if (value != null)
prop.SetValue(target, value);
}

View File

@ -149,7 +149,7 @@ public static partial class AcJsonSerializer
var propCount = props.Length;
for (var i = 0; i < propCount; i++)
{
var propValue = props[i].GetDynamicValue(value);
var propValue = props[i].GetValue(value);
if (propValue != null) ScanReferences(propValue, context, depth + 1);
}
}
@ -205,7 +205,7 @@ public static partial class AcJsonSerializer
for (var i = 0; i < propCount; i++)
{
var prop = props[i];
var propValue = prop.GetDynamicValue(value);
var propValue = prop.GetValue(value);
if (propValue == null) continue;
if (IsDefaultValueFast(propValue, prop.PropertyTypeCode, prop.PropertyType)) continue;

View File

@ -1,90 +1,16 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers;
/// <summary>
/// Enum for typed property accessor dispatch.
/// </summary>
public enum PropertyAccessorType : byte
{
Object = 0,
Int32,
Int64,
Boolean,
Double,
Single,
Decimal,
DateTime,
Byte,
Int16,
UInt16,
UInt32,
UInt64,
Guid,
Enum
}
/// <summary>
/// Base class for property accessors used by all serializers.
/// Contains common property metadata, getter functionality, and typed delegate fields.
/// Base class for property accessors used by serializers.
/// Contains getter functionality and typed delegate fields.
/// Typed getters eliminate runtime cast overhead for value type properties.
/// </summary>
public abstract class PropertyAccessorBase
public abstract class PropertyAccessorBase : PropertyMetadataBase
{
/// <summary>
/// Property name.
/// </summary>
public string Name { get; }
/// <summary>
/// Pre-encoded UTF8 bytes of property name for fast matching.
/// </summary>
public byte[] NameUtf8 { get; }
/// <summary>
/// The property type (may be nullable).
/// </summary>
public Type PropertyType { get; }
/// <summary>
/// The underlying type (unwrapped from Nullable if applicable).
/// </summary>
public Type UnderlyingType { get; }
/// <summary>
/// Cached TypeCode for fast primitive type dispatch.
/// </summary>
public TypeCode PropertyTypeCode { get; }
/// <summary>
/// Whether the property type is nullable.
/// </summary>
public bool IsNullable { get; }
/// <summary>
/// The declaring type of this property.
/// </summary>
public Type DeclaringType { get; }
/// <summary>
/// True if this property needs recursive scanning (not primitive/string).
/// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
/// </summary>
public bool IsComplexType { get; }
/// <summary>
/// The accessor type for fast typed getter dispatch.
/// </summary>
public PropertyAccessorType AccessorType { get; }
/// <summary>
/// Compiled getter delegate for reading property values (boxed).
/// </summary>
protected readonly Func<object, object?> _dynamicGetter;
#region Strongly-typed getter delegate fields (eliminates runtime cast)
// Only ONE of these is set based on AccessorType
@ -104,58 +30,12 @@ public abstract class PropertyAccessorBase
#endregion
protected PropertyAccessorBase(PropertyInfo prop, Type declaringType)
protected PropertyAccessorBase(PropertyInfo prop, Type declaringType)
: base(prop, declaringType)
{
Name = prop.Name;
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
DeclaringType = declaringType;
PropertyType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(PropertyType);
IsNullable = underlying != null;
UnderlyingType = underlying ?? PropertyType;
PropertyTypeCode = Type.GetTypeCode(UnderlyingType);
// Pre-compute: is this a complex type that needs recursive handling?
IsComplexType = !IsPrimitiveOrStringFast(PropertyType);
_dynamicGetter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop);
// Initialize typed getter
AccessorType = DetermineAccessorType(PropertyType);
InitializeTypedGetter(declaringType, prop);
}
private static PropertyAccessorType DetermineAccessorType(Type propType)
{
var underlying = Nullable.GetUnderlyingType(propType);
if (underlying != null)
return PropertyAccessorType.Object;
if (propType.IsEnum)
return PropertyAccessorType.Enum;
if (ReferenceEquals(propType, GuidType))
return PropertyAccessorType.Guid;
return Type.GetTypeCode(propType) switch
{
TypeCode.Int32 => PropertyAccessorType.Int32,
TypeCode.Int64 => PropertyAccessorType.Int64,
TypeCode.Boolean => PropertyAccessorType.Boolean,
TypeCode.Double => PropertyAccessorType.Double,
TypeCode.Single => PropertyAccessorType.Single,
TypeCode.Decimal => PropertyAccessorType.Decimal,
TypeCode.DateTime => PropertyAccessorType.DateTime,
TypeCode.Byte => PropertyAccessorType.Byte,
TypeCode.Int16 => PropertyAccessorType.Int16,
TypeCode.UInt16 => PropertyAccessorType.UInt16,
TypeCode.UInt32 => PropertyAccessorType.UInt32,
TypeCode.UInt64 => PropertyAccessorType.UInt64,
_ => PropertyAccessorType.Object
};
}
private void InitializeTypedGetter(Type declaringType, PropertyInfo prop)
{
switch (AccessorType)
@ -207,12 +87,6 @@ public abstract class PropertyAccessorBase
#region Typed Getters - Direct invocation, no cast!
/// <summary>
/// Gets the property value from the target object (boxed).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetDynamicValue(object obj) => _dynamicGetter(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetInt32(object obj) => _int32Getter!(obj);

View File

@ -0,0 +1,143 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers;
/// <summary>
/// Enum for typed property accessor dispatch.
/// </summary>
public enum PropertyAccessorType : byte
{
Object = 0,
Int32,
Int64,
Boolean,
Double,
Single,
Decimal,
DateTime,
Byte,
Int16,
UInt16,
UInt32,
UInt64,
Guid,
Enum
}
/// <summary>
/// Base class containing common property metadata shared by serializers and deserializers.
/// Contains the dynamic getter used by both serialize (for reading) and deserialize (for Populate/Merge).
/// </summary>
public abstract class PropertyMetadataBase
{
/// <summary>
/// Property name.
/// </summary>
public string Name { get; }
/// <summary>
/// Pre-encoded UTF8 bytes of property name for fast matching.
/// </summary>
public byte[] NameUtf8 { get; }
/// <summary>
/// The property type (may be nullable).
/// </summary>
public Type PropertyType { get; }
/// <summary>
/// The underlying type (unwrapped from Nullable if applicable).
/// </summary>
public Type UnderlyingType { get; }
/// <summary>
/// Cached TypeCode for fast primitive type dispatch.
/// </summary>
public TypeCode PropertyTypeCode { get; }
/// <summary>
/// Whether the property type is nullable.
/// </summary>
public bool IsNullable { get; }
/// <summary>
/// The declaring type of this property.
/// </summary>
public Type DeclaringType { get; }
/// <summary>
/// True if this property needs recursive scanning (not primitive/string).
/// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
/// </summary>
public bool IsComplexType { get; }
/// <summary>
/// The accessor type for fast typed getter/setter dispatch.
/// </summary>
public PropertyAccessorType AccessorType { get; }
/// <summary>
/// Compiled getter delegate for reading property values (boxed).
/// Used by serialize (for reading values) and deserialize (for Populate/Merge to get existing references).
/// </summary>
protected readonly Func<object, object?> _dynamicGetter;
protected PropertyMetadataBase(PropertyInfo prop, Type declaringType)
{
Name = prop.Name;
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
DeclaringType = declaringType;
PropertyType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(PropertyType);
IsNullable = underlying != null;
UnderlyingType = underlying ?? PropertyType;
PropertyTypeCode = Type.GetTypeCode(UnderlyingType);
// Pre-compute: is this a complex type that needs recursive handling?
IsComplexType = !IsPrimitiveOrStringFast(PropertyType);
AccessorType = DetermineAccessorType(PropertyType);
_dynamicGetter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop);
}
/// <summary>
/// Gets the property value from the target object (boxed).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object obj) => _dynamicGetter(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static PropertyAccessorType DetermineAccessorType(Type propType)
{
var underlying = Nullable.GetUnderlyingType(propType);
if (underlying != null)
return PropertyAccessorType.Object;
if (propType.IsEnum)
return PropertyAccessorType.Enum;
if (ReferenceEquals(propType, GuidType))
return PropertyAccessorType.Guid;
return Type.GetTypeCode(propType) switch
{
TypeCode.Int32 => PropertyAccessorType.Int32,
TypeCode.Int64 => PropertyAccessorType.Int64,
TypeCode.Boolean => PropertyAccessorType.Boolean,
TypeCode.Double => PropertyAccessorType.Double,
TypeCode.Single => PropertyAccessorType.Single,
TypeCode.Decimal => PropertyAccessorType.Decimal,
TypeCode.DateTime => PropertyAccessorType.DateTime,
TypeCode.Byte => PropertyAccessorType.Byte,
TypeCode.Int16 => PropertyAccessorType.Int16,
TypeCode.UInt16 => PropertyAccessorType.UInt16,
TypeCode.UInt32 => PropertyAccessorType.UInt32,
TypeCode.UInt64 => PropertyAccessorType.UInt64,
_ => PropertyAccessorType.Object
};
}
}

View File

@ -6,10 +6,12 @@ using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers;
/// <summary>
/// Base class for property accessors that also support setting values.
/// Used by deserializers. Extends PropertyAccessorBase with typed setters and IId collection support.
/// Base class for property setters used by deserializers.
/// Derives from PropertyMetadataBase (not PropertyAccessorBase) to avoid unnecessary typed getter delegates.
/// Contains setter functionality and typed setter delegates.
/// Inherits GetValue() from PropertyMetadataBase for Populate/Merge operations.
/// </summary>
public abstract class PropertySetterBase : PropertyAccessorBase
public abstract class PropertySetterBase : PropertyMetadataBase
{
/// <summary>
/// Compiled setter delegate for writing property values (boxed).

View File

@ -247,7 +247,7 @@ public static partial class AcToonSerializer
// Write properties
foreach (var prop in metadata.Properties)
{
var propValue = prop.GetDynamicValue(value);
var propValue = prop.GetValue(value);
// Skip null/default values if option is set
if (context.Options.OmitDefaultValues && prop.IsDefaultValue(propValue))

View File

@ -313,7 +313,7 @@ public static partial class AcToonSerializer
var metadata = GetTypeMetadata(type);
foreach (var prop in metadata.Properties)
{
var propValue = prop.GetDynamicValue(value);
var propValue = prop.GetValue(value);
if (propValue != null) ScanReferences(propValue, context, depth + 1);
}
}