diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs index cc6a79c..bcedbe8 100644 --- a/AyCode.Benchmark/SerializationBenchmarks.cs +++ b/AyCode.Benchmark/SerializationBenchmarks.cs @@ -274,8 +274,8 @@ public class AcBinaryVsMessagePackFullBenchmark sharedUser: sharedUser, sharedMetadata: sharedMeta); - // Setup options - enforce no reference handling everywhere - _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + // Setup options - WithRef uses Default (which has reference handling), NoRef explicitly disables it + _withRefOptions = AcBinarySerializerOptions.Default; _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); @@ -315,13 +315,16 @@ public class AcBinaryVsMessagePackFullBenchmark Console.WriteLine($" MessagePack: {_msgPackData.Length,8:N0} bytes"); Console.WriteLine($" BSON: {_bsonData.Length,8:N0} bytes"); Console.WriteLine(new string('-', 60)); - Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryWithRef.Length / Math.Max(1, _msgPackData.Length):F1}% (WithRef - actually NoRef)"); + Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryWithRef.Length / Math.Max(1, _msgPackData.Length):F1}% (WithRef)"); Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryNoRef.Length / Math.Max(1, _msgPackData.Length):F1}% (NoRef)"); Console.WriteLine(new string('=', 60) + "\n"); } #region Serialize Benchmarks + [Benchmark(Description = "AcBinary Serialize WithRef")] + public byte[] Serialize_AcBinary_WithRef() => AcBinarySerializer.Serialize(_testOrder, _withRefOptions); + [Benchmark(Description = "AcBinary Serialize NoRef")] public byte[] Serialize_AcBinary_NoRef() => AcBinarySerializer.Serialize(_testOrder, _noRefOptions); @@ -335,6 +338,9 @@ public class AcBinaryVsMessagePackFullBenchmark #region Deserialize Benchmarks + [Benchmark(Description = "AcBinary Deserialize WithRef")] + public TestOrder? Deserialize_AcBinary_WithRef() => AcBinaryDeserializer.Deserialize(_acBinaryWithRef); + [Benchmark(Description = "AcBinary Deserialize NoRef")] public TestOrder? Deserialize_AcBinary_NoRef() => AcBinaryDeserializer.Deserialize(_acBinaryNoRef); @@ -354,6 +360,13 @@ public class AcBinaryVsMessagePackFullBenchmark #region Populate Benchmarks + [Benchmark(Description = "AcBinary Populate WithRef")] + public void Populate_AcBinary_WithRef() + { + var target = CreatePopulateTarget(); + AcBinaryDeserializer.Populate(_acBinaryWithRef, target); + } + [Benchmark(Description = "AcBinary Populate NoRef")] public void Populate_AcBinary_NoRef() { @@ -362,6 +375,13 @@ public class AcBinaryVsMessagePackFullBenchmark AcBinaryDeserializer.Populate(_acBinaryNoRef, target); } + [Benchmark(Description = "AcBinary PopulateMerge WithRef")] + public void PopulateMerge_AcBinary_WithRef() + { + var target = CreatePopulateTarget(); + AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target); + } + [Benchmark(Description = "AcBinary PopulateMerge NoRef")] public void PopulateMerge_AcBinary_NoRef() { @@ -631,4 +651,62 @@ public class LargeScaleBinaryBenchmark [Benchmark(Description = "LargeScale MsgPack Serialize")] public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); +} + +/// +/// AcJson vs System.Text.Json comparison - measures Newtonsoft.Json based AcJson against modern STJ. +/// Uses simple flat object (PrimitiveTestClass) to avoid circular reference issues. +/// +[ShortRunJob] +[MemoryDiagnoser] +[RankColumn] +public class AcJsonVsSystemTextJsonBenchmark +{ + private PrimitiveTestClass _testData = null!; + private string _acJsonData = null!; + private string _stjData = null!; + private AcJsonSerializerOptions _acJsonOptions = null!; + private JsonSerializerOptions _stjOptions = null!; + + [GlobalSetup] + public void Setup() + { + Console.WriteLine("Creating test data for AcJson vs System.Text.Json..."); + + // Use simple flat object to avoid circular reference issues + _testData = TestDataFactory.CreatePrimitiveTestData(); + + // Setup options + _acJsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + _stjOptions = new JsonSerializerOptions + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + ReferenceHandler = null // No reference handling + }; + + // Pre-serialize + _acJsonData = AcJsonSerializer.Serialize(_testData, _acJsonOptions); + _stjData = JsonSerializer.Serialize(_testData, _stjOptions); + + Console.WriteLine($"AcJson size: {_acJsonData.Length:N0} chars"); + Console.WriteLine($"STJ size: {_stjData.Length:N0} chars"); + Console.WriteLine($"Size ratio: {100.0 * _acJsonData.Length / _stjData.Length:F1}%"); + } + + [Benchmark(Description = "AcJson Serialize", Baseline = true)] + public string Serialize_AcJson() => + AcJsonSerializer.Serialize(_testData, _acJsonOptions); + + [Benchmark(Description = "System.Text.Json Serialize")] + public string Serialize_STJ() => + JsonSerializer.Serialize(_testData, _stjOptions); + + [Benchmark(Description = "AcJson Deserialize")] + public PrimitiveTestClass? Deserialize_AcJson() => + AcJsonDeserializer.Deserialize(_acJsonData, _acJsonOptions); + + [Benchmark(Description = "System.Text.Json Deserialize")] + public PrimitiveTestClass? Deserialize_STJ() => + JsonSerializer.Deserialize(_stjData, _stjOptions); } \ No newline at end of file diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs index c9f8976..aeffeb1 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs @@ -185,6 +185,30 @@ public class AcBinarySerializerIIdReferenceTests Console.WriteLine($"Binary size: {binary.Length} bytes"); Console.WriteLine($"ObjectRef count: {objectRefCount}"); + // Assert 3: Reference identity - same TYPE with same Id should be same reference + // Tags with Id=1 should all be same reference + Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, + "CRITICAL: Item[0].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, + "CRITICAL: Item[1].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, + "CRITICAL: Item[2].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); + + // Users with Id=1 should all be same reference + Assert.AreSame(result.Owner, result.Items[0].Assignee, + "CRITICAL: Item[0].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + Assert.AreSame(result.Owner, result.Items[1].Assignee, + "CRITICAL: Item[1].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + Assert.AreSame(result.Owner, result.Items[2].Assignee, + "CRITICAL: Item[2].Assignee should be same reference as Owner (same SharedUser.Id=1)"); + + // Assert 4: Different TYPES with same Id should NOT be same reference! + Assert.AreNotSame(result.PrimaryTag, result.Owner, + "CRITICAL BUG: Tag and User are same reference! Types with same int Id were confused!"); + Assert.AreNotSame(result.PrimaryTag, result.Category, + "CRITICAL BUG: Tag and Category are same reference! Types with same int Id were confused!"); + Assert.AreNotSame(result.Owner, result.Category, + "CRITICAL BUG: User and Category are same reference! Types with same int Id were confused!"); // 4 Tags, 4 Users - each should have 3 ObjectRefs = 6 total minimum Assert.IsTrue(objectRefCount >= 6, $"CRITICAL: Expected at least 6 ObjectRef entries (3 per type for Tag and User), found {objectRefCount}. " + @@ -226,31 +250,6 @@ public class AcBinarySerializerIIdReferenceTests Assert.AreEqual(1, result.Items[i].Assignee!.Id, $"Items[{i}].Assignee.Id incorrect"); Assert.AreEqual("User_Id1", result.Items[i].Assignee.Username, $"Items[{i}].Assignee.Username incorrect - confused with Tag?"); } - - // Assert 3: Reference identity - same TYPE with same Id should be same reference - // Tags with Id=1 should all be same reference - Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, - "CRITICAL: Item[0].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); - Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, - "CRITICAL: Item[1].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); - Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, - "CRITICAL: Item[2].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); - - // Users with Id=1 should all be same reference - Assert.AreSame(result.Owner, result.Items[0].Assignee, - "CRITICAL: Item[0].Assignee should be same reference as Owner (same SharedUser.Id=1)"); - Assert.AreSame(result.Owner, result.Items[1].Assignee, - "CRITICAL: Item[1].Assignee should be same reference as Owner (same SharedUser.Id=1)"); - Assert.AreSame(result.Owner, result.Items[2].Assignee, - "CRITICAL: Item[2].Assignee should be same reference as Owner (same SharedUser.Id=1)"); - - // Assert 4: Different TYPES with same Id should NOT be same reference! - Assert.AreNotSame(result.PrimaryTag, result.Owner, - "CRITICAL BUG: Tag and User are same reference! Types with same int Id were confused!"); - Assert.AreNotSame(result.PrimaryTag, result.Category, - "CRITICAL BUG: Tag and Category are same reference! Types with same int Id were confused!"); - Assert.AreNotSame(result.Owner, result.Category, - "CRITICAL BUG: User and Category are same reference! Types with same int Id were confused!"); } /// @@ -434,6 +433,33 @@ public class AcBinarySerializerIIdReferenceTests #region Data Integrity Tests + /// + /// Diagnostic test to verify IId detection works correctly. + /// + [TestMethod] + public void IIdDetection_Diagnostic() + { + // Test GetIdInfo directly + var sharedTagType = typeof(SharedTag); + var idInfo = AyCode.Core.Helpers.JsonUtilities.GetIdInfo(sharedTagType); + + Console.WriteLine($"SharedTag GetIdInfo: IsId={idInfo.IsId}, IdType={idInfo.IdType?.Name}"); + Assert.IsTrue(idInfo.IsId, "SharedTag should be detected as IId"); + Assert.AreEqual(typeof(int), idInfo.IdType, "SharedTag Id type should be int"); + + // Test SharedUser + var sharedUserType = typeof(SharedUser); + var userIdInfo = AyCode.Core.Helpers.JsonUtilities.GetIdInfo(sharedUserType); + Console.WriteLine($"SharedUser GetIdInfo: IsId={userIdInfo.IsId}, IdType={userIdInfo.IdType?.Name}"); + Assert.IsTrue(userIdInfo.IsId, "SharedUser should be detected as IId"); + + // Test TestGuidItem + var guidItemType = typeof(TestGuidItem); + var guidIdInfo = AyCode.Core.Helpers.JsonUtilities.GetIdInfo(guidItemType); + Console.WriteLine($"TestGuidItem GetIdInfo: IsId={guidIdInfo.IsId}, IdType={guidIdInfo.IdType?.Name}"); + Assert.IsTrue(guidIdInfo.IsId, "TestGuidItem should be detected as IId"); + } + /// /// Verify data is correct regardless of reference handling. /// diff --git a/AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs b/AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs index 40d0084..884f85c 100644 --- a/AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs +++ b/AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs @@ -46,16 +46,11 @@ public class ChainReferenceDebugTest // First populate chain.ThenPopulate(list1); - Console.WriteLine($"List1 count: {list1.Count}, ID: {list1[0].Id}, Name: {list1[0].Name}"); // Second populate chain.ThenPopulate(list2); - Console.WriteLine($"List2 count: {list2.Count}, ID: {list2[0].Id}, Name: {list2[0].Name}"); - - // Check if same reference - Console.WriteLine($"Same reference: {ReferenceEquals(list1[0], list2[0])}"); - Console.WriteLine($"List1[0] hash: {list1[0].GetHashCode()}, List2[0] hash: {list2[0].GetHashCode()}"); + // Both list1[0] and list2[0] should be the same object reference Assert.AreSame(list1[0], list2[0], "Should be same object reference!"); } } diff --git a/AyCode.Core/Helpers/JsonUtilities.cs b/AyCode.Core/Helpers/JsonUtilities.cs index fb4d26b..58830e3 100644 --- a/AyCode.Core/Helpers/JsonUtilities.cs +++ b/AyCode.Core/Helpers/JsonUtilities.cs @@ -508,7 +508,8 @@ public static class JsonUtilities if (!iface.IsGenericType) continue; if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue; var idType = iface.GetGenericArguments()[0]; - return new IdTypeInfo(idType.IsValueType, idType); + // FIXED: IsId should be true if IId interface is found, not idType.IsValueType + return new IdTypeInfo(true, idType); } return new IdTypeInfo(false, null); }); diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 70b37fc..a59ce38 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using AyCode.Core.Helpers; using AyCode.Core.Serializers.Expressions; using AyCode.Core.Serializers.Jsons; using LExpression = System.Linq.Expressions.Expression; @@ -586,6 +587,202 @@ public static class AcSerializerCommon return LExpression.Lambda>(assign, objParam, valueParam).Compile(); } + + #endregion + + #region IId Reference Tracking + + /// + /// Specifies the accessor type for IId.Id property to enable typed getter dispatch without boxing. + /// + public enum IdAccessorType : byte + { + /// Type does not implement IId. + None = 0, + /// Id is int (most common). + Int32 = 1, + /// Id is long. + Int64 = 2, + /// Id is Guid. + Guid = 3, + /// Id is an exotic type (uses boxing fallback). + Object = 255 + } + + /// + /// IId-based reference tracking for serialization. + /// Supplements (not replaces) the ReferenceEquals-based SerializationReferenceTracker. + /// Tracks objects by (Type, Id) key to detect same IId across different instances. + /// Uses typed dictionaries for int/long/Guid to avoid boxing overhead. + /// Stores the original object to enable marking it as multi-referenced when a duplicate is found. + /// + public sealed class IIdReferenceTracker + { + // Typed dictionaries for common Id types (no boxing!) + // Value is the first object registered with that (Type, Id) key + private Dictionary<(Type, int), object>? _int32Cache; + private Dictionary<(Type, long), object>? _int64Cache; + private Dictionary<(Type, Guid), object>? _guidCache; + private Dictionary<(Type, object), object>? _objectCache; // Fallback for exotic types + + /// + /// Tries to get existing object for an IId. + /// Returns true if same (Type, Id) was already tracked. + /// The out parameter contains the ORIGINAL object that was first registered. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetOriginalObject(object obj, TMetadata metadata, out object? originalObject) + where TMetadata : TypeMetadataBase + { + originalObject = null; + if (!metadata.IsIId) return false; + + return metadata.IdAccessorType switch + { + IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject), + IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject), + IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject), + IdAccessorType.Object => TryGetObjectOriginal(obj, metadata, out originalObject), + _ => false + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetInt32Original(object obj, TMetadata metadata, out object? originalObject) + where TMetadata : TypeMetadataBase + { + originalObject = null; + if (_int32Cache == null) return false; + + var id = metadata.GetIdInt32(obj); + if (id == 0) return false; // Skip default Id + + var key = (obj.GetType(), id); + return _int32Cache.TryGetValue(key, out originalObject); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetInt64Original(object obj, TMetadata metadata, out object? originalObject) + where TMetadata : TypeMetadataBase + { + originalObject = null; + if (_int64Cache == null) return false; + + var id = metadata.GetIdInt64(obj); + if (id == 0) return false; // Skip default Id + + var key = (obj.GetType(), id); + return _int64Cache.TryGetValue(key, out originalObject); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetGuidOriginal(object obj, TMetadata metadata, out object? originalObject) + where TMetadata : TypeMetadataBase + { + originalObject = null; + if (_guidCache == null) return false; + + var id = metadata.GetIdGuid(obj); + if (id == Guid.Empty) return false; // Skip default Id + + var key = (obj.GetType(), id); + return _guidCache.TryGetValue(key, out originalObject); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetObjectOriginal(object obj, TMetadata metadata, out object? originalObject) + where TMetadata : TypeMetadataBase + { + originalObject = null; + if (_objectCache == null || metadata.IdGetter == null) return false; + + var id = metadata.IdGetter(obj); + if (id == null) return false; + + var key = (obj.GetType(), id); + return _objectCache.TryGetValue(key, out originalObject); + } + + /// + /// Registers an IId object. + /// + public void Register(object obj, TMetadata metadata) + where TMetadata : TypeMetadataBase + { + if (!metadata.IsIId) return; + + switch (metadata.IdAccessorType) + { + case IdAccessorType.Int32: + RegisterInt32(obj, metadata); + break; + case IdAccessorType.Int64: + RegisterInt64(obj, metadata); + break; + case IdAccessorType.Guid: + RegisterGuid(obj, metadata); + break; + case IdAccessorType.Object: + RegisterObject(obj, metadata); + break; + } + } + + private void RegisterInt32(object obj, TMetadata metadata) + where TMetadata : TypeMetadataBase + { + var id = metadata.GetIdInt32(obj); + if (id == 0) return; // Skip default Id + + _int32Cache ??= new Dictionary<(Type, int), object>(64); + _int32Cache[(obj.GetType(), id)] = obj; + } + + private void RegisterInt64(object obj, TMetadata metadata) + where TMetadata : TypeMetadataBase + { + var id = metadata.GetIdInt64(obj); + if (id == 0) return; // Skip default Id + + _int64Cache ??= new Dictionary<(Type, long), object>(64); + _int64Cache[(obj.GetType(), id)] = obj; + } + + private void RegisterGuid(object obj, TMetadata metadata) + where TMetadata : TypeMetadataBase + { + var id = metadata.GetIdGuid(obj); + if (id == Guid.Empty) return; // Skip default Id + + _guidCache ??= new Dictionary<(Type, Guid), object>(64); + _guidCache[(obj.GetType(), id)] = obj; + } + + private void RegisterObject(object obj, TMetadata metadata) + where TMetadata : TypeMetadataBase + { + if (metadata.IdGetter == null) return; + + var id = metadata.IdGetter(obj); + if (id == null) return; + + _objectCache ??= new Dictionary<(Type, object), object>(64); + _objectCache[(obj.GetType(), id)] = obj; + } + + /// + /// Clears all tracking data for reuse. + /// + public void Reset() + { + _int32Cache?.Clear(); + _int64Cache?.Clear(); + _guidCache?.Clear(); + _objectCache?.Clear(); + } + } + + #endregion #region Chain Reference Tracking @@ -676,11 +873,13 @@ public static class AcSerializerCommon #endregion + #region Serialization Reference Tracking /// /// Common reference tracking for serialization. /// Used by both JSON and Binary serializers to track multi-referenced objects. + /// Supports both ReferenceEquals-based tracking and IId-based tracking. /// Uses int IDs for efficiency (no string allocation). /// public sealed class SerializationReferenceTracker @@ -690,8 +889,12 @@ public static class AcSerializerCommon private Dictionary? _scanOccurrences; private Dictionary? _writtenRefs; + private Dictionary<(Type, object), int>? _iidWrittenRefs; // IId-based written refs private HashSet? _multiReferenced; private int _nextRefId = 1; + + // IId tracker for same-IId-different-instance deduplication + private IIdReferenceTracker? _iidTracker; /// /// Resets the tracker for reuse. @@ -701,7 +904,9 @@ public static class AcSerializerCommon _nextRefId = 1; _scanOccurrences?.Clear(); _writtenRefs?.Clear(); + _iidWrittenRefs?.Clear(); _multiReferenced?.Clear(); + _iidTracker?.Reset(); } /// @@ -713,6 +918,7 @@ public static class AcSerializerCommon { _scanOccurrences ??= new Dictionary(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); _writtenRefs ??= new Dictionary(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); + _iidWrittenRefs ??= new Dictionary<(Type, object), int>(InitialMultiRefCapacity); _multiReferenced ??= new HashSet(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance); } @@ -726,6 +932,7 @@ public static class AcSerializerCommon { if (_scanOccurrences == null) return true; + ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); if (exists) { @@ -736,6 +943,51 @@ public static class AcSerializerCommon count = 1; return true; } + + /// + /// Extended tracking with IId support. + /// First checks IId match (different instance, same Id), then falls back to ReferenceEquals. + /// Returns true if this is the first occurrence (continue scanning children). + /// Returns false if already seen (stop scanning this branch). + /// + /// Object to track + /// Type metadata with IId info + /// If returning false, contains the refId of the existing object (unused, kept for API compatibility) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrackForScanningWithIId(object obj, TMetadata metadata, out int existingRefId) + where TMetadata : TypeMetadataBase + { + existingRefId = 0; + if (_scanOccurrences == null) return true; + + // 1. IId check first (different instance, same Id) + if (metadata.IsIId && _iidTracker != null && _iidTracker.TryGetOriginalObject(obj, metadata, out var originalObject)) + { + // Same IId already seen → mark BOTH original and current as multi-referenced + _multiReferenced!.Add(originalObject!); // Original object + _multiReferenced.Add(obj); // Current object (duplicate) + return false; + } + + // 2. ReferenceEquals check (same instance) + ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); + if (exists) + { + count++; + _multiReferenced!.Add(obj); + return false; + } + count = 1; + + // 3. Register IId for future lookups + if (metadata.IsIId) + { + _iidTracker ??= new IIdReferenceTracker(); + _iidTracker.Register(obj, metadata); + } + + return true; + } /// /// Checks if object needs a reference ID during serialization. @@ -759,23 +1011,67 @@ public static class AcSerializerCommon [MethodImpl(MethodImplOptions.AggressiveInlining)] public void MarkAsWritten(object obj, int refId) { + var type = obj.GetType(); + var idInfo = JsonUtilities.GetIdInfo(type); + + // IId típus → IId alapján tároljuk + if (idInfo.IsId && idInfo.IdType != null) + { + var key = GetIIdKey(obj, type, idInfo.IdType); + if (key.HasValue) + { + _iidWrittenRefs![key.Value] = refId; + return; + } + } + + // Nem IId → ReferenceEquals alapján _writtenRefs![obj] = refId; } /// /// Tries to get existing reference ID for an object. - /// Returns true if object was already written (use $ref instead of serializing again). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetExistingRef(object obj, out int refId) { + var type = obj.GetType(); + var idInfo = JsonUtilities.GetIdInfo(type); + + // IId típus → IId alapján keresünk + if (idInfo.IsId && idInfo.IdType != null && _iidWrittenRefs != null) + { + var key = GetIIdKey(obj, type, idInfo.IdType); + if (key.HasValue && _iidWrittenRefs.TryGetValue(key.Value, out refId)) + { + return true; + } + } + + // Nem IId → ReferenceEquals alapján if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId)) { return true; } + refId = 0; return false; } + + /// + /// Gets the (Type, Id) key for IId-based lookup. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (Type, object)? GetIIdKey(object obj, Type type, Type idType) + { + var idProp = type.GetProperty("Id"); + if (idProp == null) return null; + + var id = idProp.GetValue(obj); + if (id == null || JsonUtilities.IsDefaultValue(id, idType)) return null; + + return (type, id); + } } #endregion diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 7d222e6..0df74c8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -533,6 +533,7 @@ public static partial class AcBinaryDeserializer throw new AcBinaryDeserializationException($"Unknown object reference id '{refId}'.", _position); } + return value; } @@ -549,5 +550,92 @@ public static partial class AcBinaryDeserializer var byteLength = (int)ReadVarUInt(); return ReadStringUtf8(byteLength); } + + #region IId Reference Cache + + // Typed caches for IId-based deduplication (no boxing for int/long/Guid) + private Dictionary<(Type, int), object>? _iidCacheInt32; + private Dictionary<(Type, long), object>? _iidCacheLong; + private Dictionary<(Type, Guid), object>? _iidCacheGuid; + + /// + /// After PopulateObject, checks if we should reuse an existing IId object. + /// Returns the object to use (either the new one or an existing cached one). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata) + { + if (!metadata.IsIId) return newObj; + + return metadata.IdAccessorType switch + { + AcSerializerCommon.IdAccessorType.Int32 => GetOrRegisterInt32(newObj, metadata), + AcSerializerCommon.IdAccessorType.Int64 => GetOrRegisterInt64(newObj, metadata), + AcSerializerCommon.IdAccessorType.Guid => GetOrRegisterGuid(newObj, metadata), + _ => newObj // Object fallback - don't cache (boxing overhead) + }; + } + + private object GetOrRegisterInt32(object newObj, BinaryDeserializeTypeMetadata metadata) + { + var id = metadata.GetIdInt32(newObj); + if (id == 0) return newObj; // Default Id → no caching + + var key = (newObj.GetType(), id); + + _iidCacheInt32 ??= new Dictionary<(Type, int), object>(64); + + // If already exists → return existing + if (_iidCacheInt32.TryGetValue(key, out var existing)) + { + return existing; + } + + // New → register and return + _iidCacheInt32[key] = newObj; + return newObj; + } + + private object GetOrRegisterInt64(object newObj, BinaryDeserializeTypeMetadata metadata) + { + var id = metadata.GetIdInt64(newObj); + if (id == 0) return newObj; // Default Id → no caching + + var key = (newObj.GetType(), id); + + _iidCacheLong ??= new Dictionary<(Type, long), object>(64); + + // If already exists → return existing + if (_iidCacheLong.TryGetValue(key, out var existing)) + { + return existing; + } + + // New → register and return + _iidCacheLong[key] = newObj; + return newObj; + } + + private object GetOrRegisterGuid(object newObj, BinaryDeserializeTypeMetadata metadata) + { + var id = metadata.GetIdGuid(newObj); + if (id == Guid.Empty) return newObj; // Default Id → no caching + + var key = (newObj.GetType(), id); + + _iidCacheGuid ??= new Dictionary<(Type, Guid), object>(64); + + // If already exists → return existing + if (_iidCacheGuid.TryGetValue(key, out var existing)) + { + return existing; + } + + // New → register and return + _iidCacheGuid[key] = newObj; + return newObj; + } + + #endregion } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index 33d7cc2..c523a00 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -31,7 +31,8 @@ public static partial class AcBinaryDeserializer public BinaryDeserializeTypeMetadata(Type type) : base(type, HasJsonIgnoreAttribute) { - var orderedProperties = GetSerializableProperties(type, requiresWrite: true); + // Use pre-computed WritableProperties directly - no method call overhead! + var orderedProperties = WritableProperties; PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length]; for (var i = 0; i < orderedProperties.Length; i++) diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index ab61064..3846ef5 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -179,33 +179,11 @@ public static partial class AcBinaryDeserializer var count = (int)context.ReadVarUInt(); var nextDepth = depth + 1; - - // ChainMode: Use cached IId info from element type metadata - BinaryDeserializeTypeMetadata? elementMetadata = null; - if (context.IsChainMode && IsComplexType(elementType)) - { - elementMetadata = GetTypeMetadata(elementType); - } for (int i = 0; i < count; i++) { + // ReadValue handles ChainMode internally (ReadObject returns cached instance) var value = ReadValue(ref context, elementType, nextDepth); - - // ChainMode: Check if we already have this IId object using cached metadata - if (context.IsChainMode && value != null && elementMetadata != null && - elementMetadata.IsIId && elementMetadata.IdGetter != null && elementMetadata.IdType != null) - { - var id = elementMetadata.IdGetter(value); - if (id != null && !IsDefaultValue(id, elementMetadata.IdType)) - { - if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj)) - { - targetList.Add(existingObj); - continue; - } - } - } - targetList.Add(value); } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 346fade..a6ef251 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -943,11 +943,19 @@ public static partial class AcBinaryDeserializer // Deserialize mode: object just created, skip writing defaults (already at default) PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); - // ChainMode: Check if we already have an object with this Id in the tracker - // Use cached IdType from metadata instead of id.GetType() to avoid reflection overhead - if (context.IsChainMode && metadata.IsIId && metadata.IdGetter != null && metadata.IdType != null) + // ChainMode: Register/retrieve from chain tracker (separate from reference handling) + // Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None) + if (context.IsChainMode && metadata.IsIId && metadata.IdType != null) { - var id = metadata.IdGetter(instance); + object? id = metadata.IdAccessorType switch + { + AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance), + AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance), + AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance), + AcSerializerCommon.IdAccessorType.Object when metadata.IdGetter != null => metadata.IdGetter(instance), + _ => null + }; + if (id != null && !IsDefaultValue(id, metadata.IdType)) { // Check if we already have this object @@ -962,6 +970,11 @@ public static partial class AcBinaryDeserializer context.ChainTracker.TryRegisterIIdObject(instance); } } + // Normal IId cache for non-chain deserialization + else if (context.HasReferenceHandling && metadata.IsIId) + { + instance = context.GetOrRegisterIIdObject(instance, metadata); + } return instance; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 134bdad..ec21606 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -1153,6 +1153,21 @@ public static partial class AcBinarySerializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TrackForScanning(object obj) => _refTracker.TrackForScanning(obj); + + /// + /// IId-aware tracking for the scan phase. + /// First checks IId match (different instance, same Id), then falls back to ReferenceEquals. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrackForScanningWithIId(object obj, BinaryTypeMetadata metadata, out int existingRefId) + { + if (!UseReferenceHandling) + { + existingRefId = 0; + return true; // No tracking needed + } + return _refTracker.TrackForScanningWithIId(obj, metadata, out existingRefId); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ShouldWriteRef(object obj, out int refId) => _refTracker.ShouldWriteId(obj, out refId); diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs index b59c323..a2ad9f6 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs @@ -23,17 +23,38 @@ public static partial class AcBinarySerializer /// public Type? GeneratedSerializerType { get; } + + + public BinaryTypeMetadata(Type type, Func ignorePropertyFilter) : base(type,ignorePropertyFilter) { - var orderedProperties = GetSerializableProperties(type, requiresWrite: true); + // Use pre-computed WritableProperties directly - no method call overhead! + var orderedProperties = WritableProperties; Properties = new BinaryPropertyAccessor[orderedProperties.Length]; + var complexCount = 0; + for (var i = 0; i < orderedProperties.Length; i++) { var accessor = new BinaryPropertyAccessor(orderedProperties[i], type); accessor.PropertyIndex = i; Properties[i] = accessor; + + // Count complex properties using pre-computed IsComplexType + if (accessor.IsComplexType) + { + complexCount++; + } } + + // Set scan optimization flags + HasComplexProperties = complexCount > 0; + + // Type needs reference tracking if: + // 1. It's IId (can be deduplicated by Id) + // 2. It has complex properties (children could be shared) + // 3. It's not a primitive/string (could be referenced multiple times) + NeedsReferenceTracking = IsIId || HasComplexProperties || !IsPrimitiveOrStringFast(type); // Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false)) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index fca55f2..ce873ef 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -202,7 +202,16 @@ public static partial class AcBinarySerializer var type = value.GetType(); if (IsPrimitiveOrStringFast(type)) return; - if (!context.TrackForScanning(value)) return; + + // Get metadata for IId-aware tracking + var metadata = GetTypeMetadata(type); + + // OPTIMIZATION: Skip types that don't need reference tracking + // (no IId, no complex properties that could be shared) + if (!metadata.NeedsReferenceTracking) return; + + // Use IId-aware tracking if metadata is available and UseReferenceHandling is enabled + if (!context.TrackForScanningWithIId(value, metadata, out _)) return; if (value is byte[]) return; // byte arrays are value types @@ -226,13 +235,18 @@ public static partial class AcBinarySerializer return; } - var metadata = GetTypeMetadata(type); - var properties = metadata.Properties; + // OPTIMIZATION: Skip if no complex properties to scan + if (!metadata.HasComplexProperties) return; + // Scan only complex properties using pre-computed IsComplexType flag + var properties = metadata.Properties; for (var i = 0; i < properties.Length; i++) { var prop = properties[i]; + // Skip primitive properties - use pre-computed flag, no method call! + if (!prop.IsComplexType) continue; + if (!context.ShouldSerializeProperty(value, prop)) { continue; diff --git a/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs b/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs index 401a8f1..a1cd6cd 100644 --- a/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs +++ b/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs @@ -1,46 +1,19 @@ using System.Reflection; -using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers; /// /// Base class for deserializer type metadata. -/// Extends TypeMetadataBase with IId detection for efficient chain/merge operations. +/// Extends TypeMetadataBase for deserializer-specific functionality. /// Used by both JSON and Binary deserializers. /// Generic version provides built-in ThreadLocal caching with zero ref parameter overhead. +/// Note: IId detection (IsIId, IdType, IdGetter) is now in TypeMetadataBase for use by both serializers and deserializers. /// public abstract class DeserializeTypeMetadataBase : TypeMetadataBase where TMetadata : DeserializeTypeMetadataBase { - /// - /// Whether this type implements IId interface. - /// Cached at metadata creation time to avoid runtime reflection. - /// - public bool IsIId { get; } - - /// - /// The Id property type if IsIId is true, null otherwise. - /// - public Type? IdType { get; } - - /// - /// Compiled getter for the Id property (if IsIId is true). - /// Pre-compiled delegate avoids reflection overhead during deserialization. - /// - public Func? IdGetter { get; } - protected DeserializeTypeMetadataBase(Type type, Func ignorePropertyFilter) : base(type, ignorePropertyFilter) { - // Cache IId info at construction time - no runtime reflection needed later! - var idInfo = GetIdInfo(type); - IsIId = idInfo.IsId; - IdType = idInfo.IdType; - - if (IsIId) - { - var idProp = type.GetProperty("Id"); - if (idProp != null) - IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp); - } + // IId info is now initialized in TypeMetadataBase constructor } } diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs index 5833518..2b00f17 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs @@ -26,7 +26,8 @@ public static partial class AcJsonDeserializer public JsonDeserializeTypeMetadata(Type type) : base(type, HasJsonIgnoreAttribute) { - var props = GetSerializableProperties(type, requiresWrite: true); + // Use pre-computed WritableProperties directly - no method call overhead! + var props = WritableProperties; var propertySetters = new Dictionary(props.Length, StringComparer.OrdinalIgnoreCase); var propsArray = new PropertySetterInfo[props.Length]; diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs index 9cc5c09..16c8511 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs @@ -18,7 +18,8 @@ public static partial class AcJsonSerializer public JsonTypeMetadata(Type type) : base(type, JsonUtilities.HasJsonIgnoreAttribute) { - Properties = GetSerializableProperties(type, requiresWrite: false) + // Use pre-computed ReadableProperties directly - no method call overhead! + Properties = ReadableProperties .Select(p => new PropertyAccessor(p, type)) .ToArray(); } diff --git a/AyCode.Core/Serializers/PropertyAccessorBase.cs b/AyCode.Core/Serializers/PropertyAccessorBase.cs index 7e0a118..9519104 100644 --- a/AyCode.Core/Serializers/PropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/PropertyAccessorBase.cs @@ -1,6 +1,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; +using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers; @@ -45,6 +46,12 @@ public abstract class PropertyAccessorBase /// public Type DeclaringType { get; } + /// + /// True if this property needs recursive scanning (not primitive/string). + /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. + /// + public bool IsComplexType { get; } + /// /// Compiled getter delegate for reading property values. /// @@ -62,6 +69,9 @@ public abstract class PropertyAccessorBase UnderlyingType = underlying ?? PropertyType; PropertyTypeCode = Type.GetTypeCode(UnderlyingType); + // Pre-compute: is this a complex type that needs recursive handling? + IsComplexType = !IsPrimitiveOrStringFast(PropertyType); + _getter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop); } diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs index 7197dc0..8079549 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs @@ -42,10 +42,10 @@ public static partial class AcToonSerializer } } - // Build property accessors using shared GetSerializableProperties from base + // Build property accessors using pre-computed ReadableProperties from base if (!IsCollection && !IsDictionary && !IsPrimitiveOrStringFast(type)) { - var props = GetSerializableProperties(type, requiresWrite: false) + var props = ReadableProperties .Select(p => new ToonPropertyAccessor(p, type)) .ToArray(); diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index dde80b9..10c7fd5 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -11,7 +11,7 @@ namespace AyCode.Core.Serializers; /// /// Base class for type metadata used by all serializers. -/// Contains common functionality for type analysis and constructor compilation. +/// Contains common functionality for type analysis, constructor compilation, and IId detection. /// public abstract class TypeMetadataBase { @@ -38,19 +38,144 @@ public abstract class TypeMetadataBase /// private static readonly ConcurrentDictionary<(Type, bool), List> UnfilteredPropertiesGlobalCache = new(); - private readonly ConcurrentDictionary<(Type, bool), PropertyInfo[]> _orderedPropertiesCache = new(); - private readonly Func _ignorePropertyFilter; + + /// + /// The type this metadata is for. Stored to avoid repeated reflection. + /// + protected Type SourceType { get; } + + /// + /// Readable properties (CanRead=true) for this type. Used by serializers. + /// Pre-computed in constructor, no dictionary lookup needed. + /// + protected PropertyInfo[] ReadableProperties { get; } + + /// + /// Writable properties (CanRead=true AND CanWrite=true) for this type. Used by deserializers. + /// Pre-computed in constructor, no dictionary lookup needed. + /// + protected PropertyInfo[] WritableProperties { get; } + /// /// Compiled parameterless constructor for the type. /// Null if the type is abstract or has no parameterless constructor. /// public Func? CompiledConstructor { get; } + /// + /// Whether this type implements IId interface. + /// Cached at metadata creation time to avoid runtime reflection. + /// Used by both serializers (for IId-based reference deduplication) and deserializers. + /// + public bool IsIId { get; } + + /// + /// The Id property type if IsIId is true, null otherwise. + /// + public Type? IdType { get; } + + /// + /// Compiled getter for the Id property (if IsIId is true). + /// Pre-compiled delegate avoids reflection overhead during serialization/deserialization. + /// Note: Use typed getters (GetIdInt32, GetIdInt64, GetIdGuid) for zero-boxing access. + /// + public Func? IdGetter { get; } + + /// + /// The accessor type for IId.Id property. + /// Used for fast typed getter dispatch without boxing. + /// + public AcSerializerCommon.IdAccessorType IdAccessorType { get; } + + /// + /// Typed getter delegate for IId.Id property. + /// Type depends on IdAccessorType (Func<object, int>, Func<object, long>, or Func<object, Guid>). + /// + private readonly Delegate? _typedIdGetter; + + #region Scan Optimization Flags + + /// + /// True if this type has any complex (non-primitive) properties that need recursive scanning. + /// If false, ScanReferences can skip child property scanning entirely. + /// + public bool HasComplexProperties { get; protected set; } + + /// + /// True if this type or any of its properties could potentially be shared (multi-referenced). + /// False for sealed value-like types with only primitives - these never need reference tracking. + /// + public bool NeedsReferenceTracking { get; protected set; } + + #endregion + + /// + /// Gets the Id as int without boxing. Only valid when IdAccessorType == Int32. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetIdInt32(object obj) => ((Func)_typedIdGetter!)(obj); + + /// + /// Gets the Id as long without boxing. Only valid when IdAccessorType == Int64. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetIdInt64(object obj) => ((Func)_typedIdGetter!)(obj); + + /// + /// Gets the Id as Guid without boxing. Only valid when IdAccessorType == Guid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Guid GetIdGuid(object obj) => ((Func)_typedIdGetter!)(obj); + protected TypeMetadataBase(Type type, Func ignorePropertyFilter) { + SourceType = type; _ignorePropertyFilter = ignorePropertyFilter; CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type); + + // Pre-compute property arrays - no dictionary lookup needed later! + // Uses static global cache for unfiltered properties, then applies filter once + var allReadable = GetUnfilteredProperties(type, requiresWrite: false) + .Where(p => !ignorePropertyFilter(p)) + .ToArray(); + ReadableProperties = allReadable; + WritableProperties = allReadable.Where(p => p.CanWrite).ToArray(); + + // Cache IId info at construction time - no runtime reflection needed later! + var idInfo = GetIdInfo(type); + IsIId = idInfo.IsId; + IdType = idInfo.IdType; + + if (IsIId && IdType != null) + { + var idProp = type.GetProperty("Id"); + if (idProp != null) + { + // Create typed getter for the three common Id types to avoid boxing + if (ReferenceEquals(IdType, IntType)) + { + IdAccessorType = AcSerializerCommon.IdAccessorType.Int32; + _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); + } + else if (ReferenceEquals(IdType, LongType)) + { + IdAccessorType = AcSerializerCommon.IdAccessorType.Int64; + _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); + } + else if (ReferenceEquals(IdType, GuidType)) + { + IdAccessorType = AcSerializerCommon.IdAccessorType.Guid; + _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); + } + else + { + // Fallback for exotic Id types - uses boxing + IdAccessorType = AcSerializerCommon.IdAccessorType.Object; + IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp); + } + } + } } /// @@ -61,17 +186,17 @@ public abstract class TypeMetadataBase /// - Base properties always get the same index in both base and derived types /// - Supports safe deserialization of derived types into base types /// - Enables cross-version compatibility when new derived properties are added - /// Results are cached per type and requiresWrite combination. + /// + /// PERFORMANCE: Returns pre-computed array, no dictionary lookup! /// - /// The type to analyze. + /// The type to analyze (must match SourceType). /// Whether the property must be writable (true for deserialization). - /// Array of properties with stable indices (cached). + /// Array of properties with stable indices. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public PropertyInfo[] GetSerializableProperties(Type type, bool requiresWrite = false) { - return _orderedPropertiesCache.GetOrAdd((type, requiresWrite), _ => - { - return GetUnfilteredProperties(type, requiresWrite).Where(propertyInfo => !_ignorePropertyFilter(propertyInfo)).ToArray(); - }); + // Direct array access - no dictionary lookup! + return requiresWrite ? WritableProperties : ReadableProperties; } private static List GetUnfilteredProperties(Type type, bool requiresWrite)