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:
parent
75823d593b
commit
905b1c404d
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue