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