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:
Loretta 2025-12-29 22:41:28 +01:00
parent 9f8c027366
commit 0552268ac1
30 changed files with 2559 additions and 612 deletions

View File

@ -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&lt;Item&gt; for grid display, but we also have internal cache List&lt;Item&gt;.
/// 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!");
}
}

View File

@ -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);
}
}

View File

@ -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!");
}
}

View File

@ -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)

View File

@ -475,6 +475,121 @@ public static class AcSerializerCommon
var convertToInt = LExpression.Convert(propAccess, typeof(int));
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
}

View File

@ -28,6 +28,17 @@ public static partial class AcBinaryDeserializer
public bool IsAtEnd => _position >= _buffer.Length;
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;

View File

@ -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; }
/// <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)
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,130 +52,117 @@ 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)
{
var valueType = value?.GetType().FullName ?? "null";
var propType = PropertyType.FullName;
var targetTypeName = target.GetType().FullName;
var underlyingType = Nullable.GetUnderlyingType(PropertyType);
// Get actual value info for debugging
var valueInfo = value switch
{
null => "null",
int i => $"int:{i}",
long l => $"long:{l}",
DateTime dt => $"DateTime:{dt:O}",
string s => $"string:'{s}'",
_ => $"{value.GetType().Name}:{value}"
};
throw new InvalidCastException(
$"Cannot set property '{Name}' on type '{targetTypeName}' - " +
$"PropertyType: '{propType}', ValueType: '{valueType}', Value: {valueInfo}, " +
$"IsNullable: {underlyingType != null}, UnderlyingType: {underlyingType?.FullName ?? "N/A"}, " +
$"IsValueType: {PropertyType.IsValueType}, IsCollection: {IsCollection}, IsComplexType: {IsComplexType}",
ex);
ThrowDetailedCastException(target, value, ex);
}
}
private static bool IsCollectionTypeCheck(Type type)
private void ThrowDetailedCastException(object target, object? value, InvalidCastException ex)
{
if (ReferenceEquals(type, StringType)) return false;
if (type.IsArray) return true;
return typeof(IEnumerable).IsAssignableFrom(type);
}
private static bool IsComplex(Type type)
{
var actualType = Nullable.GetUnderlyingType(type) ?? type;
if (actualType.IsPrimitive) return false;
if (ReferenceEquals(actualType, StringType)) return false;
if (actualType.IsEnum) return false;
if (ReferenceEquals(actualType, GuidType)) return false;
if (ReferenceEquals(actualType, DateTimeType)) return false;
if (ReferenceEquals(actualType, DecimalType)) return false;
if (ReferenceEquals(actualType, TimeSpanType)) return false;
if (ReferenceEquals(actualType, DateTimeOffsetType)) return false;
return true;
var valueType = value?.GetType().FullName ?? "null";
var propType = PropertyType.FullName;
var targetTypeName = target.GetType().FullName;
var underlyingType = Nullable.GetUnderlyingType(PropertyType);
var valueInfo = value switch
{
null => "null",
int i => $"int:{i}",
long l => $"long:{l}",
DateTime dt => $"DateTime:{dt:O}",
string s => $"string:'{s}'",
_ => $"{value.GetType().Name}:{value}"
};
throw new InvalidCastException(
$"Cannot set property '{Name}' on type '{targetTypeName}' - " +
$"PropertyType: '{propType}', ValueType: '{valueType}', Value: {valueInfo}, " +
$"IsNullable: {underlyingType != null}, UnderlyingType: {underlyingType?.FullName ?? "N/A"}, " +
$"IsValueType: {PropertyType.IsValueType}, IsCollection: {IsCollection}, IsComplexType: {IsComplexType}",
ex);
}
}
}

View File

@ -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);
}
@ -1188,10 +1570,44 @@ public static partial class AcBinaryDeserializer
var count = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
// ChainMode: Get IId info for element type
var isIId = false;
Type? idType = null;
Func<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)
{

View File

@ -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;

View File

@ -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
/// <summary>
/// Binary serialization property accessor with typed getters.
/// </summary>
internal sealed class BinaryPropertyAccessor : BinaryPropertyAccessorBase
{
public readonly string Name;
public readonly byte[] NameUtf8;
public readonly Type PropertyType;
public readonly TypeCode TypeCode;
public readonly Type DeclaringType;
private readonly Func<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.
/// </summary>
internal int CachedPropertyNameIndex = -1;
public BinaryPropertyAccessor(PropertyInfo prop)
public BinaryPropertyAccessor(PropertyInfo prop, Type declaringType)
: base(prop, declaringType)
{
Name = prop.Name;
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
DeclaringType = prop.DeclaringType!;
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
TypeCode = Type.GetTypeCode(PropertyType);
(_typedGetter, _accessorType) = CreateTypedGetterForAccessor(DeclaringType, prop);
_objectGetter = AcSerializerCommon.CreateCompiledGetter(DeclaringType, prop);
}
public PropertyAccessorType AccessorType => _accessorType;
public Func<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
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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() { }
}

View File

@ -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);
}
}
}

View File

@ -53,6 +53,17 @@ public static partial class AcJsonDeserializer
public bool IsMergeMode { get; set; }
public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; private set; }
/// <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)
{
@ -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)]

View File

@ -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)
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

View File

@ -58,8 +58,48 @@ public static partial class AcJsonDeserializer
PopulateObjectInternal(element, instance, metadata, context, depth);
// ChainMode: Check if we already have an object with this Id in the tracker
if (context.IsChainMode)
{
var (isIId, idType) = GetIdInfo(targetType);
if (isIId && idType != null)
{
var idProp = targetType.GetProperty("Id");
if (idProp != null)
{
var id = idProp.GetValue(instance);
if (id != null && !IsDefaultValue(id, idType))
{
// Check if we already have this object
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
{
// Update existing object's properties and return it
CopyPropertiesJson(instance, existingObj!, metadata);
return existingObj;
}
// Register this new object
context.ChainTracker.TryRegisterIIdObject(instance);
}
}
}
}
return instance;
}
/// <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)
{
@ -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);
}

View File

@ -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()
};
}

View File

@ -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 = null;
}
}
private sealed class EmptyPopulateChain : IPopulateChain
{
public IPopulateChain ThenPopulate(object target) => this;
public void Dispose() { }
_document?.Dispose();
_document = null;
}
}
#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

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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)
{

View File

@ -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>