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.
This commit is contained in:
parent
9f8c027366
commit
0552268ac1
|
|
@ -0,0 +1,233 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for Chain API reference preservation with IId objects.
|
||||
/// This is the critical feature for DevExpress DXGrid GridCustomDataSource scenario.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinarySerializerChainReferenceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void ChainPopulate_IIdObjects_PreservesReferences()
|
||||
{
|
||||
// Setup: Create internal cache with 5 categories
|
||||
var internalCache = new List<SharedCategory>
|
||||
{
|
||||
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<SharedCategory>
|
||||
{
|
||||
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<SharedCategory>();
|
||||
|
||||
// CRITICAL: Use Chain API to parse once, populate both cache and grid
|
||||
using var chain = binary.BinaryToChain<List<SharedCategory>>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test JSON Chain API reference preservation.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void JsonChainPopulate_IIdObjects_PreservesReferences()
|
||||
{
|
||||
// Setup: Create internal cache
|
||||
var internalCache = new List<SharedCategory>
|
||||
{
|
||||
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<SharedCategory>
|
||||
{
|
||||
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<SharedCategory>();
|
||||
|
||||
// Use JSON Chain API
|
||||
using var chain = json.JsonToChain<List<SharedCategory>>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test with Guid-based IId implementation.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void ChainPopulate_GuidIId_PreservesReferences()
|
||||
{
|
||||
var cache = new List<TestGuidOrder>
|
||||
{
|
||||
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<TestGuidOrder>
|
||||
{
|
||||
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<TestGuidOrder>();
|
||||
|
||||
using var chain = binary.BinaryToChain<List<TestGuidOrder>>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test multiple chain operations with different subsets.
|
||||
/// </summary>
|
||||
[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<SharedCategory>();
|
||||
var gridPage2 = new List<SharedCategory>();
|
||||
var gridPage3 = new List<SharedCategory>();
|
||||
|
||||
using var chain = binary.BinaryToChain<List<SharedCategory>>();
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple debug test to verify chain reference tracking works.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void ChainPopulate_SimpleCase_Works()
|
||||
{
|
||||
var list1 = new List<SharedCategory>();
|
||||
var list2 = new List<SharedCategory>();
|
||||
|
||||
var serverData = new List<SharedCategory>
|
||||
{
|
||||
new() { Id = 1, Name = "Cat1", SortOrder = 10 }
|
||||
};
|
||||
|
||||
var binary = serverData.ToBinary();
|
||||
|
||||
using var chain = binary.BinaryToChain<List<SharedCategory>>();
|
||||
|
||||
// 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!");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for Binary Chain API (CreateDeserializeChain and CreatePopulateChain).
|
||||
/// </summary>
|
||||
[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<TestSimpleClass>();
|
||||
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<TestSimpleClass>();
|
||||
var result1 = chain.Value;
|
||||
var result2 = chain.ThenDeserialize<TestSimpleClass>();
|
||||
var result3 = chain.ThenDeserialize<TestSimpleClass>();
|
||||
|
||||
// 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<TestNestedClass>();
|
||||
var result1 = chain.Value;
|
||||
var result2 = chain.ThenDeserialize<TestNestedClass>();
|
||||
|
||||
// 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<string> { "Apple", "Banana", "Cherry" }
|
||||
};
|
||||
var binary = original.ToBinary();
|
||||
|
||||
// Act
|
||||
using var chain = binary.BinaryToChain<TestClassWithList>();
|
||||
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<string> { "New1", "New2", "New3" }
|
||||
};
|
||||
var binary = original.ToBinary();
|
||||
var target = new TestClassWithList
|
||||
{
|
||||
Id = 1,
|
||||
Items = new List<string> { "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<byte>();
|
||||
|
||||
// Act
|
||||
using var chain = binary.BinaryToChain<TestSimpleClass>();
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(chain.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PopulateChain_EmptyBinary_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var binary = Array.Empty<byte>();
|
||||
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<TestSimpleClass>();
|
||||
var value = chain.Value;
|
||||
|
||||
// Act
|
||||
chain.Dispose();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(value); // Value from before dispose should still exist
|
||||
_ = Assert.ThrowsExactly<ObjectDisposedException>(() => chain.ThenDeserialize<TestSimpleClass>());
|
||||
}
|
||||
|
||||
[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<ObjectDisposedException>(() => 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<TestSimpleClass>(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<byte> memory = binary;
|
||||
|
||||
// Act
|
||||
using var chain = memory.BinaryToChain<TestSimpleClass>();
|
||||
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<byte> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SharedCategory>();
|
||||
var list2 = new List<SharedCategory>();
|
||||
|
||||
var serverData = new List<SharedCategory>
|
||||
{
|
||||
new() { Id = 1, Name = "Cat1", SortOrder = 10 }
|
||||
};
|
||||
|
||||
var binary = serverData.ToBinary();
|
||||
|
||||
using var chain = binary.BinaryToChain<List<SharedCategory>>();
|
||||
|
||||
// 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!");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<object, string> GetObjectToId() =>
|
||||
_objectToId ??= new Dictionary<object, string>(_estimatedObjectCount, ReferenceEqualityComparer.Instance);
|
||||
_objectToId ??= new Dictionary<object, string>(_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.
|
||||
/// </summary>
|
||||
public static IPopulateChain JsonToChain(this string json, object target)
|
||||
public static IDeserializeChain<object> JsonToChain(this string json, object target)
|
||||
{
|
||||
json = UnwrapJsonString(json);
|
||||
return AcJsonDeserializer.CreatePopulateChain(json, target);
|
||||
var chain = AcJsonDeserializer.CreateDeserializeChain<object>(json);
|
||||
chain.ThenPopulate(target);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a populate chain with options.
|
||||
/// </summary>
|
||||
public static IPopulateChain JsonToChain(this string json, object target, AcJsonSerializerOptions options)
|
||||
public static IDeserializeChain<object> JsonToChain(this string json, object target, AcJsonSerializerOptions options)
|
||||
{
|
||||
json = UnwrapJsonString(json);
|
||||
return AcJsonDeserializer.CreatePopulateChain(json, target, options);
|
||||
var chain = AcJsonDeserializer.CreateDeserializeChain<object>(json, options);
|
||||
chain.ThenPopulate(target);
|
||||
return chain;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -650,6 +655,74 @@ public static class SerializeObjectExtensions
|
|||
public static void BinaryToMerge<T>(this ReadOnlyMemory<byte> data, T target) where T : class
|
||||
=> AcBinaryDeserializer.PopulateMerge(data.Span, target);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data)
|
||||
=> AcBinaryDeserializer.CreateDeserializeChain<T>(data.AsSpan());
|
||||
|
||||
/// <summary>
|
||||
/// Create a deserialize chain with options.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data, AcBinarySerializerOptions options)
|
||||
=> AcBinaryDeserializer.CreateDeserializeChain<T>(data.AsSpan(), options);
|
||||
|
||||
/// <summary>
|
||||
/// Create a deserialize chain from ReadOnlyMemory.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this ReadOnlyMemory<byte> data)
|
||||
=> AcBinaryDeserializer.CreateDeserializeChain<T>(data.Span);
|
||||
|
||||
/// <summary>
|
||||
/// Create a deserialize chain from ReadOnlyMemory with options.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this ReadOnlyMemory<byte> data, AcBinarySerializerOptions options)
|
||||
=> AcBinaryDeserializer.CreateDeserializeChain<T>(data.Span, options);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data, T target) where T : class
|
||||
{
|
||||
var chain = AcBinaryDeserializer.CreateDeserializeChain<T>(data.AsSpan());
|
||||
chain.ThenPopulate(target);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a populate chain with options.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this byte[] data, T target, AcBinarySerializerOptions options) where T : class
|
||||
{
|
||||
var chain = AcBinaryDeserializer.CreateDeserializeChain<T>(data.AsSpan(), options);
|
||||
chain.ThenPopulate(target);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a populate chain from ReadOnlyMemory.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this ReadOnlyMemory<byte> data, T target) where T : class
|
||||
{
|
||||
var chain = AcBinaryDeserializer.CreateDeserializeChain<T>(data.Span);
|
||||
chain.ThenPopulate(target);
|
||||
return chain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a populate chain from ReadOnlyMemory with options.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> BinaryToChain<T>(this ReadOnlyMemory<byte> data, T target, AcBinarySerializerOptions options) where T : class
|
||||
{
|
||||
var chain = AcBinaryDeserializer.CreateDeserializeChain<T>(data.Span, options);
|
||||
chain.ThenPopulate(target);
|
||||
return chain;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Clone and Copy (Binary-based, zero intermediate allocation)
|
||||
|
|
|
|||
|
|
@ -476,5 +476,120 @@ public static class AcSerializerCommon
|
|||
return LExpression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a typed setter delegate to avoid boxing for value types.
|
||||
/// </summary>
|
||||
public static Action<object, TProperty> CreateTypedSetter<TProperty>(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<Action<object, TProperty>>(assign, objParam, valueParam).Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enum setter that accepts int to avoid boxing.
|
||||
/// </summary>
|
||||
public static Action<object, int> 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<Action<object, int>>(assign, objParam, valueParam).Compile();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chain Reference Tracking
|
||||
|
||||
/// <summary>
|
||||
/// Tracks IId objects across chain deserializations to maintain reference identity.
|
||||
/// Used internally by IBinaryDeserializeChain.ThenPopulate to ensure same object references.
|
||||
/// </summary>
|
||||
public sealed class ChainReferenceTracker
|
||||
{
|
||||
private readonly Dictionary<(Type, object), object> _idToObject = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers an IId object for later retrieval.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a previously registered object by type and ID.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a previously registered object by exact type and ID.
|
||||
/// </summary>
|
||||
public bool TryGetObject(Type type, object id, out object? obj)
|
||||
{
|
||||
var key = (type, NormalizeId(id));
|
||||
return _idToObject.TryGetValue(key, out obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all tracked references.
|
||||
/// </summary>
|
||||
public void Clear() => _idToObject.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the ID value for consistent dictionary lookups.
|
||||
/// Handles boxed value type comparisons.
|
||||
/// </summary>
|
||||
[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
|
||||
}
|
||||
|
|
@ -29,6 +29,17 @@ public static partial class AcBinaryDeserializer
|
|||
public int Position => _position;
|
||||
public byte MinStringInternLength => _minStringInternLength;
|
||||
|
||||
/// <summary>
|
||||
/// Chain reference tracker for maintaining object identity across chain operations.
|
||||
/// Only set when in chain mode (CreateDeserializeChain).
|
||||
/// </summary>
|
||||
public AcSerializerCommon.ChainReferenceTracker? ChainTracker { readonly get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if in chain mode (ChainTracker is set).
|
||||
/// </summary>
|
||||
public readonly bool IsChainMode => ChainTracker != null;
|
||||
|
||||
public BinaryDeserializationContext(ReadOnlySpan<byte> 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;
|
||||
|
|
|
|||
|
|
@ -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<string, BinaryPropertySetterInfo> _properties;
|
||||
|
||||
public BinaryPropertySetterInfo[] PropertiesArray { get; }
|
||||
public Func<object>? CompiledConstructor { get; }
|
||||
|
||||
public BinaryDeserializeTypeMetadata(Type type)
|
||||
/// <summary>
|
||||
/// Whether this type implements IId interface.
|
||||
/// </summary>
|
||||
public bool IsIId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiled getter for the Id property (if IsIId is true).
|
||||
/// </summary>
|
||||
public Func<object, object?>? IdGetter { get; }
|
||||
|
||||
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<string, BinaryPropertySetterInfo>.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,92 +52,101 @@ public static partial class AcBinaryDeserializer
|
|||
=> _properties.TryGetValue(name, out propertyInfo);
|
||||
}
|
||||
|
||||
internal sealed class BinaryPropertySetterInfo
|
||||
/// <summary>
|
||||
/// Binary deserialization property setter with typed setters for performance.
|
||||
/// </summary>
|
||||
internal sealed class BinaryPropertySetterInfo : BinaryPropertySetterBase
|
||||
{
|
||||
private static readonly Func<object, object?> NullGetter = static _ => null;
|
||||
private static readonly Action<object, object?> NullSetter = static (_, _) => { };
|
||||
|
||||
private readonly Func<object, object?> _getter;
|
||||
private readonly Action<object, object?> _setter;
|
||||
// Fields for manual constructor case
|
||||
private readonly Func<object, object?>? _manualGetter;
|
||||
private readonly Action<object, object?>? _manualSetter;
|
||||
private readonly string? _manualName;
|
||||
private readonly Type? _manualPropertyType;
|
||||
private readonly bool _isManualConstruction;
|
||||
private readonly Type? _manualElementType;
|
||||
private readonly Type? _manualElementIdType;
|
||||
private readonly Func<object, object?>? _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<object, object?>? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for manually created property info (used in merge operations).
|
||||
/// </summary>
|
||||
public BinaryPropertySetterInfo(
|
||||
string name,
|
||||
Type propertyType,
|
||||
bool isCollection,
|
||||
Type? elementType,
|
||||
Type? elementIdType,
|
||||
Func<object, object?>? elementIdGetter,
|
||||
Func<object, object?>? getter = null,
|
||||
Action<object, object?>? setter = null)
|
||||
Func<object, object?>? 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<object, object?>? 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)
|
||||
{
|
||||
ThrowDetailedCastException(target, value, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowDetailedCastException(object target, object? value, 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",
|
||||
|
|
@ -144,26 +165,4 @@ public static partial class AcBinaryDeserializer
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,8 +355,360 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Chain API
|
||||
|
||||
/// <summary>
|
||||
/// Create a deserialize chain that parses binary data once and allows multiple deserializations.
|
||||
/// Maintains reference identity for IId objects across chain operations.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> CreateDeserializeChain<T>(ReadOnlySpan<byte> data)
|
||||
=> CreateDeserializeChain<T>(data, AcBinarySerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a deserialize chain with options.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> CreateDeserializeChain<T>(ReadOnlySpan<byte> data, AcBinarySerializerOptions options)
|
||||
{
|
||||
if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null))
|
||||
return EmptyDeserializeChain<T>.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<T>(dataArray, options, chainTracker, (T?)result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chain Implementations
|
||||
|
||||
/// <summary>
|
||||
/// Binary implementation of deserialize chain.
|
||||
/// Maintains reference identity for IId objects across chain operations.
|
||||
/// </summary>
|
||||
private sealed class BinaryDeserializeChain<T> : IDeserializeChain<T>
|
||||
{
|
||||
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<TResult>()
|
||||
{
|
||||
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<T> 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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized value reader using FrozenDictionary dispatch table.
|
||||
/// </summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1189,9 +1571,43 @@ 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<object, object?>? 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
|
||||
/// <summary>
|
||||
/// Cached type conversion info. Using readonly struct to avoid heap allocation.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly byte[] NameUtf8;
|
||||
public readonly Type PropertyType;
|
||||
public readonly TypeCode TypeCode;
|
||||
public readonly Type DeclaringType;
|
||||
|
||||
private readonly Func<object, object?> _objectGetter;
|
||||
private readonly Delegate? _typedGetter;
|
||||
private readonly PropertyAccessorType _accessorType;
|
||||
|
||||
/// <summary>
|
||||
/// Cached property name index for metadata mode. Set by context during registration.
|
||||
/// -1 means not yet cached.
|
||||
/// Binary serialization property accessor with typed getters.
|
||||
/// </summary>
|
||||
internal int CachedPropertyNameIndex = -1;
|
||||
|
||||
public BinaryPropertyAccessor(PropertyInfo prop)
|
||||
internal sealed class BinaryPropertyAccessor : BinaryPropertyAccessorBase
|
||||
{
|
||||
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<object, object?> ObjectGetter => _objectGetter;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object? GetValue(object obj) => _objectGetter(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_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<Guid>(declaringType, prop), PropertyAccessorType.Guid);
|
||||
}
|
||||
|
||||
var typeCode = Type.GetTypeCode(propType);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => (AcSerializerCommon.CreateTypedGetter<int>(declaringType, prop), PropertyAccessorType.Int32),
|
||||
TypeCode.Int64 => (AcSerializerCommon.CreateTypedGetter<long>(declaringType, prop), PropertyAccessorType.Int64),
|
||||
TypeCode.Boolean => (AcSerializerCommon.CreateTypedGetter<bool>(declaringType, prop), PropertyAccessorType.Boolean),
|
||||
TypeCode.Double => (AcSerializerCommon.CreateTypedGetter<double>(declaringType, prop), PropertyAccessorType.Double),
|
||||
TypeCode.Single => (AcSerializerCommon.CreateTypedGetter<float>(declaringType, prop), PropertyAccessorType.Single),
|
||||
TypeCode.Decimal => (AcSerializerCommon.CreateTypedGetter<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
|
||||
TypeCode.DateTime => (AcSerializerCommon.CreateTypedGetter<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
|
||||
TypeCode.Byte => (AcSerializerCommon.CreateTypedGetter<byte>(declaringType, prop), PropertyAccessorType.Byte),
|
||||
TypeCode.Int16 => (AcSerializerCommon.CreateTypedGetter<short>(declaringType, prop), PropertyAccessorType.Int16),
|
||||
TypeCode.UInt16 => (AcSerializerCommon.CreateTypedGetter<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
|
||||
TypeCode.UInt32 => (AcSerializerCommon.CreateTypedGetter<uint>(declaringType, prop), PropertyAccessorType.UInt32),
|
||||
TypeCode.UInt64 => (AcSerializerCommon.CreateTypedGetter<ulong>(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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Binary-specific property accessor base class.
|
||||
/// Adds typed getters to avoid boxing during serialization.
|
||||
/// </summary>
|
||||
public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Cached property name index for metadata mode. Set by context during registration.
|
||||
/// -1 means not yet cached.
|
||||
/// </summary>
|
||||
internal int CachedPropertyNameIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The accessor type for fast typed getter dispatch.
|
||||
/// </summary>
|
||||
public PropertyAccessorType AccessorType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Typed getter delegate (type depends on AccessorType).
|
||||
/// </summary>
|
||||
protected readonly Delegate? _typedGetter;
|
||||
|
||||
/// <summary>
|
||||
/// Object getter for property filter context.
|
||||
/// </summary>
|
||||
public Func<object, object?> 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<Guid>(declaringType, prop), PropertyAccessorType.Guid);
|
||||
}
|
||||
|
||||
var typeCode = Type.GetTypeCode(propType);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => (AcSerializerCommon.CreateTypedGetter<int>(declaringType, prop), PropertyAccessorType.Int32),
|
||||
TypeCode.Int64 => (AcSerializerCommon.CreateTypedGetter<long>(declaringType, prop), PropertyAccessorType.Int64),
|
||||
TypeCode.Boolean => (AcSerializerCommon.CreateTypedGetter<bool>(declaringType, prop), PropertyAccessorType.Boolean),
|
||||
TypeCode.Double => (AcSerializerCommon.CreateTypedGetter<double>(declaringType, prop), PropertyAccessorType.Double),
|
||||
TypeCode.Single => (AcSerializerCommon.CreateTypedGetter<float>(declaringType, prop), PropertyAccessorType.Single),
|
||||
TypeCode.Decimal => (AcSerializerCommon.CreateTypedGetter<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
|
||||
TypeCode.DateTime => (AcSerializerCommon.CreateTypedGetter<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
|
||||
TypeCode.Byte => (AcSerializerCommon.CreateTypedGetter<byte>(declaringType, prop), PropertyAccessorType.Byte),
|
||||
TypeCode.Int16 => (AcSerializerCommon.CreateTypedGetter<short>(declaringType, prop), PropertyAccessorType.Int16),
|
||||
TypeCode.UInt16 => (AcSerializerCommon.CreateTypedGetter<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
|
||||
TypeCode.UInt32 => (AcSerializerCommon.CreateTypedGetter<uint>(declaringType, prop), PropertyAccessorType.UInt32),
|
||||
TypeCode.UInt64 => (AcSerializerCommon.CreateTypedGetter<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
|
||||
_ => (null, PropertyAccessorType.Object)
|
||||
};
|
||||
}
|
||||
|
||||
#region Typed Getters
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum for typed property accessor dispatch.
|
||||
/// </summary>
|
||||
public enum PropertyAccessorType : byte
|
||||
{
|
||||
Object = 0,
|
||||
Int32,
|
||||
Int64,
|
||||
Boolean,
|
||||
Double,
|
||||
Single,
|
||||
Decimal,
|
||||
DateTime,
|
||||
Byte,
|
||||
Int16,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
Guid,
|
||||
Enum
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Binary-specific property setter base class.
|
||||
/// Extends PropertySetterBase with binary-specific functionality and typed setters.
|
||||
/// </summary>
|
||||
public abstract class BinaryPropertySetterBase : PropertySetterBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this property is a complex type (not primitive, string, enum, or common value types).
|
||||
/// </summary>
|
||||
public bool IsComplexType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this property is a collection type.
|
||||
/// </summary>
|
||||
public bool IsCollection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The setter type for fast typed setter dispatch.
|
||||
/// </summary>
|
||||
public PropertyAccessorType SetterType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Typed setter delegate (type depends on SetterType).
|
||||
/// </summary>
|
||||
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<Guid>(declaringType, prop), PropertyAccessorType.Guid);
|
||||
}
|
||||
|
||||
var typeCode = Type.GetTypeCode(propType);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => (AcSerializerCommon.CreateTypedSetter<int>(declaringType, prop), PropertyAccessorType.Int32),
|
||||
TypeCode.Int64 => (AcSerializerCommon.CreateTypedSetter<long>(declaringType, prop), PropertyAccessorType.Int64),
|
||||
TypeCode.Boolean => (AcSerializerCommon.CreateTypedSetter<bool>(declaringType, prop), PropertyAccessorType.Boolean),
|
||||
TypeCode.Double => (AcSerializerCommon.CreateTypedSetter<double>(declaringType, prop), PropertyAccessorType.Double),
|
||||
TypeCode.Single => (AcSerializerCommon.CreateTypedSetter<float>(declaringType, prop), PropertyAccessorType.Single),
|
||||
TypeCode.Decimal => (AcSerializerCommon.CreateTypedSetter<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
|
||||
TypeCode.DateTime => (AcSerializerCommon.CreateTypedSetter<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
|
||||
TypeCode.Byte => (AcSerializerCommon.CreateTypedSetter<byte>(declaringType, prop), PropertyAccessorType.Byte),
|
||||
TypeCode.Int16 => (AcSerializerCommon.CreateTypedSetter<short>(declaringType, prop), PropertyAccessorType.Int16),
|
||||
TypeCode.UInt16 => (AcSerializerCommon.CreateTypedSetter<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
|
||||
TypeCode.UInt32 => (AcSerializerCommon.CreateTypedSetter<uint>(declaringType, prop), PropertyAccessorType.UInt32),
|
||||
TypeCode.UInt64 => (AcSerializerCommon.CreateTypedSetter<ulong>(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<object, int>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetInt64(object obj, long value) => ((Action<object, long>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetBoolean(object obj, bool value) => ((Action<object, bool>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetDouble(object obj, double value) => ((Action<object, double>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetSingle(object obj, float value) => ((Action<object, float>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetDecimal(object obj, decimal value) => ((Action<object, decimal>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetDateTime(object obj, DateTime value) => ((Action<object, DateTime>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetByte(object obj, byte value) => ((Action<object, byte>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetInt16(object obj, short value) => ((Action<object, short>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetUInt16(object obj, ushort value) => ((Action<object, ushort>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetUInt32(object obj, uint value) => ((Action<object, uint>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetUInt64(object obj, ulong value) => ((Action<object, ulong>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetGuid(object obj, Guid value) => ((Action<object, Guid>)_typedSetter!)(obj, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetEnumAsInt32(object obj, int value) => ((Action<object, int>)_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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IDeserializeChain<T> : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The first deserialized value.
|
||||
/// </summary>
|
||||
T? Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize to another type from the same data.
|
||||
/// </summary>
|
||||
TResult? ThenDeserialize<TResult>();
|
||||
|
||||
/// <summary>
|
||||
/// Populate an existing object from the same data.
|
||||
/// Returns this chain for fluent API.
|
||||
/// </summary>
|
||||
IDeserializeChain<T> ThenPopulate(object target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empty deserialize chain implementation for null/empty data.
|
||||
/// </summary>
|
||||
public sealed class EmptyDeserializeChain<T> : IDeserializeChain<T>
|
||||
{
|
||||
public static readonly IDeserializeChain<T> Instance = new EmptyDeserializeChain<T>();
|
||||
|
||||
private EmptyDeserializeChain() { }
|
||||
|
||||
public T? Value => default;
|
||||
public TResult? ThenDeserialize<TResult>() => default;
|
||||
public IDeserializeChain<T> ThenPopulate(object target) => this;
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for merging IId collections during deserialization.
|
||||
/// Shared between JSON and Binary deserializers.
|
||||
/// </summary>
|
||||
public static class IIdCollectionMergeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a lookup dictionary from an existing IId collection.
|
||||
/// Maps Id values to their corresponding items.
|
||||
/// </summary>
|
||||
/// <param name="existingList">The existing collection to index.</param>
|
||||
/// <param name="idGetter">Function to extract Id from an item.</param>
|
||||
/// <param name="idType">The type of the Id property.</param>
|
||||
/// <returns>Dictionary mapping Id to item, or null if collection is empty.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Dictionary<object, object>? BuildIdLookup(
|
||||
IList existingList,
|
||||
Func<object, object?> idGetter,
|
||||
Type idType)
|
||||
{
|
||||
var count = existingList.Count;
|
||||
if (count == 0) return null;
|
||||
|
||||
var dict = new Dictionary<object, object>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes orphaned items from the collection that are not present in the source IDs.
|
||||
/// </summary>
|
||||
/// <param name="existingList">The collection to clean up.</param>
|
||||
/// <param name="existingById">Lookup dictionary of existing items.</param>
|
||||
/// <param name="sourceIds">Set of IDs that were seen in source data.</param>
|
||||
public static void RemoveOrphanedItems(
|
||||
IList existingList,
|
||||
Dictionary<object, object> existingById,
|
||||
HashSet<object> sourceIds)
|
||||
{
|
||||
var itemsToRemove = new List<object>();
|
||||
foreach (var kvp in existingById)
|
||||
{
|
||||
if (!sourceIds.Contains(kvp.Key))
|
||||
{
|
||||
itemsToRemove.Add(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in itemsToRemove)
|
||||
{
|
||||
existingList.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies properties from source object to target object using metadata.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPropertyInfo">Type of property info (varies by serializer).</typeparam>
|
||||
/// <param name="source">Source object to copy from.</param>
|
||||
/// <param name="target">Target object to copy to.</param>
|
||||
/// <param name="properties">Array of property accessors.</param>
|
||||
/// <param name="getter">Function to get property value.</param>
|
||||
/// <param name="setter">Action to set property value.</param>
|
||||
public static void CopyProperties<TPropertyInfo>(
|
||||
object source,
|
||||
object target,
|
||||
TPropertyInfo[] properties,
|
||||
Func<TPropertyInfo, object, object?> getter,
|
||||
Action<TPropertyInfo, object, object?> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,17 @@ public static partial class AcJsonDeserializer
|
|||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chain reference tracker for maintaining object identity across chain operations.
|
||||
/// Only set when in chain mode (CreateDeserializeChain).
|
||||
/// </summary>
|
||||
public AcSerializerCommon.ChainReferenceTracker? ChainTracker { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if in chain mode (ChainTracker is set).
|
||||
/// </summary>
|
||||
public bool IsChainMode => ChainTracker != null;
|
||||
|
||||
public DeserializationContext(in AcJsonSerializerOptions options)
|
||||
{
|
||||
Reset(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)]
|
||||
|
|
|
|||
|
|
@ -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<string, PropertySetterInfo> PropertySettersFrozen { get; }
|
||||
public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects)
|
||||
public Func<object>? 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<PropertyInfo>(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<string, PropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
|
||||
var propsArray = new PropertySetterInfo[propsList.Count];
|
||||
var propertySetters = new Dictionary<string, PropertySetterInfo>(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
|
||||
/// <summary>
|
||||
/// JSON deserialization property setter.
|
||||
/// </summary>
|
||||
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<object, object?>? 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<object, object?> _setter;
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
||||
// Typed setters for common primitive types (avoid boxing)
|
||||
internal readonly Action<object, int>? _setInt32;
|
||||
internal readonly Action<object, long>? _setInt64;
|
||||
internal readonly Action<object, double>? _setDouble;
|
||||
internal readonly Action<object, bool>? _setBool;
|
||||
internal readonly Action<object, decimal>? _setDecimal;
|
||||
internal readonly Action<object, float>? _setSingle;
|
||||
internal readonly Action<object, DateTime>? _setDateTime;
|
||||
internal readonly Action<object, Guid>? _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)
|
||||
: 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<int>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, LongType))
|
||||
_setInt64 = CreateTypedSetter<long>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, DoubleType))
|
||||
_setDouble = CreateTypedSetter<double>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, BoolType))
|
||||
_setBool = CreateTypedSetter<bool>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, DecimalType))
|
||||
_setDecimal = CreateTypedSetter<decimal>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, FloatType))
|
||||
_setSingle = CreateTypedSetter<float>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, DateTimeType))
|
||||
_setDateTime = CreateTypedSetter<DateTime>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, GuidType))
|
||||
_setGuid = CreateTypedSetter<Guid>(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<object, T> CreateTypedSetter<T>(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<Action<object, T>>(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);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -58,9 +58,49 @@ 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies properties from source to target using JSON metadata.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
var propsDict = metadata.PropertySettersFrozen;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -488,7 +488,7 @@ public static partial class AcJsonDeserializer
|
|||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static IDeserializeChain<T> CreateDeserializeChain<T>(string json)
|
||||
=> CreateDeserializeChain<T>(json, AcJsonSerializerOptions.Default);
|
||||
|
|
@ -499,57 +499,21 @@ public static partial class AcJsonDeserializer
|
|||
public static IDeserializeChain<T> CreateDeserializeChain<T>(string json, in AcJsonSerializerOptions options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json) || json == "null")
|
||||
return DeserializeChain<T>.Empty;
|
||||
return EmptyDeserializeChain<T>.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<T>(doc, context, options, (T?)result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DeserializationContextPool.Return(context);
|
||||
doc.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a populate chain that parses JSON once and allows populating multiple objects.
|
||||
/// Efficient for populating multiple objects from the same JSON source.
|
||||
/// </summary>
|
||||
public static IPopulateChain CreatePopulateChain(string json, object target)
|
||||
=> CreatePopulateChain(json, target, AcJsonSerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a populate chain with options.
|
||||
/// </summary>
|
||||
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<T>(doc, context, chainTracker, (T?)result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -586,38 +550,39 @@ public static partial class AcJsonDeserializer
|
|||
#region Chain Implementations (Nested Classes)
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of deserialize chain.
|
||||
/// JSON implementation of deserialize chain.
|
||||
/// Maintains reference identity for IId objects across chain operations.
|
||||
/// </summary>
|
||||
private sealed class DeserializeChain<T> : IDeserializeChain<T>
|
||||
private sealed class JsonDeserializeChain<T> : IDeserializeChain<T>
|
||||
{
|
||||
public static readonly IDeserializeChain<T> 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<TOther>()
|
||||
public TResult? ThenDeserialize<TResult>()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (_document == null || _context == null)
|
||||
throw new ObjectDisposedException(nameof(DeserializeChain<T>));
|
||||
throw new ObjectDisposedException(nameof(JsonDeserializeChain<T>));
|
||||
|
||||
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<T>
|
||||
{
|
||||
public T? Value => default;
|
||||
public TOther? ThenDeserialize<TOther>() => default;
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of populate chain.
|
||||
/// </summary>
|
||||
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<T> ThenPopulate(object target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
ThrowIfDisposed();
|
||||
if (_document == null || _context == null)
|
||||
throw new ObjectDisposedException(nameof(PopulateChain));
|
||||
throw new ObjectDisposedException(nameof(JsonDeserializeChain<T>));
|
||||
|
||||
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?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EmptyPopulateChain : IPopulateChain
|
||||
{
|
||||
public IPopulateChain ThenPopulate(object target) => this;
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Chain Public Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// Represents a deserialize chain that allows multiple deserializations from the same parsed JSON.
|
||||
/// Implements IDisposable - call Dispose() when done or use 'using' statement.
|
||||
/// </summary>
|
||||
public interface IDeserializeChain<T> : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The first deserialized value.
|
||||
/// </summary>
|
||||
T? Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize to another type from the same JSON.
|
||||
/// </summary>
|
||||
TOther? ThenDeserialize<TOther>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a populate chain that allows populating multiple objects from the same parsed JSON.
|
||||
/// Implements IDisposable - call Dispose() when done or use 'using' statement.
|
||||
/// </summary>
|
||||
public interface IPopulateChain : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Populate another object from the same JSON.
|
||||
/// </summary>
|
||||
IPopulateChain ThenPopulate(object target);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -69,9 +69,9 @@ public static partial class AcJsonSerializer
|
|||
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, string>(32, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, Serializers.ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, string>(32, Serializers.ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(32, Serializers.ReferenceEqualityComparer.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,13 +130,3 @@ public static partial class AcJsonSerializer
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference equality comparer for object identity comparison.
|
||||
/// </summary>
|
||||
internal sealed class ReferenceEqualityComparer : IEqualityComparer<object>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// <summary>
|
||||
/// JSON serialization property accessor.
|
||||
/// </summary>
|
||||
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<object, object?> _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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AyCode.Core.Serializers.Jsons;
|
||||
|
||||
/// <summary>
|
||||
/// JSON-specific property accessor base class.
|
||||
/// Adds JSON-specific encoding (JsonEncodedText for Utf8JsonWriter).
|
||||
/// </summary>
|
||||
public abstract class JsonPropertyAccessorBase : PropertyAccessorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Pre-encoded property name for Utf8JsonWriter (STJ optimization).
|
||||
/// </summary>
|
||||
public JsonEncodedText JsonNameEncoded { get; }
|
||||
|
||||
protected JsonPropertyAccessorBase(PropertyInfo prop, Type declaringType)
|
||||
: base(prop, declaringType)
|
||||
{
|
||||
JsonNameEncoded = JsonEncodedText.Encode(prop.Name);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// JSON-specific property setter base class.
|
||||
/// Adds typed setters for common primitives to avoid boxing during deserialization.
|
||||
/// </summary>
|
||||
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<object, int>? _setInt32;
|
||||
internal readonly Action<object, long>? _setInt64;
|
||||
internal readonly Action<object, double>? _setDouble;
|
||||
internal readonly Action<object, bool>? _setBool;
|
||||
internal readonly Action<object, decimal>? _setDecimal;
|
||||
internal readonly Action<object, float>? _setSingle;
|
||||
internal readonly Action<object, DateTime>? _setDateTime;
|
||||
internal readonly Action<object, Guid>? _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<int>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, LongType))
|
||||
_setInt64 = CreateTypedSetter<long>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, DoubleType))
|
||||
_setDouble = CreateTypedSetter<double>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, BoolType))
|
||||
_setBool = CreateTypedSetter<bool>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, DecimalType))
|
||||
_setDecimal = CreateTypedSetter<decimal>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, FloatType))
|
||||
_setSingle = CreateTypedSetter<float>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, DateTimeType))
|
||||
_setDateTime = CreateTypedSetter<DateTime>(declaringType, prop);
|
||||
else if (ReferenceEquals(PropertyType, GuidType))
|
||||
_setGuid = CreateTypedSetter<Guid>(declaringType, prop);
|
||||
}
|
||||
}
|
||||
|
||||
private static Action<object, T> CreateTypedSetter<T>(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<Action<object, T>>(assign, objParam, valueParam).Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set a boolean value without boxing.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set an int32 value without boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrySetInt32(object target, int value)
|
||||
{
|
||||
if (_setInt32 != null)
|
||||
{
|
||||
_setInt32(target, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set an int64 value without boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrySetInt64(object target, long value)
|
||||
{
|
||||
if (_setInt64 != null)
|
||||
{
|
||||
_setInt64(target, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set a double value without boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrySetDouble(object target, double value)
|
||||
{
|
||||
if (_setDouble != null)
|
||||
{
|
||||
_setDouble(target, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set a decimal value without boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrySetDecimal(object target, decimal value)
|
||||
{
|
||||
if (_setDecimal != null)
|
||||
{
|
||||
_setDecimal(target, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set a float value without boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrySetSingle(object target, float value)
|
||||
{
|
||||
if (_setSingle != null)
|
||||
{
|
||||
_setSingle(target, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set a DateTime value without boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrySetDateTime(object target, DateTime value)
|
||||
{
|
||||
if (_setDateTime != null)
|
||||
{
|
||||
_setDateTime(target, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to set a Guid value without boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrySetGuid(object target, Guid value)
|
||||
{
|
||||
if (_setGuid != null)
|
||||
{
|
||||
_setGuid(target, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for property accessors used by all serializers.
|
||||
/// Contains common property metadata and getter functionality.
|
||||
/// </summary>
|
||||
public abstract class PropertyAccessorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Property name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-encoded UTF8 bytes of property name for fast matching.
|
||||
/// </summary>
|
||||
public byte[] NameUtf8 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The property type (may be nullable).
|
||||
/// </summary>
|
||||
public Type PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The underlying type (unwrapped from Nullable if applicable).
|
||||
/// </summary>
|
||||
public Type UnderlyingType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached TypeCode for fast primitive type dispatch.
|
||||
/// </summary>
|
||||
public TypeCode PropertyTypeCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the property type is nullable.
|
||||
/// </summary>
|
||||
public bool IsNullable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The declaring type of this property.
|
||||
/// </summary>
|
||||
public Type DeclaringType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiled getter delegate for reading property values.
|
||||
/// </summary>
|
||||
protected readonly Func<object, object?> _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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property value from the target object.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object? GetValue(object obj) => _getter(obj);
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for property accessors that also support setting values.
|
||||
/// Used by deserializers. Extends PropertyAccessorBase with setter and IId collection support.
|
||||
/// </summary>
|
||||
public abstract class PropertySetterBase : PropertyAccessorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiled setter delegate for writing property values.
|
||||
/// </summary>
|
||||
protected readonly Action<object, object?> _setter;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this property is a collection of IId elements.
|
||||
/// </summary>
|
||||
public bool IsIIdCollection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Element type if this property is a collection, null otherwise.
|
||||
/// </summary>
|
||||
public Type? ElementType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Id type of collection elements (if IsIIdCollection is true).
|
||||
/// </summary>
|
||||
public Type? ElementIdType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiled getter for the Id property of collection elements.
|
||||
/// </summary>
|
||||
public Func<object, object?>? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the property value on the target object.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void SetValue(object target, object? value) => _setter(target, value);
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// Shared reference tracking logic for serialization.
|
||||
/// Tracks object references to enable $id/$ref handling for circular references.
|
||||
/// </summary>
|
||||
public sealed class SerializationReferenceTracker
|
||||
{
|
||||
private readonly Dictionary<object, int> _scanOccurrences;
|
||||
private readonly Dictionary<object, string> _writtenRefs;
|
||||
private readonly HashSet<object> _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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an object should have an $id written and returns the id.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an object as written with its assigned id.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, string id) => _writtenRefs[obj] = id;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get an existing reference id for an object.
|
||||
/// If found, a $ref should be written instead of the full object.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out string refId)
|
||||
{
|
||||
return _writtenRefs.TryGetValue(obj, out refId!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all tracking data for reuse.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_scanOccurrences.Clear();
|
||||
_writtenRefs.Clear();
|
||||
_multiReferenced.Clear();
|
||||
_nextId = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared reference tracking logic for deserialization.
|
||||
/// Resolves $id/$ref references during deserialization.
|
||||
/// </summary>
|
||||
public sealed class DeserializationReferenceTracker
|
||||
{
|
||||
private Dictionary<string, object>? _idToObject;
|
||||
|
||||
/// <summary>
|
||||
/// Registers an object with its $id.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterObject(string id, object obj)
|
||||
{
|
||||
_idToObject ??= new Dictionary<string, object>(8, StringComparer.Ordinal);
|
||||
_idToObject[id] = obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a referenced object by its $id.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetReferencedObject(string id, out object? obj)
|
||||
{
|
||||
if (_idToObject != null)
|
||||
return _idToObject.TryGetValue(id, out obj);
|
||||
obj = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all tracking data for reuse.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_idToObject?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference equality comparer for object identity comparison.
|
||||
/// Used for reference tracking dictionaries.
|
||||
/// </summary>
|
||||
public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System.Reflection;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for type metadata used by all serializers.
|
||||
/// Contains common functionality for type analysis and constructor compilation.
|
||||
/// </summary>
|
||||
public abstract class TypeMetadataBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiled parameterless constructor for the type.
|
||||
/// Null if the type is abstract or has no parameterless constructor.
|
||||
/// </summary>
|
||||
public Func<object>? CompiledConstructor { get; }
|
||||
|
||||
protected TypeMetadataBase(Type type)
|
||||
{
|
||||
CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the properties that should be serialized for a type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to analyze.</param>
|
||||
/// <param name="requiresRead">Whether the property must be readable.</param>
|
||||
/// <param name="requiresWrite">Whether the property must be writable.</param>
|
||||
/// <returns>Enumerable of properties that meet the criteria.</returns>
|
||||
protected static IEnumerable<PropertyInfo> 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TIList>();
|
||||
else
|
||||
fromSource = System.Text.Encoding.UTF8.GetString(responseData).JsonTo<TIList>();
|
||||
fromSource = GzipHelper.DecompressToString(responseData).JsonTo<TIList>();
|
||||
|
||||
if (fromSource != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AyCode.Core\AyCode.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue