From 0552268ac19b1f74cccef55be57468a92755af58 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 29 Dec 2025 22:41:28 +0100 Subject: [PATCH] Refactor: Add high-performance Chain API for serializers Major overhaul of serialization/deserialization infrastructure: - Introduced unified Chain API for binary/JSON, enabling multi-deserialization/population with strong IId reference identity (critical for Blazor/DXGrid). - Added base classes for property accessors/setters and centralized type metadata. - Implemented ChainReferenceTracker and shared IIdCollectionMergeHelper for reference tracking and collection merging. - Refactored property access logic to use typed delegates for primitives/enums. - Updated extension methods and replaced legacy chain/populate interfaces. - Improved error handling and diagnostics. - Added comprehensive tests for chain API and reference preservation. - Minor fixes and performance optimizations throughout. --- .../AcBinarySerializerChainReferenceTests.cs | 233 ++++++++++ .../AcBinarySerializerChainTests.cs | 347 ++++++++++++++ .../Serialization/ChainReferenceDebugTest.cs | 61 +++ .../Extensions/SerializeObjectExtensions.cs | 85 +++- AyCode.Core/Serializers/AcSerializerCommon.cs | 115 +++++ ...serializer.BinaryDeserializationContext.cs | 12 + ...erializer.BinaryDeserializeTypeMetadata.cs | 217 +++++---- .../Binaries/AcBinaryDeserializer.cs | 429 +++++++++++++++++- ...rySerializer.BinarySerializationContext.cs | 1 - .../AcBinarySerializer.BinaryTypeMetadata.cs | 153 +------ .../Binaries/AcBinarySerializer.cs | 3 +- .../Binaries/BinaryPropertyAccessorBase.cs | 145 ++++++ .../Binaries/BinaryPropertySetterBase.cs | 181 ++++++++ .../Serializers/DeserializeChainBase.cs | 40 ++ .../Serializers/IIdCollectionMergeHelper.cs | 94 ++++ ...JsonDeserializer.DeserializationContext.cs | 13 + ...sonDeserializer.DeserializeTypeMetadata.cs | 173 ++----- .../Jsons/AcJsonDeserializer.JsonElement.cs | 70 +++ .../Jsons/AcJsonDeserializer.Utf8Reader.cs | 38 +- .../Serializers/Jsons/AcJsonDeserializer.cs | 170 ++----- .../AcJsonSerializer.SerializationContext.cs | 16 +- .../Jsons/AcJsonSerializer.TypeMetadata.cs | 35 +- .../Jsons/JsonPropertyAccessorBase.cs | 22 + .../Jsons/JsonPropertySetterBase.cs | 175 +++++++ .../Serializers/PropertyAccessorBase.cs | 73 +++ AyCode.Core/Serializers/PropertySetterBase.cs | 71 +++ AyCode.Core/Serializers/ReferenceTracker.cs | 138 ++++++ AyCode.Core/Serializers/TypeMetadataBase.cs | 41 ++ .../SignalRs/AcSignalRDataSource.cs | 7 +- TestChainRunner/TestChainRunner.csproj | 13 - 30 files changed, 2559 insertions(+), 612 deletions(-) create mode 100644 AyCode.Core.Tests/Serialization/AcBinarySerializerChainReferenceTests.cs create mode 100644 AyCode.Core.Tests/Serialization/AcBinarySerializerChainTests.cs create mode 100644 AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs create mode 100644 AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs create mode 100644 AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs create mode 100644 AyCode.Core/Serializers/DeserializeChainBase.cs create mode 100644 AyCode.Core/Serializers/IIdCollectionMergeHelper.cs create mode 100644 AyCode.Core/Serializers/Jsons/JsonPropertyAccessorBase.cs create mode 100644 AyCode.Core/Serializers/Jsons/JsonPropertySetterBase.cs create mode 100644 AyCode.Core/Serializers/PropertyAccessorBase.cs create mode 100644 AyCode.Core/Serializers/PropertySetterBase.cs create mode 100644 AyCode.Core/Serializers/ReferenceTracker.cs create mode 100644 AyCode.Core/Serializers/TypeMetadataBase.cs delete mode 100644 TestChainRunner/TestChainRunner.csproj diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerChainReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerChainReferenceTests.cs new file mode 100644 index 0000000..c86e5f3 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerChainReferenceTests.cs @@ -0,0 +1,233 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for Chain API reference preservation with IId objects. +/// This is the critical feature for DevExpress DXGrid GridCustomDataSource scenario. +/// +[TestClass] +public class AcBinarySerializerChainReferenceTests +{ + /// + /// CRITICAL TEST: DevExpress DXGrid scenario with Chain API. + /// Server returns List<Item> for grid display, but we also have internal cache List<Item>. + /// When using ThenPopulate, the grid's visible items MUST be the same object references + /// from the cache to ensure Blazor binding works correctly. + /// + [TestMethod] + public void ChainPopulate_IIdObjects_PreservesReferences() + { + // Setup: Create internal cache with 5 categories + var internalCache = new List + { + new() { Id = 1, Name = "Category1", SortOrder = 1 }, + new() { Id = 2, Name = "Category2", SortOrder = 2 }, + new() { Id = 3, Name = "Category3", SortOrder = 3 }, + new() { Id = 4, Name = "Category4", SortOrder = 4 }, + new() { Id = 5, Name = "Category5", SortOrder = 5 } + }; + + // Server returns subset of categories (like grid pagination - page 2: items 3-5) + var serverData = new List + { + new() { Id = 3, Name = "Category3_Updated", SortOrder = 33 }, + new() { Id = 4, Name = "Category4_Updated", SortOrder = 44 }, + new() { Id = 5, Name = "Category5_Updated", SortOrder = 55 } + }; + + // Serialize server response + var binary = serverData.ToBinary(); + + // Grid's visible list (empty initially) + var gridVisibleList = new List(); + + // CRITICAL: Use Chain API to parse once, populate both cache and grid + using var chain = binary.BinaryToChain>(); + + // First: Update internal cache (will become 3 items: 3-5 updated) + chain.ThenPopulate(internalCache); + + // Second: Populate grid's visible list + chain.ThenPopulate(gridVisibleList); + + // VERIFICATION: After ThenPopulate, internalCache contains the 3 items from server + Assert.AreEqual(3, gridVisibleList.Count); + Assert.AreEqual(3, internalCache.Count, "ThenPopulate replaces list contents with server data"); + + // CRITICAL ASSERTION: Grid items MUST be same object references as cache items! + Assert.AreSame(internalCache[0], gridVisibleList[0], + "Grid item MUST be same reference as cache item for Blazor binding!"); + Assert.AreSame(internalCache[1], gridVisibleList[1], + "Grid item MUST be same reference as cache item for Blazor binding!"); + Assert.AreSame(internalCache[2], gridVisibleList[2], + "Grid item MUST be same reference as cache item for Blazor binding!"); + + // Verify data was updated correctly + Assert.AreEqual(3, internalCache[0].Id); + Assert.AreEqual("Category3_Updated", internalCache[0].Name); + Assert.AreEqual(33, internalCache[0].SortOrder); + } + + /// + /// Test JSON Chain API reference preservation. + /// + [TestMethod] + public void JsonChainPopulate_IIdObjects_PreservesReferences() + { + // Setup: Create internal cache + var internalCache = new List + { + new() { Id = 1, Name = "Category1", SortOrder = 1 }, + new() { Id = 2, Name = "Category2", SortOrder = 2 }, + new() { Id = 3, Name = "Category3", SortOrder = 3 } + }; + + // Server returns subset + var serverData = new List + { + new() { Id = 2, Name = "Category2_Updated", SortOrder = 22 }, + new() { Id = 3, Name = "Category3_Updated", SortOrder = 33 } + }; + + // Serialize server response + var json = serverData.ToJson(); + + // Grid's visible list + var gridVisibleList = new List(); + + // Use JSON Chain API + using var chain = json.JsonToChain>(); + + // Update internal cache (will replace with 2 items) + chain.ThenPopulate(internalCache); + + // Populate grid's visible list + chain.ThenPopulate(gridVisibleList); + + // VERIFICATION + Assert.AreEqual(2, gridVisibleList.Count); + Assert.AreEqual(2, internalCache.Count, "ThenPopulate replaces list contents"); + + // CRITICAL: Same references! + Assert.AreSame(internalCache[0], gridVisibleList[0]); + Assert.AreSame(internalCache[1], gridVisibleList[1]); + + // Verify updates + Assert.AreEqual(2, internalCache[0].Id); + Assert.AreEqual("Category2_Updated", internalCache[0].Name); + Assert.AreEqual(22, internalCache[0].SortOrder); + } + + /// + /// Test with Guid-based IId implementation. + /// + [TestMethod] + public void ChainPopulate_GuidIId_PreservesReferences() + { + var cache = new List + { + new() { Id = Guid.NewGuid(), Code = "ORD-001", Count = 10 }, + new() { Id = Guid.NewGuid(), Code = "ORD-002", Count = 20 } + }; + + var id1 = cache[0].Id; + var id2 = cache[1].Id; + + var serverData = new List + { + new() { Id = id1, Code = "ORD-001-UPDATED", Count = 11 }, + new() { Id = id2, Code = "ORD-002-UPDATED", Count = 22 } + }; + + var binary = serverData.ToBinary(); + var gridList = new List(); + + using var chain = binary.BinaryToChain>(); + + chain.ThenPopulate(cache); + chain.ThenPopulate(gridList); + + Assert.AreEqual(2, gridList.Count); + Assert.AreSame(cache[0], gridList[0], "Guid-based IId should also preserve references"); + Assert.AreSame(cache[1], gridList[1]); + Assert.AreEqual("ORD-001-UPDATED", cache[0].Code); + } + + /// + /// Test multiple chain operations with different subsets. + /// + [TestMethod] + public void ChainPopulate_MultipleSubsets_PreservesReferencesAcrossAll() + { + // Large internal cache + var internalCache = Enumerable.Range(1, 10) + .Select(i => new SharedCategory { Id = i, Name = $"Category{i}", SortOrder = i * 10 }) + .ToList(); + + // Server returns items 3-7 + var serverData = Enumerable.Range(3, 5) + .Select(i => new SharedCategory { Id = i, Name = $"Category{i}_Updated", SortOrder = i * 11 }) + .ToList(); + + var binary = serverData.ToBinary(); + + // Three different grid pages/views + var gridPage1 = new List(); + var gridPage2 = new List(); + var gridPage3 = new List(); + + using var chain = binary.BinaryToChain>(); + + // Update cache first + chain.ThenPopulate(internalCache); + + // Populate different grid pages + chain.ThenPopulate(gridPage1); + chain.ThenPopulate(gridPage2); + chain.ThenPopulate(gridPage3); + + // All pages should have same references + Assert.AreEqual(5, gridPage1.Count); + Assert.AreEqual(5, gridPage2.Count); + Assert.AreEqual(5, gridPage3.Count); + + // All three pages point to the SAME objects + for (int i = 0; i < 5; i++) + { + Assert.AreSame(gridPage1[i], gridPage2[i], $"Page1 and Page2 item {i} must be same reference"); + Assert.AreSame(gridPage2[i], gridPage3[i], $"Page2 and Page3 item {i} must be same reference"); + Assert.AreSame(internalCache[i], gridPage1[i], $"Cache and Page1 item {i} must be same reference"); + } + } + + /// + /// Simple debug test to verify chain reference tracking works. + /// + [TestMethod] + public void ChainPopulate_SimpleCase_Works() + { + var list1 = new List(); + var list2 = new List(); + + var serverData = new List + { + new() { Id = 1, Name = "Cat1", SortOrder = 10 } + }; + + var binary = serverData.ToBinary(); + + using var chain = binary.BinaryToChain>(); + + // First populate + chain.ThenPopulate(list1); + Assert.AreEqual(1, list1.Count); + Assert.AreEqual(1, list1[0].Id); + + // Second populate - should reuse same object + chain.ThenPopulate(list2); + Assert.AreEqual(1, list2.Count); + Assert.AreSame(list1[0], list2[0], "Should be same object reference!"); + } +} diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerChainTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerChainTests.cs new file mode 100644 index 0000000..0547744 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerChainTests.cs @@ -0,0 +1,347 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Tests.TestModels; +using static AyCode.Core.Tests.TestModels.AcSerializerModels; + +namespace AyCode.Core.Tests.Serialization; + +/// +/// Tests for Binary Chain API (CreateDeserializeChain and CreatePopulateChain). +/// +[TestClass] +public class AcBinarySerializerChainTests +{ + [TestMethod] + public void DeserializeChain_SingleDeserialization_WorksCorrectly() + { + // Arrange + var original = new TestSimpleClass { Id = 42, Name = "John", Value = 3.14, IsActive = true }; + var binary = original.ToBinary(); + + // Act + using var chain = binary.BinaryToChain(); + var result = chain.Value; + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(42, result.Id); + Assert.AreEqual("John", result.Name); + Assert.AreEqual(3.14, result.Value); + Assert.AreEqual(true, result.IsActive); + } + + [TestMethod] + public void DeserializeChain_MultipleDeserializations_ParsesOnlyOnce() + { + // Arrange + var original = new TestSimpleClass { Id = 100, Name = "Test", Value = 99.9, IsActive = false }; + var binary = original.ToBinary(); + + // Act + using var chain = binary.BinaryToChain(); + var result1 = chain.Value; + var result2 = chain.ThenDeserialize(); + var result3 = chain.ThenDeserialize(); + + // Assert - All three deserializations should work + Assert.IsNotNull(result1); + Assert.AreEqual(100, result1.Id); + + Assert.IsNotNull(result2); + Assert.AreEqual(100, result2.Id); + Assert.AreEqual("Test", result2.Name); + + Assert.IsNotNull(result3); + Assert.AreEqual(99.9, result3.Value); + Assert.AreEqual(false, result3.IsActive); + } + + [TestMethod] + public void DeserializeChain_NestedObjects_WorksCorrectly() + { + // Arrange + var original = new TestNestedClass + { + Id = 1, + Name = "Parent", + Child = new TestSimpleClass { Id = 2, Name = "Child", Value = 10.5 } + }; + var binary = original.ToBinary(); + + // Act + using var chain = binary.BinaryToChain(); + var result1 = chain.Value; + var result2 = chain.ThenDeserialize(); + + // Assert + Assert.IsNotNull(result1); + Assert.AreEqual("Parent", result1.Name); + Assert.IsNotNull(result1.Child); + Assert.AreEqual("Child", result1.Child.Name); + + Assert.IsNotNull(result2); + Assert.AreEqual(1, result2.Id); + Assert.IsNotNull(result2.Child); + Assert.AreEqual(10.5, result2.Child.Value); + } + + [TestMethod] + public void DeserializeChain_WithList_WorksCorrectly() + { + // Arrange + var original = new TestClassWithList + { + Id = 5, + Items = new List { "Apple", "Banana", "Cherry" } + }; + var binary = original.ToBinary(); + + // Act + using var chain = binary.BinaryToChain(); + var result = chain.Value; + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(5, result.Id); + Assert.AreEqual(3, result.Items.Count); + Assert.AreEqual("Apple", result.Items[0]); + Assert.AreEqual("Banana", result.Items[1]); + Assert.AreEqual("Cherry", result.Items[2]); + } + + [TestMethod] + public void PopulateChain_SinglePopulate_UpdatesObject() + { + // Arrange + var original = new TestSimpleClass { Id = 99, Name = "Updated", Value = 123.45, IsActive = true }; + var binary = original.ToBinary(); + var target = new TestSimpleClass { Id = 1, Name = "Old", Value = 0, IsActive = false }; + + // Act + using var chain = binary.BinaryToChain(target); + + // Assert + Assert.AreEqual(99, target.Id); + Assert.AreEqual("Updated", target.Name); + Assert.AreEqual(123.45, target.Value); + Assert.AreEqual(true, target.IsActive); + } + + [TestMethod] + public void PopulateChain_MultiplePopulates_UpdatesAllObjects() + { + // Arrange + var original = new TestSimpleClass { Id = 100, Name = "Shared", Value = 50.0 }; + var binary = original.ToBinary(); + var target1 = new TestSimpleClass { Id = 1, Name = "Old1" }; + var target2 = new TestSimpleClass { Id = 2, Name = "Old2" }; + var target3 = new TestSimpleClass { Id = 3, Name = "Old3" }; + + // Act + using var chain = binary.BinaryToChain(target1); + chain.ThenPopulate(target2); + chain.ThenPopulate(target3); + + // Assert + Assert.AreEqual(100, target1.Id); + Assert.AreEqual("Shared", target1.Name); + Assert.AreEqual(50.0, target1.Value); + + Assert.AreEqual(100, target2.Id); + Assert.AreEqual("Shared", target2.Name); + Assert.AreEqual(50.0, target2.Value); + + Assert.AreEqual(100, target3.Id); + Assert.AreEqual("Shared", target3.Name); + Assert.AreEqual(50.0, target3.Value); + } + + [TestMethod] + public void PopulateChain_NestedObjects_MergesCorrectly() + { + // Arrange + var original = new TestNestedClass + { + Id = 10, + Name = "UpdatedParent", + Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 99.9 } + }; + var binary = original.ToBinary(); + var target = new TestNestedClass + { + Id = 1, + Name = "OldParent", + Child = new TestSimpleClass { Id = 2, Name = "OldChild", Value = 1.0 } + }; + + // Act + using var chain = binary.BinaryToChain(target); + + // Assert + Assert.AreEqual(10, target.Id); + Assert.AreEqual("UpdatedParent", target.Name); + Assert.IsNotNull(target.Child); + Assert.AreEqual(20, target.Child.Id); + Assert.AreEqual("UpdatedChild", target.Child.Name); + Assert.AreEqual(99.9, target.Child.Value); + } + + [TestMethod] + public void PopulateChain_WithList_UpdatesCollection() + { + // Arrange + var original = new TestClassWithList + { + Id = 7, + Items = new List { "New1", "New2", "New3" } + }; + var binary = original.ToBinary(); + var target = new TestClassWithList + { + Id = 1, + Items = new List { "Old1" } + }; + + // Act + using var chain = binary.BinaryToChain(target); + + // Assert + Assert.AreEqual(7, target.Id); + Assert.AreEqual(3, target.Items.Count); + Assert.AreEqual("New1", target.Items[0]); + Assert.AreEqual("New2", target.Items[1]); + Assert.AreEqual("New3", target.Items[2]); + } + + [TestMethod] + public void DeserializeChain_EmptyBinary_ReturnsEmpty() + { + // Arrange + var binary = Array.Empty(); + + // Act + using var chain = binary.BinaryToChain(); + + // Assert + Assert.IsNull(chain.Value); + } + + [TestMethod] + public void PopulateChain_EmptyBinary_DoesNothing() + { + // Arrange + var binary = Array.Empty(); + var target = new TestSimpleClass { Id = 42, Name = "Original" }; + + // Act + using var chain = binary.BinaryToChain(target); + + // Assert - Should remain unchanged + Assert.AreEqual(42, target.Id); + Assert.AreEqual("Original", target.Name); + } + + [TestMethod] + public void DeserializeChain_Dispose_CannotReuseAfterDispose() + { + // Arrange + var original = new TestSimpleClass { Id = 1, Name = "Test" }; + var binary = original.ToBinary(); + var chain = binary.BinaryToChain(); + var value = chain.Value; + + // Act + chain.Dispose(); + + // Assert + Assert.IsNotNull(value); // Value from before dispose should still exist + _ = Assert.ThrowsExactly(() => chain.ThenDeserialize()); + } + + [TestMethod] + public void PopulateChain_Dispose_CannotReuseAfterDispose() + { + // Arrange + var original = new TestSimpleClass { Id = 1, Name = "Test" }; + var binary = original.ToBinary(); + var target1 = new TestSimpleClass(); + var chain = binary.BinaryToChain(target1); + + // Act + chain.Dispose(); + + // Assert + var target2 = new TestSimpleClass(); + _ = Assert.ThrowsExactly(() => chain.ThenPopulate(target2)); + } + + [TestMethod] + public void DeserializeChain_WithOptions_UsesCorrectOptions() + { + // Arrange + var original = new TestSimpleClass { Id = 1, Name = "Test", Value = 10.5 }; + var binary = original.ToBinary(); + var options = new AcBinarySerializerOptions(); + + // Act + using var chain = binary.BinaryToChain(options); + var result = chain.Value; + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Id); + Assert.AreEqual("Test", result.Name); + } + + [TestMethod] + public void PopulateChain_WithOptions_UsesCorrectOptions() + { + // Arrange + var original = new TestSimpleClass { Id = 99, Name = "Updated" }; + var binary = original.ToBinary(); + var target = new TestSimpleClass { Id = 1, Name = "Old" }; + var options = new AcBinarySerializerOptions(); + + // Act + using var chain = binary.BinaryToChain(target, options); + + // Assert + Assert.AreEqual(99, target.Id); + Assert.AreEqual("Updated", target.Name); + } + + [TestMethod] + public void DeserializeChain_ReadOnlyMemory_WorksCorrectly() + { + // Arrange + var original = new TestSimpleClass { Id = 42, Name = "Memory Test" }; + var binary = original.ToBinary(); + ReadOnlyMemory memory = binary; + + // Act + using var chain = memory.BinaryToChain(); + var result = chain.Value; + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(42, result.Id); + Assert.AreEqual("Memory Test", result.Name); + } + + [TestMethod] + public void PopulateChain_ReadOnlyMemory_WorksCorrectly() + { + // Arrange + var original = new TestSimpleClass { Id = 99, Name = "Memory Update" }; + var binary = original.ToBinary(); + ReadOnlyMemory memory = binary; + var target = new TestSimpleClass { Id = 1, Name = "Old" }; + + // Act + using var chain = memory.BinaryToChain(target); + + // Assert + Assert.AreEqual(99, target.Id); + Assert.AreEqual("Memory Update", target.Name); + } +} diff --git a/AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs b/AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs new file mode 100644 index 0000000..40d0084 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/ChainReferenceDebugTest.cs @@ -0,0 +1,61 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Serializers; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.Serialization; + +[TestClass] +public class ChainReferenceDebugTest +{ + [TestMethod] + public void DebugChainReferences_DirectTest() + { + // Test ChainReferenceTracker directly + var tracker = new AcSerializerCommon.ChainReferenceTracker(); + + var category = new SharedCategory { Id = 100, Name = "TestCategory" }; + + // Register using reflection (like ThenPopulate does) + tracker.TryRegisterIIdObject(category); + + // Try to retrieve using boxed int (like MergeIIdCollection does) + object id = 100; // Boxed int + var found = tracker.TryGetObject(id, out var retrievedCategory); + + Console.WriteLine($"Found: {found}"); + Console.WriteLine($"Same reference: {ReferenceEquals(category, retrievedCategory)}"); + + Assert.IsTrue(found, "Should find the category by ID"); + Assert.AreSame(category, retrievedCategory, "Should be same object reference"); + } + + [TestMethod] + public void DebugSimpleChainPopulate() + { + var list1 = new List(); + var list2 = new List(); + + var serverData = new List + { + new() { Id = 1, Name = "Cat1", SortOrder = 10 } + }; + + var binary = serverData.ToBinary(); + + using var chain = binary.BinaryToChain>(); + + // 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()}"); + + Assert.AreSame(list1[0], list2[0], "Should be same object reference!"); + } +} diff --git a/AyCode.Core/Extensions/SerializeObjectExtensions.cs b/AyCode.Core/Extensions/SerializeObjectExtensions.cs index 72008ef..962875d 100644 --- a/AyCode.Core/Extensions/SerializeObjectExtensions.cs +++ b/AyCode.Core/Extensions/SerializeObjectExtensions.cs @@ -5,12 +5,13 @@ using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; using AyCode.Core.Interfaces; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using static AyCode.Core.Helpers.JsonUtilities; -using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer; +using static AyCode.Core.Serializers.Binaries.AcBinaryDeserializer; namespace AyCode.Core.Extensions; @@ -74,7 +75,7 @@ public class HybridReferenceResolver : IReferenceResolver [MethodImpl(MethodImplOptions.AggressiveInlining)] private Dictionary GetObjectToId() => - _objectToId ??= new Dictionary(_estimatedObjectCount, ReferenceEqualityComparer.Instance); + _objectToId ??= new Dictionary(_estimatedObjectCount, AyCode.Core.Serializers.ReferenceEqualityComparer.Instance); public void AddReference(object context, string reference, object value) { @@ -472,19 +473,23 @@ public static class SerializeObjectExtensions /// Efficient for populating multiple objects from the same JSON source. /// Use with 'using' statement or call Dispose() when done. /// - public static IPopulateChain JsonToChain(this string json, object target) + public static IDeserializeChain JsonToChain(this string json, object target) { json = UnwrapJsonString(json); - return AcJsonDeserializer.CreatePopulateChain(json, target); + var chain = AcJsonDeserializer.CreateDeserializeChain(json); + chain.ThenPopulate(target); + return chain; } /// /// Create a populate chain with options. /// - public static IPopulateChain JsonToChain(this string json, object target, AcJsonSerializerOptions options) + public static IDeserializeChain JsonToChain(this string json, object target, AcJsonSerializerOptions options) { json = UnwrapJsonString(json); - return AcJsonDeserializer.CreatePopulateChain(json, target, options); + var chain = AcJsonDeserializer.CreateDeserializeChain(json, options); + chain.ThenPopulate(target); + return chain; } #endregion @@ -650,6 +655,74 @@ public static class SerializeObjectExtensions public static void BinaryToMerge(this ReadOnlyMemory data, T target) where T : class => AcBinaryDeserializer.PopulateMerge(data.Span, target); + /// + /// Create a deserialize chain that parses binary data once and allows multiple deserializations. + /// Efficient for deserializing the same binary to multiple different types. + /// Use with 'using' statement or call Dispose() when done. + /// + public static IDeserializeChain BinaryToChain(this byte[] data) + => AcBinaryDeserializer.CreateDeserializeChain(data.AsSpan()); + + /// + /// Create a deserialize chain with options. + /// + public static IDeserializeChain BinaryToChain(this byte[] data, AcBinarySerializerOptions options) + => AcBinaryDeserializer.CreateDeserializeChain(data.AsSpan(), options); + + /// + /// Create a deserialize chain from ReadOnlyMemory. + /// + public static IDeserializeChain BinaryToChain(this ReadOnlyMemory data) + => AcBinaryDeserializer.CreateDeserializeChain(data.Span); + + /// + /// Create a deserialize chain from ReadOnlyMemory with options. + /// + public static IDeserializeChain BinaryToChain(this ReadOnlyMemory data, AcBinarySerializerOptions options) + => AcBinaryDeserializer.CreateDeserializeChain(data.Span, options); + + /// + /// Create a populate chain that parses binary data once and allows populating multiple objects. + /// Efficient for populating multiple objects from the same binary source. + /// Use with 'using' statement or call Dispose() when done. + /// + public static IDeserializeChain BinaryToChain(this byte[] data, T target) where T : class + { + var chain = AcBinaryDeserializer.CreateDeserializeChain(data.AsSpan()); + chain.ThenPopulate(target); + return chain; + } + + /// + /// Create a populate chain with options. + /// + public static IDeserializeChain BinaryToChain(this byte[] data, T target, AcBinarySerializerOptions options) where T : class + { + var chain = AcBinaryDeserializer.CreateDeserializeChain(data.AsSpan(), options); + chain.ThenPopulate(target); + return chain; + } + + /// + /// Create a populate chain from ReadOnlyMemory. + /// + public static IDeserializeChain BinaryToChain(this ReadOnlyMemory data, T target) where T : class + { + var chain = AcBinaryDeserializer.CreateDeserializeChain(data.Span); + chain.ThenPopulate(target); + return chain; + } + + /// + /// Create a populate chain from ReadOnlyMemory with options. + /// + public static IDeserializeChain BinaryToChain(this ReadOnlyMemory data, T target, AcBinarySerializerOptions options) where T : class + { + var chain = AcBinaryDeserializer.CreateDeserializeChain(data.Span, options); + chain.ThenPopulate(target); + return chain; + } + #endregion #region Clone and Copy (Binary-based, zero intermediate allocation) diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index ede31b6..88d4cd1 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -475,6 +475,121 @@ public static class AcSerializerCommon var convertToInt = LExpression.Convert(propAccess, typeof(int)); return LExpression.Lambda>(convertToInt, objParam).Compile(); } + + /// + /// Creates a typed setter delegate to avoid boxing for value types. + /// + public static Action CreateTypedSetter(Type declaringType, PropertyInfo prop) + { + var objParam = LExpression.Parameter(typeof(object), "obj"); + var valueParam = LExpression.Parameter(typeof(TProperty), "value"); + var castExpr = LExpression.Convert(objParam, declaringType); + var propAccess = LExpression.Property(castExpr, prop); + var assign = LExpression.Assign(propAccess, valueParam); + return LExpression.Lambda>(assign, objParam, valueParam).Compile(); + } + + /// + /// Creates an enum setter that accepts int to avoid boxing. + /// + public static Action CreateEnumSetter(Type declaringType, PropertyInfo prop) + { + var objParam = LExpression.Parameter(typeof(object), "obj"); + var valueParam = LExpression.Parameter(typeof(int), "value"); + var castExpr = LExpression.Convert(objParam, declaringType); + var propAccess = LExpression.Property(castExpr, prop); + var convertToEnum = LExpression.Convert(valueParam, prop.PropertyType); + var assign = LExpression.Assign(propAccess, convertToEnum); + return LExpression.Lambda>(assign, objParam, valueParam).Compile(); + } + + #endregion + + #region Chain Reference Tracking + + /// + /// Tracks IId objects across chain deserializations to maintain reference identity. + /// Used internally by IBinaryDeserializeChain.ThenPopulate to ensure same object references. + /// + public sealed class ChainReferenceTracker + { + private readonly Dictionary<(Type, object), object> _idToObject = new(); + + /// + /// Registers an IId object for later retrieval. + /// + public bool TryRegisterIIdObject(object obj) + { + if (obj == null) return false; + + var type = obj.GetType(); + var idProp = type.GetProperty("Id"); + if (idProp == null) return false; + + var id = idProp.GetValue(obj); + if (id == null) return false; + + // Create a normalized key + var key = (type, NormalizeId(id)); + _idToObject[key] = obj; + return true; + } + + /// + /// Tries to get a previously registered object by type and ID. + /// + public bool TryGetObject(object id, out object? obj) + { + obj = null; + if (id == null) return false; + + var normalizedId = NormalizeId(id); + + // Search by normalized ID (ignoring type for simplicity in lookup) + foreach (var kvp in _idToObject) + { + if (Equals(kvp.Key.Item2, normalizedId)) + { + obj = kvp.Value; + return true; + } + } + + return false; + } + + /// + /// Tries to get a previously registered object by exact type and ID. + /// + public bool TryGetObject(Type type, object id, out object? obj) + { + var key = (type, NormalizeId(id)); + return _idToObject.TryGetValue(key, out obj); + } + + /// + /// Clears all tracked references. + /// + public void Clear() => _idToObject.Clear(); + + /// + /// Normalizes the ID value for consistent dictionary lookups. + /// Handles boxed value type comparisons. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object NormalizeId(object id) + { + // Convert common ID types to a consistent representation + return id switch + { + int i => i, + long l => l, + Guid g => g, + string s => s, + _ => id + }; + } + } #endregion } \ No newline at end of file diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 4fe7355..a2313e8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -28,6 +28,17 @@ public static partial class AcBinaryDeserializer public bool IsAtEnd => _position >= _buffer.Length; public int Position => _position; public byte MinStringInternLength => _minStringInternLength; + + /// + /// Chain reference tracker for maintaining object identity across chain operations. + /// Only set when in chain mode (CreateDeserializeChain). + /// + public AcSerializerCommon.ChainReferenceTracker? ChainTracker { readonly get; set; } + + /// + /// Returns true if in chain mode (ChainTracker is set). + /// + public readonly bool IsChainMode => ChainTracker != null; public BinaryDeserializationContext(ReadOnlySpan data) : this(data, AcBinarySerializerOptions.Default) @@ -46,6 +57,7 @@ public static partial class AcBinaryDeserializer HasReferenceHandling = false; IsMergeMode = false; RemoveOrphanedItems = false; + ChainTracker = null; _minStringInternLength = options.MinStringInternLength; _useStringCaching = options.UseStringCaching; _maxCachedStringLength = options.MaxCachedStringLength; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index c9a3cbd..a2b30fd 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -1,38 +1,50 @@ -using System; using System.Collections; using System.Collections.Frozen; -using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinaryDeserializer { - internal sealed class BinaryDeserializeTypeMetadata + internal sealed class BinaryDeserializeTypeMetadata : TypeMetadataBase { private readonly FrozenDictionary _properties; public BinaryPropertySetterInfo[] PropertiesArray { get; } - public Func? CompiledConstructor { get; } + + /// + /// Whether this type implements IId interface. + /// + public bool IsIId { get; } + + /// + /// Compiled getter for the Id property (if IsIId is true). + /// + public Func? IdGetter { get; } - public BinaryDeserializeTypeMetadata(Type type) + public BinaryDeserializeTypeMetadata(Type type) : base(type) { - PropertiesArray = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(static p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0 && - p.GetMethod is { IsPublic: true } && - p.SetMethod is { IsPublic: true } && - !HasJsonIgnoreAttribute(p)) - .Select(static p => new BinaryPropertySetterInfo(p)) + PropertiesArray = GetSerializableProperties(type, requiresRead: true, requiresWrite: true) + .Where(static p => p.GetMethod is { IsPublic: true } && p.SetMethod is { IsPublic: true }) + .Select(static p => new BinaryPropertySetterInfo(p, p.DeclaringType!)) .ToArray(); _properties = PropertiesArray.Length == 0 ? FrozenDictionary.Empty : PropertiesArray.ToFrozenDictionary(static p => p.Name, static p => p, StringComparer.Ordinal); - - CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type); + + // Check if type implements IId + var (isIId, _) = GetIdInfo(type); + IsIId = isIId; + if (isIId) + { + var idProp = type.GetProperty("Id"); + if (idProp != null) + IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -40,130 +52,117 @@ public static partial class AcBinaryDeserializer => _properties.TryGetValue(name, out propertyInfo); } - internal sealed class BinaryPropertySetterInfo + /// + /// Binary deserialization property setter with typed setters for performance. + /// + internal sealed class BinaryPropertySetterInfo : BinaryPropertySetterBase { private static readonly Func NullGetter = static _ => null; private static readonly Action NullSetter = static (_, _) => { }; - private readonly Func _getter; - private readonly Action _setter; + // Fields for manual constructor case + private readonly Func? _manualGetter; + private readonly Action? _manualSetter; + private readonly string? _manualName; + private readonly Type? _manualPropertyType; + private readonly bool _isManualConstruction; + private readonly Type? _manualElementType; + private readonly Type? _manualElementIdType; + private readonly Func? _manualElementIdGetter; + private readonly bool _manualIsIIdCollection; - public string Name { get; } - public Type PropertyType { get; } - public bool IsComplexType { get; } - public bool IsCollection { get; } - public Type? ElementType { get; } - public bool IsIIdCollection { get; } - public Type? ElementIdType { get; } - public Func? ElementIdGetter { get; } - - public BinaryPropertySetterInfo(PropertyInfo property) + public BinaryPropertySetterInfo(PropertyInfo property, Type declaringType) + : base(property, declaringType) { - Name = property.Name; - PropertyType = property.PropertyType; - IsCollection = IsCollectionTypeCheck(PropertyType); - ElementType = IsCollection ? GetCollectionElementType(PropertyType) : null; - - if (ElementType != null) - { - var elementIdInfo = GetIdInfo(ElementType); - IsIIdCollection = elementIdInfo.IsId; - ElementIdType = elementIdInfo.IdType; - - if (IsIIdCollection) - { - var idProp = ElementType.GetProperty("Id", BindingFlags.Instance | BindingFlags.Public); - if (idProp != null) - { - ElementIdGetter = AcSerializerCommon.CreateCompiledGetter(ElementType, idProp); - } - } - } - - IsComplexType = IsComplex(PropertyType); - _getter = AcSerializerCommon.CreateCompiledGetter(property.DeclaringType!, property); - _setter = AcSerializerCommon.CreateCompiledSetter(property.DeclaringType!, property); + _isManualConstruction = false; } + /// + /// Constructor for manually created property info (used in merge operations). + /// public BinaryPropertySetterInfo( string name, Type propertyType, bool isCollection, Type? elementType, Type? elementIdType, - Func? elementIdGetter, - Func? getter = null, - Action? setter = null) + Func? elementIdGetter) + : base(CreateDummyProperty(), typeof(object)) { - Name = name; - PropertyType = propertyType; - IsCollection = isCollection; - ElementType = elementType; - ElementIdType = elementIdType; - ElementIdGetter = elementIdGetter; - IsIIdCollection = elementIdGetter != null && elementIdType != null; - IsComplexType = elementType != null ? IsComplex(elementType) : IsComplex(propertyType); - - _getter = getter ?? NullGetter; - _setter = setter ?? NullSetter; + _isManualConstruction = true; + _manualName = name; + _manualPropertyType = propertyType; + _manualElementType = elementType; + _manualElementIdType = elementIdType; + _manualElementIdGetter = elementIdGetter; + _manualIsIIdCollection = elementIdGetter != null && elementIdType != null; + _manualGetter = NullGetter; + _manualSetter = NullSetter; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetValue(object target) => _getter(target); + // Dummy property for manual construction - we override all relevant properties + private static PropertyInfo CreateDummyProperty() + { + return typeof(DummyClass).GetProperty(nameof(DummyClass.DummyProp))!; + } + + private sealed class DummyClass + { + public object? DummyProp { get; set; } + } + + // Override properties for manual constructor + public new string Name => _isManualConstruction ? _manualName! : base.Name; + public new Type PropertyType => _isManualConstruction ? _manualPropertyType! : base.PropertyType; + public new Type? ElementType => _isManualConstruction ? _manualElementType : base.ElementType; + public new Type? ElementIdType => _isManualConstruction ? _manualElementIdType : base.ElementIdType; + public new Func? ElementIdGetter => _isManualConstruction ? _manualElementIdGetter : base.ElementIdGetter; + public new bool IsIIdCollection => _isManualConstruction ? _manualIsIIdCollection : base.IsIIdCollection; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetValue(object target, object? value) + public new object? GetValue(object target) => _isManualConstruction ? _manualGetter!(target) : base.GetValue(target); + + public override void SetValue(object target, object? value) { + if (_isManualConstruction) + { + _manualSetter!(target, value); + return; + } + try { - _setter(target, value); + base.SetValue(target, value); } catch (InvalidCastException ex) { - var valueType = value?.GetType().FullName ?? "null"; - var propType = PropertyType.FullName; - var targetTypeName = target.GetType().FullName; - var underlyingType = Nullable.GetUnderlyingType(PropertyType); - - // Get actual value info for debugging - var valueInfo = value switch - { - null => "null", - int i => $"int:{i}", - long l => $"long:{l}", - DateTime dt => $"DateTime:{dt:O}", - string s => $"string:'{s}'", - _ => $"{value.GetType().Name}:{value}" - }; - - throw new InvalidCastException( - $"Cannot set property '{Name}' on type '{targetTypeName}' - " + - $"PropertyType: '{propType}', ValueType: '{valueType}', Value: {valueInfo}, " + - $"IsNullable: {underlyingType != null}, UnderlyingType: {underlyingType?.FullName ?? "N/A"}, " + - $"IsValueType: {PropertyType.IsValueType}, IsCollection: {IsCollection}, IsComplexType: {IsComplexType}", - ex); + ThrowDetailedCastException(target, value, ex); } } - private static bool IsCollectionTypeCheck(Type type) + private void ThrowDetailedCastException(object target, object? value, InvalidCastException ex) { - if (ReferenceEquals(type, StringType)) return false; - if (type.IsArray) return true; - return typeof(IEnumerable).IsAssignableFrom(type); - } - - private static bool IsComplex(Type type) - { - var actualType = Nullable.GetUnderlyingType(type) ?? type; - if (actualType.IsPrimitive) return false; - if (ReferenceEquals(actualType, StringType)) return false; - if (actualType.IsEnum) return false; - if (ReferenceEquals(actualType, GuidType)) return false; - if (ReferenceEquals(actualType, DateTimeType)) return false; - if (ReferenceEquals(actualType, DecimalType)) return false; - if (ReferenceEquals(actualType, TimeSpanType)) return false; - if (ReferenceEquals(actualType, DateTimeOffsetType)) return false; - return true; + var valueType = value?.GetType().FullName ?? "null"; + var propType = PropertyType.FullName; + var targetTypeName = target.GetType().FullName; + var underlyingType = Nullable.GetUnderlyingType(PropertyType); + + var valueInfo = value switch + { + null => "null", + int i => $"int:{i}", + long l => $"long:{l}", + DateTime dt => $"DateTime:{dt:O}", + string s => $"string:'{s}'", + _ => $"{value.GetType().Name}:{value}" + }; + + throw new InvalidCastException( + $"Cannot set property '{Name}' on type '{targetTypeName}' - " + + $"PropertyType: '{propType}', ValueType: '{valueType}', Value: {valueInfo}, " + + $"IsNullable: {underlyingType != null}, UnderlyingType: {underlyingType?.FullName ?? "N/A"}, " + + $"IsValueType: {PropertyType.IsValueType}, IsCollection: {IsCollection}, IsComplexType: {IsComplexType}", + ex); } } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 5d769e0..80db678 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -355,8 +355,360 @@ public static partial class AcBinaryDeserializer #endregion + #region Chain API + + /// + /// Create a deserialize chain that parses binary data once and allows multiple deserializations. + /// Maintains reference identity for IId objects across chain operations. + /// + public static IDeserializeChain CreateDeserializeChain(ReadOnlySpan data) + => CreateDeserializeChain(data, AcBinarySerializerOptions.Default); + + /// + /// Create a deserialize chain with options. + /// + public static IDeserializeChain CreateDeserializeChain(ReadOnlySpan data, AcBinarySerializerOptions options) + { + if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null)) + return EmptyDeserializeChain.Instance; + + var targetType = typeof(T); + + // Copy data to array for chain storage + var dataArray = data.ToArray(); + var chainTracker = new AcSerializerCommon.ChainReferenceTracker(); + var context = new BinaryDeserializationContext(dataArray, options) { ChainTracker = chainTracker }; + + try + { + context.ReadHeader(); + var result = ReadValue(ref context, targetType, 0); + return new BinaryDeserializeChain(dataArray, options, chainTracker, (T?)result); + } + catch + { + throw; + } + } + + #endregion + + #region Chain Implementations + + /// + /// Binary implementation of deserialize chain. + /// Maintains reference identity for IId objects across chain operations. + /// + private sealed class BinaryDeserializeChain : IDeserializeChain + { + private readonly byte[] _data; + private readonly AcBinarySerializerOptions _options; + private readonly AcSerializerCommon.ChainReferenceTracker _chainTracker; + private bool _isDisposed; + + public T? Value { get; } + + public BinaryDeserializeChain(byte[] data, AcBinarySerializerOptions options, AcSerializerCommon.ChainReferenceTracker chainTracker, T? value) + { + _data = data; + _options = options; + _chainTracker = chainTracker; + Value = value; + } + + public TResult? ThenDeserialize() + { + ThrowIfDisposed(); + + var targetType = typeof(TResult); + var context = new BinaryDeserializationContext(_data, _options) { ChainTracker = _chainTracker }; + + try + { + context.ReadHeader(); + var result = ReadValue(ref context, targetType, 0); + return (TResult?)result; + } + catch (AcBinaryDeserializationException) { throw; } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to deserialize to type '{targetType.Name}' in chain: {ex.Message}", + 0, targetType, ex); + } + } + + public IDeserializeChain ThenPopulate(object target) + { + ArgumentNullException.ThrowIfNull(target); + ThrowIfDisposed(); + + var targetType = target.GetType(); + var context = new BinaryDeserializationContext(_data, _options) { ChainTracker = _chainTracker }; + + try + { + context.ReadHeader(); + var typeCode = context.PeekByte(); + + if (typeCode == BinaryTypeCode.Object) + { + context.ReadByte(); + PopulateObject(ref context, target, targetType, 0); + } + else if (typeCode == BinaryTypeCode.Array && target is IList targetList) + { + context.ReadByte(); + PopulateList(ref context, targetList, targetType, 0); + } + else + { + throw new AcBinaryDeserializationException( + $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}", + context.Position, targetType); + } + + return this; + } + catch (AcBinaryDeserializationException) { throw; } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to populate object of type '{targetType.Name}' in chain: {ex.Message}", + 0, targetType, ex); + } + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + } + + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _chainTracker.Clear(); + } + } + + #endregion + #region Value Reading + /// + /// Tries to read and set a primitive value directly using typed setters to avoid boxing. + /// Returns true if handled, false if should fall back to generic path. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryReadAndSetTypedValue(ref BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, byte peekCode) + { + // Only handle if we have a typed setter + if (propInfo.SetterType == PropertyAccessorType.Object) + return false; + + // Handle based on property setter type and incoming data type + switch (propInfo.SetterType) + { + case PropertyAccessorType.Int32: + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + if (peekCode == BinaryTypeCode.Int32) + { + context.ReadByte(); + propInfo.SetInt32(target, context.ReadVarInt()); + return true; + } + break; + + case PropertyAccessorType.Int64: + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + if (peekCode == BinaryTypeCode.Int32) + { + context.ReadByte(); + propInfo.SetInt64(target, context.ReadVarInt()); + return true; + } + if (peekCode == BinaryTypeCode.Int64) + { + context.ReadByte(); + propInfo.SetInt64(target, context.ReadVarLong()); + return true; + } + break; + + case PropertyAccessorType.Boolean: + if (peekCode == BinaryTypeCode.True) + { + context.ReadByte(); + propInfo.SetBoolean(target, true); + return true; + } + if (peekCode == BinaryTypeCode.False) + { + context.ReadByte(); + propInfo.SetBoolean(target, false); + return true; + } + break; + + case PropertyAccessorType.Double: + if (peekCode == BinaryTypeCode.Float64) + { + context.ReadByte(); + propInfo.SetDouble(target, context.ReadDoubleUnsafe()); + return true; + } + break; + + case PropertyAccessorType.Single: + if (peekCode == BinaryTypeCode.Float32) + { + context.ReadByte(); + propInfo.SetSingle(target, context.ReadSingleUnsafe()); + return true; + } + break; + + case PropertyAccessorType.Decimal: + if (peekCode == BinaryTypeCode.Decimal) + { + context.ReadByte(); + propInfo.SetDecimal(target, context.ReadDecimalUnsafe()); + return true; + } + break; + + case PropertyAccessorType.DateTime: + if (peekCode == BinaryTypeCode.DateTime) + { + context.ReadByte(); + propInfo.SetDateTime(target, context.ReadDateTimeUnsafe()); + return true; + } + break; + + case PropertyAccessorType.Guid: + if (peekCode == BinaryTypeCode.Guid) + { + context.ReadByte(); + propInfo.SetGuid(target, context.ReadGuidUnsafe()); + return true; + } + break; + + case PropertyAccessorType.Byte: + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + if (peekCode == BinaryTypeCode.UInt8) + { + context.ReadByte(); + propInfo.SetByte(target, context.ReadByte()); + return true; + } + break; + + case PropertyAccessorType.Int16: + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + if (peekCode == BinaryTypeCode.Int16) + { + context.ReadByte(); + propInfo.SetInt16(target, context.ReadInt16Unsafe()); + return true; + } + break; + + case PropertyAccessorType.UInt16: + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + if (peekCode == BinaryTypeCode.UInt16) + { + context.ReadByte(); + propInfo.SetUInt16(target, context.ReadUInt16Unsafe()); + return true; + } + break; + + case PropertyAccessorType.UInt32: + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + if (peekCode == BinaryTypeCode.UInt32) + { + context.ReadByte(); + propInfo.SetUInt32(target, context.ReadVarUInt()); + return true; + } + break; + + case PropertyAccessorType.UInt64: + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + if (peekCode == BinaryTypeCode.UInt64) + { + context.ReadByte(); + propInfo.SetUInt64(target, context.ReadVarULong()); + return true; + } + break; + + case PropertyAccessorType.Enum: + if (peekCode == BinaryTypeCode.Enum) + { + context.ReadByte(); + var enumByte = context.ReadByte(); + int enumValue; + if (BinaryTypeCode.IsTinyInt(enumByte)) + enumValue = BinaryTypeCode.DecodeTinyInt(enumByte); + else if (enumByte == BinaryTypeCode.Int32) + enumValue = context.ReadVarInt(); + else + return false; + propInfo.SetEnumAsInt32(target, enumValue); + return true; + } + // Enum can also be encoded as TinyInt directly + if (BinaryTypeCode.IsTinyInt(peekCode)) + { + context.ReadByte(); + propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + return true; + } + break; + } + + return false; + } + /// /// Optimized value reader using FrozenDictionary dispatch table. /// @@ -528,6 +880,8 @@ public static partial class AcBinaryDeserializer } var metadata = GetTypeMetadata(targetType); + + // Create instance var instance = CreateInstance(targetType, metadata); if (instance == null) return null; @@ -538,6 +892,30 @@ public static partial class AcBinaryDeserializer } PopulateObject(ref context, instance, metadata, depth); + + // ChainMode: Check if we already have an object with this Id in the tracker + if (context.IsChainMode && metadata.IsIId && metadata.IdGetter != null) + { + var id = metadata.IdGetter(instance); + if (id != null) + { + var idType = id.GetType(); + if (!IsDefaultValue(id, idType)) + { + // Check if we already have this object + if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj)) + { + // Update existing object's properties and return it + CopyProperties(instance, existingObj!, metadata); + return existingObj; + } + + // Register this new object + context.ChainTracker.TryRegisterIIdObject(instance); + } + } + } + return instance; } @@ -647,6 +1025,10 @@ public static partial class AcBinaryDeserializer var positionBeforeRead = context.Position; try { + // OPTIMIZATION: Use typed setters for primitives to avoid boxing + if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) + continue; + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); propInfo.SetValue(target, value); } @@ -1188,10 +1570,44 @@ public static partial class AcBinaryDeserializer var count = (int)context.ReadVarUInt(); var nextDepth = depth + 1; + + // ChainMode: Get IId info for element type + var isIId = false; + Type? idType = null; + Func? idGetter = null; + + if (context.IsChainMode) + { + var idInfo = GetIdInfo(elementType); + isIId = idInfo.IsId; + idType = idInfo.IdType; + if (isIId && idType != null) + { + var idProp = elementType.GetProperty("Id"); + if (idProp != null) + idGetter = AcSerializerCommon.CreateCompiledGetter(elementType, idProp); + } + } for (int i = 0; i < count; i++) { var value = ReadValue(ref context, elementType, nextDepth); + + // ChainMode: Check if we already have this IId object + if (context.IsChainMode && value != null && idGetter != null && idType != null) + { + var id = idGetter(value); + if (id != null && !IsDefaultValue(id, idType)) + { + if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj)) + { + // Use existing object instead of new one + targetList.Add(existingObj); + continue; + } + } + } + targetList.Add(value); } } @@ -1507,15 +1923,16 @@ public static partial class AcBinaryDeserializer } #endregion - - // Implementation moved to AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs } -sealed class TypeConversionInfo +/// +/// Cached type conversion info. Using readonly struct to avoid heap allocation. +/// +readonly struct TypeConversionInfo { - public Type UnderlyingType { get; } - public TypeCode TypeCode { get; } - public bool IsEnum { get; } + public readonly Type UnderlyingType; + public readonly TypeCode TypeCode; + public readonly bool IsEnum; public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index c98acd8..64b83f0 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -7,7 +7,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Text; -using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer; namespace AyCode.Core.Serializers.Binaries; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs index 4f1828d..a1b5605 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -10,158 +9,26 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinarySerializer { - internal sealed class BinaryTypeMetadata + internal sealed class BinaryTypeMetadata : TypeMetadataBase { public BinaryPropertyAccessor[] Properties { get; } - public BinaryTypeMetadata(Type type) + public BinaryTypeMetadata(Type type) : base(type) { - Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanRead && - p.GetIndexParameters().Length == 0 && - !HasJsonIgnoreAttribute(p)) - .Select(p => new BinaryPropertyAccessor(p)) + Properties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false) + .Select(p => new BinaryPropertyAccessor(p, type)) .ToArray(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BinaryTypeMetadata GetTypeMetadata(Type type) - => TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t)); } - internal sealed class BinaryPropertyAccessor + /// + /// Binary serialization property accessor with typed getters. + /// + internal sealed class BinaryPropertyAccessor : BinaryPropertyAccessorBase { - public readonly string Name; - public readonly byte[] NameUtf8; - public readonly Type PropertyType; - public readonly TypeCode TypeCode; - public readonly Type DeclaringType; - - private readonly Func _objectGetter; - private readonly Delegate? _typedGetter; - private readonly PropertyAccessorType _accessorType; - - /// - /// Cached property name index for metadata mode. Set by context during registration. - /// -1 means not yet cached. - /// - internal int CachedPropertyNameIndex = -1; - - public BinaryPropertyAccessor(PropertyInfo prop) + public BinaryPropertyAccessor(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) { - Name = prop.Name; - NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); - DeclaringType = prop.DeclaringType!; - PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; - TypeCode = Type.GetTypeCode(PropertyType); - - (_typedGetter, _accessorType) = CreateTypedGetterForAccessor(DeclaringType, prop); - _objectGetter = AcSerializerCommon.CreateCompiledGetter(DeclaringType, prop); } - - public PropertyAccessorType AccessorType => _accessorType; - public Func ObjectGetter => _objectGetter; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetValue(object obj) => _objectGetter(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetInt32(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetInt64(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool GetBoolean(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double GetDouble(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float GetSingle(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public decimal GetDecimal(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DateTime GetDateTime(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte GetByte(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public short GetInt16(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort GetUInt16(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint GetUInt32(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ulong GetUInt64(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Guid GetGuid(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetEnumAsInt32(object obj) => ((Func)_typedGetter!)(obj); - - private static (Delegate?, PropertyAccessorType) CreateTypedGetterForAccessor(Type declaringType, PropertyInfo prop) - { - var propType = prop.PropertyType; - var underlying = Nullable.GetUnderlyingType(propType); - if (underlying != null) - { - return (null, PropertyAccessorType.Object); - } - - if (propType.IsEnum) - { - return (AcSerializerCommon.CreateEnumGetter(declaringType, prop), PropertyAccessorType.Enum); - } - - if (ReferenceEquals(propType, GuidType)) - { - return (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Guid); - } - - var typeCode = Type.GetTypeCode(propType); - return typeCode switch - { - TypeCode.Int32 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Int32), - TypeCode.Int64 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Int64), - TypeCode.Boolean => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Boolean), - TypeCode.Double => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Double), - TypeCode.Single => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Single), - TypeCode.Decimal => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Decimal), - TypeCode.DateTime => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.DateTime), - TypeCode.Byte => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Byte), - TypeCode.Int16 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Int16), - TypeCode.UInt16 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.UInt16), - TypeCode.UInt32 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.UInt32), - TypeCode.UInt64 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.UInt64), - _ => (null, PropertyAccessorType.Object) - }; - } - } - - internal enum PropertyAccessorType : byte - { - Object = 0, - Int32, - Int64, - Boolean, - Double, - Single, - Decimal, - DateTime, - Byte, - Int16, - UInt16, - UInt32, - UInt64, - Guid, - Enum } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 27d91cb..9f1161b 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; using System.Text; using AyCode.Core.Serializers.Expressions; using static AyCode.Core.Helpers.JsonUtilities; -using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer; namespace AyCode.Core.Serializers.Binaries; @@ -770,7 +769,7 @@ public static partial class AcBinarySerializer // Object type - use regular getter var value = prop.GetValue(obj); if (value == null) return true; - if (prop.TypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value); + if (prop.PropertyTypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value); return false; } } diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs new file mode 100644 index 0000000..db580f0 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs @@ -0,0 +1,145 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Binary-specific property accessor base class. +/// Adds typed getters to avoid boxing during serialization. +/// +public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase +{ + /// + /// Cached property name index for metadata mode. Set by context during registration. + /// -1 means not yet cached. + /// + internal int CachedPropertyNameIndex = -1; + + /// + /// The accessor type for fast typed getter dispatch. + /// + public PropertyAccessorType AccessorType { get; } + + /// + /// Typed getter delegate (type depends on AccessorType). + /// + protected readonly Delegate? _typedGetter; + + /// + /// Object getter for property filter context. + /// + public Func ObjectGetter => _getter; + + protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) + { + (_typedGetter, AccessorType) = CreateTypedGetterForAccessor(declaringType, prop); + } + + private static (Delegate?, PropertyAccessorType) CreateTypedGetterForAccessor(Type declaringType, PropertyInfo prop) + { + var propType = prop.PropertyType; + var underlying = Nullable.GetUnderlyingType(propType); + if (underlying != null) + { + return (null, PropertyAccessorType.Object); + } + + if (propType.IsEnum) + { + return (AcSerializerCommon.CreateEnumGetter(declaringType, prop), PropertyAccessorType.Enum); + } + + if (ReferenceEquals(propType, GuidType)) + { + return (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Guid); + } + + var typeCode = Type.GetTypeCode(propType); + return typeCode switch + { + TypeCode.Int32 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Int32), + TypeCode.Int64 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Int64), + TypeCode.Boolean => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Boolean), + TypeCode.Double => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Double), + TypeCode.Single => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Single), + TypeCode.Decimal => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Decimal), + TypeCode.DateTime => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.DateTime), + TypeCode.Byte => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Byte), + TypeCode.Int16 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.Int16), + TypeCode.UInt16 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.UInt16), + TypeCode.UInt32 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.UInt32), + TypeCode.UInt64 => (AcSerializerCommon.CreateTypedGetter(declaringType, prop), PropertyAccessorType.UInt64), + _ => (null, PropertyAccessorType.Object) + }; + } + + #region Typed Getters + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetInt32(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetInt64(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetBoolean(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetDouble(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetSingle(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public decimal GetDecimal(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTime GetDateTime(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte GetByte(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short GetInt16(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort GetUInt16(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint GetUInt32(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong GetUInt64(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Guid GetGuid(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetEnumAsInt32(object obj) => ((Func)_typedGetter!)(obj); + + #endregion +} + +/// +/// Enum for typed property accessor dispatch. +/// +public enum PropertyAccessorType : byte +{ + Object = 0, + Int32, + Int64, + Boolean, + Double, + Single, + Decimal, + DateTime, + Byte, + Int16, + UInt16, + UInt32, + UInt64, + Guid, + Enum +} diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs new file mode 100644 index 0000000..6baa1f3 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs @@ -0,0 +1,181 @@ +using System.Collections; +using System.Reflection; +using System.Runtime.CompilerServices; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Binary-specific property setter base class. +/// Extends PropertySetterBase with binary-specific functionality and typed setters. +/// +public abstract class BinaryPropertySetterBase : PropertySetterBase +{ + /// + /// Whether this property is a complex type (not primitive, string, enum, or common value types). + /// + public bool IsComplexType { get; } + + /// + /// Whether this property is a collection type. + /// + public bool IsCollection { get; } + + /// + /// The setter type for fast typed setter dispatch. + /// + public PropertyAccessorType SetterType { get; } + + /// + /// Typed setter delegate (type depends on SetterType). + /// + protected readonly Delegate? _typedSetter; + + protected BinaryPropertySetterBase(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) + { + IsCollection = IsCollectionTypeCheck(PropertyType); + IsComplexType = IsComplex(PropertyType); + (_typedSetter, SetterType) = CreateTypedSetterForAccessor(declaringType, prop); + } + + private static (Delegate?, PropertyAccessorType) CreateTypedSetterForAccessor(Type declaringType, PropertyInfo prop) + { + var propType = prop.PropertyType; + var underlying = Nullable.GetUnderlyingType(propType); + if (underlying != null) + { + // Nullable types use Object path + return (null, PropertyAccessorType.Object); + } + + if (propType.IsEnum) + { + return (AcSerializerCommon.CreateEnumSetter(declaringType, prop), PropertyAccessorType.Enum); + } + + if (ReferenceEquals(propType, GuidType)) + { + return (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Guid); + } + + var typeCode = Type.GetTypeCode(propType); + return typeCode switch + { + TypeCode.Int32 => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Int32), + TypeCode.Int64 => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Int64), + TypeCode.Boolean => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Boolean), + TypeCode.Double => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Double), + TypeCode.Single => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Single), + TypeCode.Decimal => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Decimal), + TypeCode.DateTime => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.DateTime), + TypeCode.Byte => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Byte), + TypeCode.Int16 => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.Int16), + TypeCode.UInt16 => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.UInt16), + TypeCode.UInt32 => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.UInt32), + TypeCode.UInt64 => (AcSerializerCommon.CreateTypedSetter(declaringType, prop), PropertyAccessorType.UInt64), + TypeCode.String => (null, PropertyAccessorType.Object), // String doesn't benefit from typed setter + _ => (null, PropertyAccessorType.Object) + }; + } + + #region Typed Setters + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetInt32(object obj, int value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetInt64(object obj, long value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetBoolean(object obj, bool value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetDouble(object obj, double value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetSingle(object obj, float value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetDecimal(object obj, decimal value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetDateTime(object obj, DateTime value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetByte(object obj, byte value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetInt16(object obj, short value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetUInt16(object obj, ushort value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetUInt32(object obj, uint value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetUInt64(object obj, ulong value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetGuid(object obj, Guid value) => ((Action)_typedSetter!)(obj, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetEnumAsInt32(object obj, int value) => ((Action)_typedSetter!)(obj, value); + + #endregion + + public override void SetValue(object target, object? value) + { + try + { + base.SetValue(target, value); + } + catch (InvalidCastException ex) + { + var valueType = value?.GetType().FullName ?? "null"; + var propType = PropertyType.FullName; + var targetTypeName = target.GetType().FullName; + var underlyingType = Nullable.GetUnderlyingType(PropertyType); + + // Get actual value info for debugging + var valueInfo = value switch + { + null => "null", + int i => $"int:{i}", + long l => $"long:{l}", + DateTime dt => $"DateTime:{dt:O}", + string s => $"string:'{s}'", + _ => $"{value.GetType().Name}:{value}" + }; + + throw new InvalidCastException( + $"Cannot set property '{Name}' on type '{targetTypeName}' - " + + $"PropertyType: '{propType}', ValueType: '{valueType}', Value: {valueInfo}, " + + $"IsNullable: {underlyingType != null}, UnderlyingType: {underlyingType?.FullName ?? "N/A"}, " + + $"IsValueType: {PropertyType.IsValueType}, IsCollection: {IsCollection}, IsComplexType: {IsComplexType}", + ex); + } + } + + private static bool IsCollectionTypeCheck(Type type) + { + if (ReferenceEquals(type, StringType)) return false; + if (type.IsArray) return true; + return typeof(IEnumerable).IsAssignableFrom(type); + } + + private static bool IsComplex(Type type) + { + var actualType = Nullable.GetUnderlyingType(type) ?? type; + if (actualType.IsPrimitive) return false; + if (ReferenceEquals(actualType, StringType)) return false; + if (actualType.IsEnum) return false; + if (ReferenceEquals(actualType, GuidType)) return false; + if (ReferenceEquals(actualType, DateTimeType)) return false; + if (ReferenceEquals(actualType, DecimalType)) return false; + if (ReferenceEquals(actualType, TimeSpanType)) return false; + if (ReferenceEquals(actualType, DateTimeOffsetType)) return false; + return true; + } +} diff --git a/AyCode.Core/Serializers/DeserializeChainBase.cs b/AyCode.Core/Serializers/DeserializeChainBase.cs new file mode 100644 index 0000000..4fedc26 --- /dev/null +++ b/AyCode.Core/Serializers/DeserializeChainBase.cs @@ -0,0 +1,40 @@ +namespace AyCode.Core.Serializers; + +/// +/// Represents a deserialize chain that allows multiple deserializations from the same parsed data. +/// Maintains reference identity for IId objects across chain operations. +/// Implements IDisposable - call Dispose() when done or use 'using' statement. +/// +public interface IDeserializeChain : IDisposable +{ + /// + /// The first deserialized value. + /// + T? Value { get; } + + /// + /// Deserialize to another type from the same data. + /// + TResult? ThenDeserialize(); + + /// + /// Populate an existing object from the same data. + /// Returns this chain for fluent API. + /// + IDeserializeChain ThenPopulate(object target); +} + +/// +/// Empty deserialize chain implementation for null/empty data. +/// +public sealed class EmptyDeserializeChain : IDeserializeChain +{ + public static readonly IDeserializeChain Instance = new EmptyDeserializeChain(); + + private EmptyDeserializeChain() { } + + public T? Value => default; + public TResult? ThenDeserialize() => default; + public IDeserializeChain ThenPopulate(object target) => this; + public void Dispose() { } +} diff --git a/AyCode.Core/Serializers/IIdCollectionMergeHelper.cs b/AyCode.Core/Serializers/IIdCollectionMergeHelper.cs new file mode 100644 index 0000000..d4bd3cc --- /dev/null +++ b/AyCode.Core/Serializers/IIdCollectionMergeHelper.cs @@ -0,0 +1,94 @@ +using System.Collections; +using System.Runtime.CompilerServices; +using AyCode.Core.Helpers; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers; + +/// +/// Helper class for merging IId collections during deserialization. +/// Shared between JSON and Binary deserializers. +/// +public static class IIdCollectionMergeHelper +{ + /// + /// Builds a lookup dictionary from an existing IId collection. + /// Maps Id values to their corresponding items. + /// + /// The existing collection to index. + /// Function to extract Id from an item. + /// The type of the Id property. + /// Dictionary mapping Id to item, or null if collection is empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Dictionary? BuildIdLookup( + IList existingList, + Func idGetter, + Type idType) + { + var count = existingList.Count; + if (count == 0) return null; + + var dict = new Dictionary(count); + for (var i = 0; i < count; i++) + { + var item = existingList[i]; + if (item == null) continue; + + var id = idGetter(item); + if (id != null && !IsDefaultValue(id, idType)) + dict[id] = item; + } + return dict; + } + + /// + /// Removes orphaned items from the collection that are not present in the source IDs. + /// + /// The collection to clean up. + /// Lookup dictionary of existing items. + /// Set of IDs that were seen in source data. + public static void RemoveOrphanedItems( + IList existingList, + Dictionary existingById, + HashSet sourceIds) + { + var itemsToRemove = new List(); + foreach (var kvp in existingById) + { + if (!sourceIds.Contains(kvp.Key)) + { + itemsToRemove.Add(kvp.Value); + } + } + + foreach (var item in itemsToRemove) + { + existingList.Remove(item); + } + } + + /// + /// Copies properties from source object to target object using metadata. + /// + /// Type of property info (varies by serializer). + /// Source object to copy from. + /// Target object to copy to. + /// Array of property accessors. + /// Function to get property value. + /// Action to set property value. + public static void CopyProperties( + object source, + object target, + TPropertyInfo[] properties, + Func getter, + Action setter) + { + for (var i = 0; i < properties.Length; i++) + { + var prop = properties[i]; + var value = getter(prop, source); + if (value != null) + setter(prop, target, value); + } + } +} diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializationContext.cs index 8f19fdf..6ec54f5 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializationContext.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializationContext.cs @@ -53,6 +53,17 @@ public static partial class AcJsonDeserializer public bool IsMergeMode { get; set; } public bool UseReferenceHandling { get; private set; } public byte MaxDepth { get; private set; } + + /// + /// Chain reference tracker for maintaining object identity across chain operations. + /// Only set when in chain mode (CreateDeserializeChain). + /// + public AcSerializerCommon.ChainReferenceTracker? ChainTracker { get; set; } + + /// + /// Returns true if in chain mode (ChainTracker is set). + /// + public bool IsChainMode => ChainTracker != null; public DeserializationContext(in AcJsonSerializerOptions options) { @@ -64,12 +75,14 @@ public static partial class AcJsonDeserializer UseReferenceHandling = options.UseReferenceHandling; MaxDepth = options.MaxDepth; IsMergeMode = false; + ChainTracker = null; } public void Clear() { _idToObject?.Clear(); _propertiesToResolve?.Clear(); + ChainTracker = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializeTypeMetadata.cs index 4ee2576..8784be7 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.DeserializeTypeMetadata.cs @@ -1,12 +1,8 @@ -using System.Collections; using System.Collections.Concurrent; using System.Collections.Frozen; -using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using System.Text.Json; -using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Jsons; @@ -18,31 +14,20 @@ public static partial class AcJsonDeserializer private static DeserializeTypeMetadata GetTypeMetadata(in Type type) => TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t)); - private sealed class DeserializeTypeMetadata + private sealed class DeserializeTypeMetadata : TypeMetadataBase { public FrozenDictionary PropertySettersFrozen { get; } public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects) - public Func? CompiledConstructor { get; } - public DeserializeTypeMetadata(Type type) + public DeserializeTypeMetadata(Type type) : base(type) { - CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type); + var props = GetSerializableProperties(type, requiresRead: true, requiresWrite: true).ToList(); - var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - var propsList = new List(allProps.Length); - - foreach (var p in allProps) - { - if (!p.CanWrite || !p.CanRead || p.GetIndexParameters().Length != 0) continue; - if (HasJsonIgnoreAttribute(p)) continue; - propsList.Add(p); - } - - var propertySetters = new Dictionary(propsList.Count, StringComparer.OrdinalIgnoreCase); - var propsArray = new PropertySetterInfo[propsList.Count]; + var propertySetters = new Dictionary(props.Count, StringComparer.OrdinalIgnoreCase); + var propsArray = new PropertySetterInfo[props.Count]; var index = 0; - foreach (var prop in propsList) + foreach (var prop in props) { var propInfo = new PropertySetterInfo(prop, type); propertySetters[prop.Name] = propInfo; @@ -50,7 +35,6 @@ public static partial class AcJsonDeserializer } PropertiesArray = propsArray; - // Create frozen dictionary for faster lookup in hot paths PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } @@ -74,104 +58,16 @@ public static partial class AcJsonDeserializer } } - private sealed class PropertySetterInfo + /// + /// JSON deserialization property setter. + /// + private sealed class PropertySetterInfo : JsonPropertySetterBase { - public readonly Type PropertyType; - public readonly Type UnderlyingType; // Cached: Nullable.GetUnderlyingType(PropertyType) ?? PropertyType - public readonly TypeCode PropertyTypeCode; // Cached TypeCode for fast primitive reading - public readonly bool IsNullable; - public readonly bool IsIIdCollection; - public readonly Type? ElementType; - public readonly Type? ElementIdType; - public readonly Func? ElementIdGetter; - public readonly byte[] NameUtf8; // Pre-computed UTF-8 bytes of property name for fast matching - - // Typed setters to avoid boxing for primitives - private readonly Action _setter; - private readonly Func _getter; - - // Typed setters for common primitive types (avoid boxing) - internal readonly Action? _setInt32; - internal readonly Action? _setInt64; - internal readonly Action? _setDouble; - internal readonly Action? _setBool; - internal readonly Action? _setDecimal; - internal readonly Action? _setSingle; - internal readonly Action? _setDateTime; - internal readonly Action? _setGuid; - - // Pre-boxed boolean values to avoid repeated boxing - private static readonly object BoxedTrue = true; - private static readonly object BoxedFalse = false; - - public PropertySetterInfo(PropertyInfo prop, Type declaringType) + public PropertySetterInfo(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) { - PropertyType = prop.PropertyType; - var underlying = Nullable.GetUnderlyingType(PropertyType); - IsNullable = underlying != null; - UnderlyingType = underlying ?? PropertyType; - PropertyTypeCode = Type.GetTypeCode(UnderlyingType); - NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); - - _setter = AcSerializerCommon.CreateCompiledSetter(declaringType, prop); - _getter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop); - - // Create typed setters for common primitives to avoid boxing - if (!IsNullable) - { - if (ReferenceEquals(PropertyType, IntType)) - _setInt32 = CreateTypedSetter(declaringType, prop); - else if (ReferenceEquals(PropertyType, LongType)) - _setInt64 = CreateTypedSetter(declaringType, prop); - else if (ReferenceEquals(PropertyType, DoubleType)) - _setDouble = CreateTypedSetter(declaringType, prop); - else if (ReferenceEquals(PropertyType, BoolType)) - _setBool = CreateTypedSetter(declaringType, prop); - else if (ReferenceEquals(PropertyType, DecimalType)) - _setDecimal = CreateTypedSetter(declaringType, prop); - else if (ReferenceEquals(PropertyType, FloatType)) - _setSingle = CreateTypedSetter(declaringType, prop); - else if (ReferenceEquals(PropertyType, DateTimeType)) - _setDateTime = CreateTypedSetter(declaringType, prop); - else if (ReferenceEquals(PropertyType, GuidType)) - _setGuid = CreateTypedSetter(declaringType, prop); - } - - ElementType = GetCollectionElementType(PropertyType); - var isCollection = ElementType != null && ElementType != typeof(object) && - typeof(IEnumerable).IsAssignableFrom(PropertyType) && - !ReferenceEquals(PropertyType, StringType); - - if (isCollection && ElementType != null) - { - var idInfo = GetIdInfo(ElementType); - if (idInfo.IsId) - { - IsIIdCollection = true; - ElementIdType = idInfo.IdType; - var idProp = ElementType.GetProperty("Id"); - if (idProp != null) - ElementIdGetter = AcSerializerCommon.CreateCompiledGetter(ElementType, idProp); - } - } } - private static Action CreateTypedSetter(Type declaringType, PropertyInfo prop) - { - var objParam = Expression.Parameter(typeof(object), "obj"); - var valueParam = Expression.Parameter(typeof(T), "value"); - var castObj = Expression.Convert(objParam, declaringType); - var propAccess = Expression.Property(castObj, prop); - var assign = Expression.Assign(propAccess, valueParam); - return Expression.Lambda>(assign, objParam, valueParam).Compile(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetValue(object target, object? value) => _setter(target, value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetValue(object target) => _getter(target); - /// /// Read and set value directly from Utf8JsonReader, avoiding boxing for primitives. /// Returns true if value was set, false if it needs fallback to SetValue. @@ -186,48 +82,57 @@ public static partial class AcJsonDeserializer { if (IsNullable || !PropertyType.IsValueType) { - _setter(target, null); + SetValue(target, null); return true; } return true; // Skip null for non-nullable value types } - // Fast path for booleans - no boxing needed with typed setter + // Fast path for booleans if (tokenType == JsonTokenType.True) { - if (_setBool != null) { _setBool(target, true); return true; } - _setter(target, BoxedTrue); + TrySetBoolean(target, true); return true; } if (tokenType == JsonTokenType.False) { - if (_setBool != null) { _setBool(target, false); return true; } - _setter(target, BoxedFalse); + TrySetBoolean(target, false); return true; } - // Fast path for numbers - use typed setters when available + // Fast path for numbers - try typed setters, TrySet returns false if type doesn't match if (tokenType == JsonTokenType.Number) { - if (_setInt32 != null) { _setInt32(target, reader.GetInt32()); return true; } - if (_setInt64 != null) { _setInt64(target, reader.GetInt64()); return true; } - if (_setDouble != null) { _setDouble(target, reader.GetDouble()); return true; } - if (_setDecimal != null) { _setDecimal(target, reader.GetDecimal()); return true; } - if (_setSingle != null) { _setSingle(target, reader.GetSingle()); return true; } + // Use TryGet to handle scientific notation + if (reader.TryGetInt32(out var i32) && TrySetInt32(target, i32)) return true; + if (reader.TryGetInt64(out var i64) && TrySetInt64(target, i64)) return true; + if (TrySetDouble(target, reader.GetDouble())) return true; + if (TrySetDecimal(target, reader.GetDecimal())) return true; + if (TrySetSingle(target, reader.GetSingle())) return true; return false; // Fallback to boxed path } - // Fast path for strings - common types + // Fast path for strings - check type BEFORE calling reader.Get*() to avoid FormatException if (tokenType == JsonTokenType.String) { - if (ReferenceEquals(UnderlyingType, StringType)) + if (ReferenceEquals(UnderlyingType, typeof(string))) { - _setter(target, reader.GetString()); + SetValue(target, reader.GetString()); return true; } - if (_setDateTime != null) { _setDateTime(target, reader.GetDateTime()); return true; } - if (_setGuid != null) { _setGuid(target, reader.GetGuid()); return true; } - return false; // Fallback to boxed path + // Only call reader.GetDateTime() if property is DateTime type + if (ReferenceEquals(UnderlyingType, typeof(DateTime))) + { + TrySetDateTime(target, reader.GetDateTime()); + return true; + } + // Only call reader.GetGuid() if property is Guid type + if (ReferenceEquals(UnderlyingType, typeof(Guid))) + { + TrySetGuid(target, reader.GetGuid()); + return true; + } + return false; // Fallback to boxed path for other string-based types } return false; // Complex types need standard handling diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs index fc7961d..fe020bc 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs @@ -58,8 +58,48 @@ public static partial class AcJsonDeserializer PopulateObjectInternal(element, instance, metadata, context, depth); + // ChainMode: Check if we already have an object with this Id in the tracker + if (context.IsChainMode) + { + var (isIId, idType) = GetIdInfo(targetType); + if (isIId && idType != null) + { + var idProp = targetType.GetProperty("Id"); + if (idProp != null) + { + var id = idProp.GetValue(instance); + if (id != null && !IsDefaultValue(id, idType)) + { + // Check if we already have this object + if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj)) + { + // Update existing object's properties and return it + CopyPropertiesJson(instance, existingObj!, metadata); + return existingObj; + } + + // Register this new object + context.ChainTracker.TryRegisterIIdObject(instance); + } + } + } + } + return instance; } + + /// + /// Copies properties from source to target using JSON metadata. + /// + private static void CopyPropertiesJson(object source, object target, DeserializeTypeMetadata metadata) + { + foreach (var prop in metadata.PropertiesArray) + { + var value = prop.GetValue(source); + if (value != null) + prop.SetValue(target, value); + } + } private static void PopulateObjectInternal(in JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) { @@ -174,9 +214,39 @@ public static partial class AcJsonDeserializer targetList.Clear(); var nextDepth = depth + 1; + // ChainMode: Get IId info for element type + var isIId = false; + Type? idType = null; + System.Reflection.PropertyInfo? idProp = null; + + if (context.IsChainMode) + { + var idInfo = GetIdInfo(elementType); + isIId = idInfo.IsId; + idType = idInfo.IdType; + if (isIId && idType != null) + idProp = elementType.GetProperty("Id"); + } + foreach (var item in arrayElement.EnumerateArray()) { var value = ReadValue(item, elementType, context, nextDepth); + + // ChainMode: Check if we already have this IId object + if (context.IsChainMode && value != null && idProp != null && idType != null) + { + var id = idProp.GetValue(value); + if (id != null && !IsDefaultValue(id, idType)) + { + if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj)) + { + // Use existing object instead of new one + targetList.Add(existingObj); + continue; + } + } + } + if (value != null) targetList.Add(value); } diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs index a38cc72..3375f14 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs @@ -77,18 +77,20 @@ public static partial class AcJsonDeserializer { return propInfo.PropertyTypeCode switch { - TypeCode.Int32 => reader.GetInt32(), - TypeCode.Int64 => reader.GetInt64(), + TypeCode.Int32 => reader.TryGetInt32(out var i32) ? i32 : (int)reader.GetDouble(), + TypeCode.Int64 => reader.TryGetInt64(out var i64) ? i64 : (long)reader.GetDouble(), TypeCode.Double => reader.GetDouble(), TypeCode.Decimal => reader.GetDecimal(), TypeCode.Single => reader.GetSingle(), - TypeCode.Byte => reader.GetByte(), - TypeCode.Int16 => reader.GetInt16(), - TypeCode.UInt16 => reader.GetUInt16(), - TypeCode.UInt32 => reader.GetUInt32(), - TypeCode.UInt64 => reader.GetUInt64(), - TypeCode.SByte => reader.GetSByte(), - _ => propInfo.UnderlyingType.IsEnum ? Enum.ToObject(propInfo.UnderlyingType, reader.GetInt32()) : reader.GetDouble() + TypeCode.Byte => reader.TryGetByte(out var b) ? b : (byte)reader.GetDouble(), + TypeCode.Int16 => reader.TryGetInt16(out var i16) ? i16 : (short)reader.GetDouble(), + TypeCode.UInt16 => reader.TryGetUInt16(out var ui16) ? ui16 : (ushort)reader.GetDouble(), + TypeCode.UInt32 => reader.TryGetUInt32(out var ui32) ? ui32 : (uint)reader.GetDouble(), + TypeCode.UInt64 => reader.TryGetUInt64(out var ui64) ? ui64 : (ulong)reader.GetDouble(), + TypeCode.SByte => reader.TryGetSByte(out var sb) ? sb : (sbyte)reader.GetDouble(), + _ => propInfo.UnderlyingType.IsEnum + ? Enum.ToObject(propInfo.UnderlyingType, reader.TryGetInt32(out var enumVal) ? enumVal : (int)reader.GetDouble()) + : reader.GetDouble() }; } @@ -152,18 +154,18 @@ public static partial class AcJsonDeserializer return typeCode switch { - TypeCode.Int32 => reader.GetInt32(), - TypeCode.Int64 => reader.GetInt64(), + TypeCode.Int32 => reader.TryGetInt32(out var i32) ? i32 : (int)reader.GetDouble(), + TypeCode.Int64 => reader.TryGetInt64(out var i64) ? i64 : (long)reader.GetDouble(), TypeCode.Double => reader.GetDouble(), TypeCode.Decimal => reader.GetDecimal(), TypeCode.Single => reader.GetSingle(), - TypeCode.Byte => reader.GetByte(), - TypeCode.Int16 => reader.GetInt16(), - TypeCode.UInt16 => reader.GetUInt16(), - TypeCode.UInt32 => reader.GetUInt32(), - TypeCode.UInt64 => reader.GetUInt64(), - TypeCode.SByte => reader.GetSByte(), - _ => type.IsEnum ? Enum.ToObject(type, reader.GetInt32()) : reader.GetDouble() + TypeCode.Byte => reader.TryGetByte(out var b) ? b : (byte)reader.GetDouble(), + TypeCode.Int16 => reader.TryGetInt16(out var i16) ? i16 : (short)reader.GetDouble(), + TypeCode.UInt16 => reader.TryGetUInt16(out var ui16) ? ui16 : (ushort)reader.GetDouble(), + TypeCode.UInt32 => reader.TryGetUInt32(out var ui32) ? ui32 : (uint)reader.GetDouble(), + TypeCode.UInt64 => reader.TryGetUInt64(out var ui64) ? ui64 : (ulong)reader.GetDouble(), + TypeCode.SByte => reader.TryGetSByte(out var sb) ? sb : (sbyte)reader.GetDouble(), + _ => type.IsEnum ? Enum.ToObject(type, reader.TryGetInt32(out var enumVal) ? enumVal : (int)reader.GetDouble()) : reader.GetDouble() }; } diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs index 5604ae8..4725683 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs @@ -488,7 +488,7 @@ public static partial class AcJsonDeserializer /// /// Create a deserialize chain that parses JSON once and allows multiple deserializations. - /// Efficient for deserializing the same JSON to multiple different types. + /// Maintains reference identity for IId objects across chain operations. /// public static IDeserializeChain CreateDeserializeChain(string json) => CreateDeserializeChain(json, AcJsonSerializerOptions.Default); @@ -499,57 +499,21 @@ public static partial class AcJsonDeserializer public static IDeserializeChain CreateDeserializeChain(string json, in AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") - return DeserializeChain.Empty; + return EmptyDeserializeChain.Instance; var targetType = typeof(T); ValidateJson(json, targetType); var doc = JsonDocument.Parse(json); + var chainTracker = new AcSerializerCommon.ChainReferenceTracker(); var context = DeserializationContextPool.Get(options); + context.ChainTracker = chainTracker; try { var result = ReadValue(doc.RootElement, targetType, context, 0); context.ResolveReferences(); - return new DeserializeChain(doc, context, options, (T?)result); - } - catch - { - DeserializationContextPool.Return(context); - doc.Dispose(); - throw; - } - } - - /// - /// Create a populate chain that parses JSON once and allows populating multiple objects. - /// Efficient for populating multiple objects from the same JSON source. - /// - public static IPopulateChain CreatePopulateChain(string json, object target) - => CreatePopulateChain(json, target, AcJsonSerializerOptions.Default); - - /// - /// Create a populate chain with options. - /// - public static IPopulateChain CreatePopulateChain(string json, object target, in AcJsonSerializerOptions options) - { - ArgumentNullException.ThrowIfNull(target); - - if (string.IsNullOrEmpty(json) || json == "null") - return PopulateChain.Empty; - - var targetType = target.GetType(); - ValidateJson(json, targetType); - - var doc = JsonDocument.Parse(json); - var context = DeserializationContextPool.Get(options); - context.IsMergeMode = true; - - try - { - PopulateFromDocument(doc.RootElement, target, targetType, context); - context.ResolveReferences(); - return new PopulateChain(doc, context, options); + return new JsonDeserializeChain(doc, context, chainTracker, (T?)result); } catch { @@ -586,38 +550,39 @@ public static partial class AcJsonDeserializer #region Chain Implementations (Nested Classes) /// - /// Implementation of deserialize chain. + /// JSON implementation of deserialize chain. + /// Maintains reference identity for IId objects across chain operations. /// - private sealed class DeserializeChain : IDeserializeChain + private sealed class JsonDeserializeChain : IDeserializeChain { - public static readonly IDeserializeChain Empty = new EmptyDeserializeChain(); - private JsonDocument? _document; private DeserializationContext? _context; - private readonly AcJsonSerializerOptions _options; + private readonly AcSerializerCommon.ChainReferenceTracker _chainTracker; + private bool _isDisposed; public T? Value { get; } - public DeserializeChain(JsonDocument document, DeserializationContext context, AcJsonSerializerOptions options, T? value) + public JsonDeserializeChain(JsonDocument document, DeserializationContext context, AcSerializerCommon.ChainReferenceTracker chainTracker, T? value) { _document = document; _context = context; - _options = options; + _chainTracker = chainTracker; Value = value; } - public TOther? ThenDeserialize() + public TResult? ThenDeserialize() { + ThrowIfDisposed(); if (_document == null || _context == null) - throw new ObjectDisposedException(nameof(DeserializeChain)); + throw new ObjectDisposedException(nameof(JsonDeserializeChain)); - var targetType = typeof(TOther); + var targetType = typeof(TResult); try { var result = ReadValue(_document.RootElement, targetType, _context, 0); _context.ResolveReferences(); - return (TOther?)result; + return (TResult?)result; } catch (AcJsonDeserializationException) { throw; } catch (Exception ex) @@ -628,52 +593,12 @@ public static partial class AcJsonDeserializer } } - public void Dispose() - { - if (_context != null) - { - DeserializationContextPool.Return(_context); - _context = null; - } - if (_document != null) - { - _document.Dispose(); - _document = null; - } - } - - private sealed class EmptyDeserializeChain : IDeserializeChain - { - public T? Value => default; - public TOther? ThenDeserialize() => default; - public void Dispose() { } - } - } - - /// - /// Implementation of populate chain. - /// - private sealed class PopulateChain : IPopulateChain - { - public static readonly IPopulateChain Empty = new EmptyPopulateChain(); - - private JsonDocument? _document; - private DeserializationContext? _context; - private readonly AcJsonSerializerOptions _options; - - public PopulateChain(JsonDocument document, DeserializationContext context, AcJsonSerializerOptions options) - { - _document = document; - _context = context; - _options = options; - } - - public IPopulateChain ThenPopulate(object target) + public IDeserializeChain ThenPopulate(object target) { ArgumentNullException.ThrowIfNull(target); - + ThrowIfDisposed(); if (_document == null || _context == null) - throw new ObjectDisposedException(nameof(PopulateChain)); + throw new ObjectDisposedException(nameof(JsonDeserializeChain)); var targetType = target.GetType(); @@ -692,59 +617,26 @@ public static partial class AcJsonDeserializer } } + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + } + public void Dispose() { + if (_isDisposed) return; + _isDisposed = true; + _chainTracker.Clear(); + if (_context != null) { DeserializationContextPool.Return(_context); _context = null; } - if (_document != null) - { - _document.Dispose(); - _document = null; - } - } - - private sealed class EmptyPopulateChain : IPopulateChain - { - public IPopulateChain ThenPopulate(object target) => this; - public void Dispose() { } + _document?.Dispose(); + _document = null; } } #endregion } - -#region Chain Public Interfaces - -/// -/// Represents a deserialize chain that allows multiple deserializations from the same parsed JSON. -/// Implements IDisposable - call Dispose() when done or use 'using' statement. -/// -public interface IDeserializeChain : IDisposable -{ - /// - /// The first deserialized value. - /// - T? Value { get; } - - /// - /// Deserialize to another type from the same JSON. - /// - TOther? ThenDeserialize(); -} - -/// -/// Represents a populate chain that allows populating multiple objects from the same parsed JSON. -/// Implements IDisposable - call Dispose() when done or use 'using' statement. -/// -public interface IPopulateChain : IDisposable -{ - /// - /// Populate another object from the same JSON. - /// - IPopulateChain ThenPopulate(object target); -} - -#endregion diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.SerializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.SerializationContext.cs index 8219d43..72f6194 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.SerializationContext.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.SerializationContext.cs @@ -69,9 +69,9 @@ public static partial class AcJsonSerializer if (UseReferenceHandling) { - _scanOccurrences ??= new Dictionary(64, ReferenceEqualityComparer.Instance); - _writtenRefs ??= new Dictionary(32, ReferenceEqualityComparer.Instance); - _multiReferenced ??= new HashSet(32, ReferenceEqualityComparer.Instance); + _scanOccurrences ??= new Dictionary(64, Serializers.ReferenceEqualityComparer.Instance); + _writtenRefs ??= new Dictionary(32, Serializers.ReferenceEqualityComparer.Instance); + _multiReferenced ??= new HashSet(32, Serializers.ReferenceEqualityComparer.Instance); } } @@ -130,13 +130,3 @@ public static partial class AcJsonSerializer } } } - -/// -/// Reference equality comparer for object identity comparison. -/// -internal sealed class ReferenceEqualityComparer : IEqualityComparer -{ - public static readonly ReferenceEqualityComparer Instance = new(); - public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); -} diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.TypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.TypeMetadata.cs index b3a455b..ae72e7f 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.TypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.TypeMetadata.cs @@ -1,9 +1,7 @@ using System.Collections.Concurrent; -using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; -using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Jsons; @@ -15,39 +13,26 @@ public static partial class AcJsonSerializer private static TypeMetadata GetTypeMetadata(in Type type) => TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t)); - private sealed class TypeMetadata + private sealed class TypeMetadata : TypeMetadataBase { public PropertyAccessor[] Properties { get; } - public TypeMetadata(Type type) + public TypeMetadata(Type type) : base(type) { - Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanRead && - p.GetIndexParameters().Length == 0 && - !HasJsonIgnoreAttribute(p)) - .Select(p => new PropertyAccessor(p)) + Properties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false) + .Select(p => new PropertyAccessor(p, type)) .ToArray(); } } - private sealed class PropertyAccessor + /// + /// JSON serialization property accessor. + /// + private sealed class PropertyAccessor : JsonPropertyAccessorBase { - public readonly string JsonName; - public readonly JsonEncodedText JsonNameEncoded; // STJ optimization - pre-encoded property name - public readonly Type PropertyType; - public readonly TypeCode PropertyTypeCode; - private readonly Func _getter; - - public PropertyAccessor(PropertyInfo prop) + public PropertyAccessor(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) { - JsonName = prop.Name; - JsonNameEncoded = JsonEncodedText.Encode(prop.Name); - PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; - PropertyTypeCode = Type.GetTypeCode(PropertyType); - _getter = AcSerializerCommon.CreateCompiledGetter(prop.DeclaringType!, prop); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetValue(object obj) => _getter(obj); } } diff --git a/AyCode.Core/Serializers/Jsons/JsonPropertyAccessorBase.cs b/AyCode.Core/Serializers/Jsons/JsonPropertyAccessorBase.cs new file mode 100644 index 0000000..fed7ce9 --- /dev/null +++ b/AyCode.Core/Serializers/Jsons/JsonPropertyAccessorBase.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Text.Json; + +namespace AyCode.Core.Serializers.Jsons; + +/// +/// JSON-specific property accessor base class. +/// Adds JSON-specific encoding (JsonEncodedText for Utf8JsonWriter). +/// +public abstract class JsonPropertyAccessorBase : PropertyAccessorBase +{ + /// + /// Pre-encoded property name for Utf8JsonWriter (STJ optimization). + /// + public JsonEncodedText JsonNameEncoded { get; } + + protected JsonPropertyAccessorBase(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) + { + JsonNameEncoded = JsonEncodedText.Encode(prop.Name); + } +} diff --git a/AyCode.Core/Serializers/Jsons/JsonPropertySetterBase.cs b/AyCode.Core/Serializers/Jsons/JsonPropertySetterBase.cs new file mode 100644 index 0000000..7bc368c --- /dev/null +++ b/AyCode.Core/Serializers/Jsons/JsonPropertySetterBase.cs @@ -0,0 +1,175 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers.Jsons; + +/// +/// JSON-specific property setter base class. +/// Adds typed setters for common primitives to avoid boxing during deserialization. +/// +public abstract class JsonPropertySetterBase : PropertySetterBase +{ + // Pre-boxed boolean values to avoid repeated boxing + private static readonly object BoxedTrue = true; + private static readonly object BoxedFalse = false; + + // Typed setters for common primitive types (avoid boxing) + internal readonly Action? _setInt32; + internal readonly Action? _setInt64; + internal readonly Action? _setDouble; + internal readonly Action? _setBool; + internal readonly Action? _setDecimal; + internal readonly Action? _setSingle; + internal readonly Action? _setDateTime; + internal readonly Action? _setGuid; + + protected JsonPropertySetterBase(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) + { + // Create typed setters for common primitives to avoid boxing + if (!IsNullable) + { + if (ReferenceEquals(PropertyType, IntType)) + _setInt32 = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, LongType)) + _setInt64 = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, DoubleType)) + _setDouble = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, BoolType)) + _setBool = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, DecimalType)) + _setDecimal = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, FloatType)) + _setSingle = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, DateTimeType)) + _setDateTime = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, GuidType)) + _setGuid = CreateTypedSetter(declaringType, prop); + } + } + + private static Action CreateTypedSetter(Type declaringType, PropertyInfo prop) + { + var objParam = Expression.Parameter(typeof(object), "obj"); + var valueParam = Expression.Parameter(typeof(T), "value"); + var castObj = Expression.Convert(objParam, declaringType); + var propAccess = Expression.Property(castObj, prop); + var assign = Expression.Assign(propAccess, valueParam); + return Expression.Lambda>(assign, objParam, valueParam).Compile(); + } + + /// + /// Try to set a boolean value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetBoolean(object target, bool value) + { + if (_setBool != null) + { + _setBool(target, value); + return true; + } + _setter(target, value ? BoxedTrue : BoxedFalse); + return true; + } + + /// + /// Try to set an int32 value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetInt32(object target, int value) + { + if (_setInt32 != null) + { + _setInt32(target, value); + return true; + } + return false; + } + + /// + /// Try to set an int64 value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetInt64(object target, long value) + { + if (_setInt64 != null) + { + _setInt64(target, value); + return true; + } + return false; + } + + /// + /// Try to set a double value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetDouble(object target, double value) + { + if (_setDouble != null) + { + _setDouble(target, value); + return true; + } + return false; + } + + /// + /// Try to set a decimal value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetDecimal(object target, decimal value) + { + if (_setDecimal != null) + { + _setDecimal(target, value); + return true; + } + return false; + } + + /// + /// Try to set a float value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetSingle(object target, float value) + { + if (_setSingle != null) + { + _setSingle(target, value); + return true; + } + return false; + } + + /// + /// Try to set a DateTime value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetDateTime(object target, DateTime value) + { + if (_setDateTime != null) + { + _setDateTime(target, value); + return true; + } + return false; + } + + /// + /// Try to set a Guid value without boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetGuid(object target, Guid value) + { + if (_setGuid != null) + { + _setGuid(target, value); + return true; + } + return false; + } +} diff --git a/AyCode.Core/Serializers/PropertyAccessorBase.cs b/AyCode.Core/Serializers/PropertyAccessorBase.cs new file mode 100644 index 0000000..7e0a118 --- /dev/null +++ b/AyCode.Core/Serializers/PropertyAccessorBase.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; + +namespace AyCode.Core.Serializers; + +/// +/// Base class for property accessors used by all serializers. +/// Contains common property metadata and getter functionality. +/// +public abstract class PropertyAccessorBase +{ + /// + /// Property name. + /// + public string Name { get; } + + /// + /// Pre-encoded UTF8 bytes of property name for fast matching. + /// + public byte[] NameUtf8 { get; } + + /// + /// The property type (may be nullable). + /// + public Type PropertyType { get; } + + /// + /// The underlying type (unwrapped from Nullable if applicable). + /// + public Type UnderlyingType { get; } + + /// + /// Cached TypeCode for fast primitive type dispatch. + /// + public TypeCode PropertyTypeCode { get; } + + /// + /// Whether the property type is nullable. + /// + public bool IsNullable { get; } + + /// + /// The declaring type of this property. + /// + public Type DeclaringType { get; } + + /// + /// Compiled getter delegate for reading property values. + /// + protected readonly Func _getter; + + protected PropertyAccessorBase(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); + + _getter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop); + } + + /// + /// Gets the property value from the target object. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object? GetValue(object obj) => _getter(obj); +} diff --git a/AyCode.Core/Serializers/PropertySetterBase.cs b/AyCode.Core/Serializers/PropertySetterBase.cs new file mode 100644 index 0000000..9284846 --- /dev/null +++ b/AyCode.Core/Serializers/PropertySetterBase.cs @@ -0,0 +1,71 @@ +using System.Collections; +using System.Reflection; +using System.Runtime.CompilerServices; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers; + +/// +/// Base class for property accessors that also support setting values. +/// Used by deserializers. Extends PropertyAccessorBase with setter and IId collection support. +/// +public abstract class PropertySetterBase : PropertyAccessorBase +{ + /// + /// Compiled setter delegate for writing property values. + /// + protected readonly Action _setter; + + /// + /// Whether this property is a collection of IId elements. + /// + public bool IsIIdCollection { get; } + + /// + /// Element type if this property is a collection, null otherwise. + /// + public Type? ElementType { get; } + + /// + /// The Id type of collection elements (if IsIIdCollection is true). + /// + public Type? ElementIdType { get; } + + /// + /// Compiled getter for the Id property of collection elements. + /// + public Func? ElementIdGetter { get; } + + protected PropertySetterBase(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) + { + _setter = AcSerializerCommon.CreateCompiledSetter(declaringType, prop); + + // Determine collection element type + ElementType = GetCollectionElementType(PropertyType); + + var isCollection = ElementType != null && + ElementType != typeof(object) && + typeof(IEnumerable).IsAssignableFrom(PropertyType) && + !ReferenceEquals(PropertyType, StringType); + + if (isCollection && ElementType != null) + { + var idInfo = GetIdInfo(ElementType); + if (idInfo.IsId) + { + IsIIdCollection = true; + ElementIdType = idInfo.IdType; + var idProp = ElementType.GetProperty("Id"); + if (idProp != null) + ElementIdGetter = AcSerializerCommon.CreateCompiledGetter(ElementType, idProp); + } + } + } + + /// + /// Sets the property value on the target object. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void SetValue(object target, object? value) => _setter(target, value); +} diff --git a/AyCode.Core/Serializers/ReferenceTracker.cs b/AyCode.Core/Serializers/ReferenceTracker.cs new file mode 100644 index 0000000..dd9fe1f --- /dev/null +++ b/AyCode.Core/Serializers/ReferenceTracker.cs @@ -0,0 +1,138 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace AyCode.Core.Serializers; + +/// +/// Shared reference tracking logic for serialization. +/// Tracks object references to enable $id/$ref handling for circular references. +/// +public sealed class SerializationReferenceTracker +{ + private readonly Dictionary _scanOccurrences; + private readonly Dictionary _writtenRefs; + private readonly HashSet _multiReferenced; + private int _nextId; + + public SerializationReferenceTracker(int initialCapacity = 32) + { + _scanOccurrences = new(initialCapacity, ReferenceEqualityComparer.Instance); + _writtenRefs = new(initialCapacity, ReferenceEqualityComparer.Instance); + _multiReferenced = new(initialCapacity, ReferenceEqualityComparer.Instance); + _nextId = 1; + } + + /// + /// Tracks an object during the scanning phase. + /// Returns true if this is the first occurrence (should continue scanning children). + /// Returns false if object was seen before (multi-referenced). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrackForScanning(object obj) + { + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); + if (exists) + { + count++; + _multiReferenced.Add(obj); + return false; + } + count = 1; + return true; + } + + /// + /// Checks if an object should have an $id written and returns the id. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldWriteId(object obj, out string id) + { + if (_multiReferenced.Contains(obj) && !_writtenRefs.ContainsKey(obj)) + { + id = _nextId++.ToString(); + return true; + } + id = ""; + return false; + } + + /// + /// Marks an object as written with its assigned id. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MarkAsWritten(object obj, string id) => _writtenRefs[obj] = id; + + /// + /// Tries to get an existing reference id for an object. + /// If found, a $ref should be written instead of the full object. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetExistingRef(object obj, out string refId) + { + return _writtenRefs.TryGetValue(obj, out refId!); + } + + /// + /// Clears all tracking data for reuse. + /// + public void Clear() + { + _scanOccurrences.Clear(); + _writtenRefs.Clear(); + _multiReferenced.Clear(); + _nextId = 1; + } +} + +/// +/// Shared reference tracking logic for deserialization. +/// Resolves $id/$ref references during deserialization. +/// +public sealed class DeserializationReferenceTracker +{ + private Dictionary? _idToObject; + + /// + /// Registers an object with its $id. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterObject(string id, object obj) + { + _idToObject ??= new Dictionary(8, StringComparer.Ordinal); + _idToObject[id] = obj; + } + + /// + /// Tries to get a referenced object by its $id. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetReferencedObject(string id, out object? obj) + { + if (_idToObject != null) + return _idToObject.TryGetValue(id, out obj); + obj = null; + return false; + } + + /// + /// Clears all tracking data for reuse. + /// + public void Clear() + { + _idToObject?.Clear(); + } +} + +/// +/// Reference equality comparer for object identity comparison. +/// Used for reference tracking dictionaries. +/// +public sealed class ReferenceEqualityComparer : IEqualityComparer +{ + public static readonly ReferenceEqualityComparer Instance = new(); + + private ReferenceEqualityComparer() { } + + public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); +} diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs new file mode 100644 index 0000000..b525169 --- /dev/null +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers; + +/// +/// Base class for type metadata used by all serializers. +/// Contains common functionality for type analysis and constructor compilation. +/// +public abstract class TypeMetadataBase +{ + /// + /// Compiled parameterless constructor for the type. + /// Null if the type is abstract or has no parameterless constructor. + /// + public Func? CompiledConstructor { get; } + + protected TypeMetadataBase(Type type) + { + CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type); + } + + /// + /// Gets the properties that should be serialized for a type. + /// + /// The type to analyze. + /// Whether the property must be readable. + /// Whether the property must be writable. + /// Enumerable of properties that meet the criteria. + protected static IEnumerable GetSerializableProperties( + Type type, + bool requiresRead = true, + bool requiresWrite = false) + { + return type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => (!requiresRead || p.CanRead) && + (!requiresWrite || p.CanWrite) && + p.GetIndexParameters().Length == 0 && + !HasJsonIgnoreAttribute(p)); + } +} diff --git a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs index a3e9e13..967c9fc 100644 --- a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs +++ b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs @@ -8,6 +8,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using AyCode.Core.Serializers.Jsons; +using AyCode.Core.Compression; namespace AyCode.Services.Server.SignalRs { @@ -338,8 +339,8 @@ namespace AyCode.Services.Server.SignalRs } else { - // JSON mode - var json = System.Text.Encoding.UTF8.GetString(responseData); + // JSON mode - decompress GZip first + var json = GzipHelper.DecompressToString(responseData); if (InnerList is IAcObservableCollection observable) { observable.PopulateFromJson(json); @@ -357,7 +358,7 @@ namespace AyCode.Services.Server.SignalRs if (serializerType == AcSerializerType.Binary) fromSource = responseData.BinaryTo(); else - fromSource = System.Text.Encoding.UTF8.GetString(responseData).JsonTo(); + fromSource = GzipHelper.DecompressToString(responseData).JsonTo(); if (fromSource != null) { diff --git a/TestChainRunner/TestChainRunner.csproj b/TestChainRunner/TestChainRunner.csproj deleted file mode 100644 index 631ceeb..0000000 --- a/TestChainRunner/TestChainRunner.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - Exe - net9.0 - enable - - - - - - -