Refactor serialization/deserialization context base classes
Introduce SerializationContextBase and DeserializationContextBase to separate serialization and deserialization logic. Move tracking API from AcSerializerContextBase to SerializationContextBase, and add generic identity map methods to TypeMetadataWrapper for type-safe object tracking. Update all context classes to inherit from the new base classes. Comment out shared reference tracker and property mapping cache classes. Clean up unused usings and obsolete code. This improves code organization, performance, and maintainability.
This commit is contained in:
parent
7d133a4b24
commit
8f35f172f0
|
|
@ -1,10 +1,8 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Serializers.Expressions;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using LExpression = System.Linq.Expressions.Expression;
|
||||
|
|
@ -60,19 +58,14 @@ public static class AcSerializerCommon
|
|||
/// </summary>
|
||||
/// <typeparam name="TKey">Cache key type (usually Type)</typeparam>
|
||||
/// <typeparam name="TValue">Cached value type</typeparam>
|
||||
public sealed class ThreadLocalCache<TKey, TValue> where TKey : notnull
|
||||
public sealed class ThreadLocalCache<TKey, TValue>(Func<TKey, TValue> factory)
|
||||
where TKey : notnull
|
||||
{
|
||||
private readonly ConcurrentDictionary<TKey, TValue> _globalCache = new();
|
||||
private readonly Func<TKey, TValue> _factory;
|
||||
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<TKey, TValue>? t_localCache;
|
||||
|
||||
public ThreadLocalCache(Func<TKey, TValue> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value from cache, creating it if necessary.
|
||||
/// Uses ThreadLocal cache for hot path, falls back to ConcurrentDictionary.
|
||||
|
|
@ -94,7 +87,7 @@ public static class AcSerializerCommon
|
|||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private TValue GetSlow(TKey key)
|
||||
{
|
||||
var value = _globalCache.GetOrAdd(key, _factory);
|
||||
var value = _globalCache.GetOrAdd(key, factory);
|
||||
|
||||
// Populate ThreadLocal cache
|
||||
var localCache = t_localCache ??= new Dictionary<TKey, TValue>();
|
||||
|
|
@ -1033,109 +1026,104 @@ public static class AcSerializerCommon
|
|||
/// Used by cross-type deserialization (Deserialize<TSource, TDest>).
|
||||
/// Thread-safe and cached for performance.
|
||||
/// </summary>
|
||||
public sealed class PropertyMappingCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<(Type Source, Type Dest), PropertyMappingInfo> _cache = new();
|
||||
//public sealed class PropertyMappingCache
|
||||
//{
|
||||
// private readonly ConcurrentDictionary<(Type Source, Type Dest), PropertyMappingInfo> _cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or builds property mapping between two types.
|
||||
/// Result is cached for subsequent calls.
|
||||
/// </summary>
|
||||
public PropertyMappingInfo GetOrBuild(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, object> getMetadata)
|
||||
{
|
||||
var key = (sourceType, destType);
|
||||
return _cache.GetOrAdd(key, _ => BuildMapping(sourceType, destType, customMapper, getMetadata));
|
||||
}
|
||||
// /// <summary>
|
||||
// /// Gets or builds property mapping between two types.
|
||||
// /// Result is cached for subsequent calls.
|
||||
// /// </summary>
|
||||
// public PropertyMappingInfo GetOrBuild(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, object> getMetadata)
|
||||
// {
|
||||
// var key = (sourceType, destType);
|
||||
// return _cache.GetOrAdd(key, _ => BuildMapping(sourceType, destType, customMapper, getMetadata));
|
||||
// }
|
||||
|
||||
private static PropertyMappingInfo BuildMapping(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, object> getMetadata)
|
||||
{
|
||||
var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead)
|
||||
.ToArray();
|
||||
// private static PropertyMappingInfo BuildMapping(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, object> getMetadata)
|
||||
// {
|
||||
// var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
// .Where(p => p.CanRead)
|
||||
// .ToArray();
|
||||
|
||||
var destProps = destType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanWrite)
|
||||
.ToDictionary(p => p.Name, StringComparer.Ordinal);
|
||||
// var destProps = destType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
// .Where(p => p.CanWrite)
|
||||
// .ToDictionary(p => p.Name, StringComparer.Ordinal);
|
||||
|
||||
var mappings = new List<(PropertyInfo Source, PropertyInfo Dest)>();
|
||||
// var mappings = new List<(PropertyInfo Source, PropertyInfo Dest)>();
|
||||
|
||||
foreach (var sourceProp in sourceProps)
|
||||
{
|
||||
PropertyInfo? destProp = null;
|
||||
// foreach (var sourceProp in sourceProps)
|
||||
// {
|
||||
// PropertyInfo? destProp = null;
|
||||
|
||||
// Use custom mapper if provided
|
||||
if (customMapper != null)
|
||||
{
|
||||
destProp = customMapper(sourceProp, destType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default: match by name
|
||||
destProps.TryGetValue(sourceProp.Name, out destProp);
|
||||
}
|
||||
// // Use custom mapper if provided
|
||||
// if (customMapper != null)
|
||||
// {
|
||||
// destProp = customMapper(sourceProp, destType);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // Default: match by name
|
||||
// destProps.TryGetValue(sourceProp.Name, out destProp);
|
||||
// }
|
||||
|
||||
if (destProp != null && AreTypesCompatible(sourceProp.PropertyType, destProp.PropertyType))
|
||||
{
|
||||
mappings.Add((sourceProp, destProp));
|
||||
}
|
||||
}
|
||||
// if (destProp != null && AreTypesCompatible(sourceProp.PropertyType, destProp.PropertyType))
|
||||
// {
|
||||
// mappings.Add((sourceProp, destProp));
|
||||
// }
|
||||
// }
|
||||
|
||||
return new PropertyMappingInfo(mappings.ToArray());
|
||||
}
|
||||
// return new PropertyMappingInfo(mappings.ToArray());
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if two property types are compatible for mapping.
|
||||
/// Handles exact match, inheritance, nullable unwrapping, and numeric conversions.
|
||||
/// </summary>
|
||||
private static bool AreTypesCompatible(Type sourceType, Type destType)
|
||||
{
|
||||
// Exact match
|
||||
if (sourceType == destType) return true;
|
||||
// /// <summary>
|
||||
// /// Checks if two property types are compatible for mapping.
|
||||
// /// Handles exact match, inheritance, nullable unwrapping, and numeric conversions.
|
||||
// /// </summary>
|
||||
// private static bool AreTypesCompatible(Type sourceType, Type destType)
|
||||
// {
|
||||
// // Exact match
|
||||
// if (sourceType == destType) return true;
|
||||
|
||||
// Assignable (inheritance, interfaces)
|
||||
if (destType.IsAssignableFrom(sourceType)) return true;
|
||||
// // Assignable (inheritance, interfaces)
|
||||
// if (destType.IsAssignableFrom(sourceType)) return true;
|
||||
|
||||
// Unwrap nullable types
|
||||
var sourceUnderlying = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
|
||||
var destUnderlying = Nullable.GetUnderlyingType(destType) ?? destType;
|
||||
// // Unwrap nullable types
|
||||
// var sourceUnderlying = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
|
||||
// var destUnderlying = Nullable.GetUnderlyingType(destType) ?? destType;
|
||||
|
||||
if (sourceUnderlying == destUnderlying) return true;
|
||||
// if (sourceUnderlying == destUnderlying) return true;
|
||||
|
||||
// Numeric conversions (int -> long, float -> double, etc.)
|
||||
if (IsNumericType(sourceUnderlying) && IsNumericType(destUnderlying))
|
||||
return true;
|
||||
// // Numeric conversions (int -> long, float -> double, etc.)
|
||||
// if (IsNumericType(sourceUnderlying) && IsNumericType(destUnderlying))
|
||||
// return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
// return false;
|
||||
// }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsNumericType(Type type)
|
||||
{
|
||||
return type == typeof(byte) || type == typeof(sbyte) ||
|
||||
type == typeof(short) || type == typeof(ushort) ||
|
||||
type == typeof(int) || type == typeof(uint) ||
|
||||
type == typeof(long) || type == typeof(ulong) ||
|
||||
type == typeof(float) || type == typeof(double) ||
|
||||
type == typeof(decimal);
|
||||
}
|
||||
}
|
||||
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
// private static bool IsNumericType(Type type)
|
||||
// {
|
||||
// return type == typeof(byte) || type == typeof(sbyte) ||
|
||||
// type == typeof(short) || type == typeof(ushort) ||
|
||||
// type == typeof(int) || type == typeof(uint) ||
|
||||
// type == typeof(long) || type == typeof(ulong) ||
|
||||
// type == typeof(float) || type == typeof(double) ||
|
||||
// type == typeof(decimal);
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Contains property mapping information for a source->destination type pair.
|
||||
/// Immutable and thread-safe.
|
||||
/// </summary>
|
||||
public sealed class PropertyMappingInfo
|
||||
{
|
||||
public PropertyMappingInfo(IReadOnlyList<(PropertyInfo Source, PropertyInfo Dest)> mappings)
|
||||
{
|
||||
Mappings = mappings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of source->destination property pairs.
|
||||
/// </summary>
|
||||
public IReadOnlyList<(PropertyInfo Source, PropertyInfo Dest)> Mappings { get; }
|
||||
}
|
||||
//public sealed record PropertyMappingInfo(IReadOnlyList<(PropertyInfo Source, PropertyInfo Dest)> Mappings)
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// List of source->destination property pairs.
|
||||
// /// </summary>
|
||||
// public IReadOnlyList<(PropertyInfo Source, PropertyInfo Dest)> Mappings { get; } = Mappings;
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Binary-specific index-to-index mapping for cross-type deserialization.
|
||||
|
|
|
|||
|
|
@ -30,9 +30,6 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
|
|||
/// </summary>
|
||||
protected abstract Func<Type, TMetadata> MetadataFactory { get; }
|
||||
|
||||
private const int BitArraySize = 1024;
|
||||
private const int MaxSmallId = BitArraySize * 64;
|
||||
|
||||
#region Wrapper Access
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -67,149 +64,21 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, int refId, out object? instance)
|
||||
{
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<int>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<int>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
|
||||
return map.TryGetValue(refId, out instance);
|
||||
return wrapper.TryGetValue(refId, out instance);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, long refId, out object? instance)
|
||||
{
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<long>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<long>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
|
||||
return map.TryGetValue(refId, out instance);
|
||||
return wrapper.TryGetValue(refId, out instance);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, Guid refId, out object? instance)
|
||||
{
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<Guid>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<Guid>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
|
||||
return map.TryGetValue(refId, out instance);
|
||||
return wrapper.TryGetValue(refId, out instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track an object with int RefId.
|
||||
/// Use when wrapper.Metadata.IdAccessorType == Int32.
|
||||
/// Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out int refId)
|
||||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32);
|
||||
|
||||
var getter = (Func<object, int>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
|
||||
// BitArray fast path for small positive IDs
|
||||
//if (refId >= 0 && refId < MaxSmallId)
|
||||
//{
|
||||
// return TryTrackSmallId(wrapper, refId);
|
||||
//}
|
||||
|
||||
// IdentityMap for large/negative IDs
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<int>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<int>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
return map.TryAddKey(refId);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryTrackSmallId(TypeMetadataWrapper<TMetadata> wrapper, int id)
|
||||
{
|
||||
wrapper.EnsureSmallIdBitmap();
|
||||
var bitmap = wrapper.SmallIdBitmap!;
|
||||
|
||||
uint idx = (uint)id;
|
||||
uint wordIdx = idx >> 6;
|
||||
int bitIdx = (int)(idx & 63);
|
||||
|
||||
ulong mask = 1UL << bitIdx;
|
||||
ref ulong word = ref bitmap[wordIdx];
|
||||
|
||||
if ((word & mask) == 0)
|
||||
{
|
||||
word |= mask;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tracking API - long
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track an object with long RefId.
|
||||
/// Use when wrapper.Metadata.IdAccessorType == Int64.
|
||||
/// Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out long refId)
|
||||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64);
|
||||
|
||||
var getter = (Func<object, long>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<long>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<long>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
return map.TryAddKey(refId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tracking API - Guid
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track an object with Guid RefId.
|
||||
/// Use when wrapper.Metadata.IdAccessorType == Guid.
|
||||
/// Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out Guid refId)
|
||||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid);
|
||||
|
||||
var getter = (Func<object, Guid>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<Guid>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<Guid>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
return map.TryAddKey(refId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization API - TryGetOrStore
|
||||
|
||||
/// <summary>
|
||||
/// For deserialization: checks if an object with this Id was already seen.
|
||||
/// If yes, returns the existing object. If no, stores this object and returns it.
|
||||
|
|
@ -218,15 +87,7 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreInt32(TypeMetadataWrapper<TMetadata> wrapper, object newObj, int id)
|
||||
{
|
||||
if (id == 0) return newObj; // Default Id - no tracking
|
||||
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<int>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<int>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
return map.TryGetOrAddValue(id, newObj);
|
||||
return id == 0 ? newObj : wrapper.TryGetOrStoreId(id, newObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -235,15 +96,7 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreLong(TypeMetadataWrapper<TMetadata> wrapper, object newObj, long id)
|
||||
{
|
||||
if (id == 0) return newObj;
|
||||
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<long>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<long>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
return map.TryGetOrAddValue(id, newObj);
|
||||
return id == 0 ? newObj : wrapper.TryGetOrStoreId(id, newObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -252,15 +105,7 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreGuid(TypeMetadataWrapper<TMetadata> wrapper, object newObj, Guid id)
|
||||
{
|
||||
if (id == Guid.Empty) return newObj;
|
||||
|
||||
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<Guid>;
|
||||
if (map == null)
|
||||
{
|
||||
map = new AcSerializerCommon.IdentityMap<Guid>();
|
||||
wrapper.IdentityMap = map;
|
||||
}
|
||||
return map.TryGetOrAddValue(id, newObj);
|
||||
return id == Guid.Empty ? newObj : wrapper.TryGetOrStoreId(id, newObj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public static partial class AcBinarySerializer
|
|||
/// <summary>
|
||||
/// Binary serialization context. Public for generated serializers.
|
||||
/// </summary>
|
||||
internal sealed class BinarySerializationContext : AcSerializerContextBase<BinarySerializeTypeMetadata>, IDisposable
|
||||
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata>, IDisposable
|
||||
{
|
||||
private const int MinBufferSize = 256;
|
||||
private const int PropertyIndexBufferMaxCache = 512;
|
||||
|
|
@ -65,7 +65,7 @@ public static partial class AcBinarySerializer
|
|||
private int _initialBufferSize;
|
||||
|
||||
// Use shared reference tracker from AcSerializerCommon
|
||||
private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
|
||||
//private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
|
||||
|
||||
private Dictionary<string, int>? _internedStrings;
|
||||
private List<string>? _internedStringList;
|
||||
|
|
@ -117,10 +117,10 @@ public static partial class AcBinarySerializer
|
|||
PropertyFilter = options.PropertyFilter;
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
|
||||
_refTracker.Reset();
|
||||
//_refTracker.Reset();
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
_refTracker.EnsureInitialized();
|
||||
//_refTracker.EnsureInitialized();
|
||||
}
|
||||
|
||||
// Reset wrapper tracking state from base class (IId tracking)
|
||||
|
|
@ -143,7 +143,7 @@ public static partial class AcBinarySerializer
|
|||
_bloomFilter2 = 0;
|
||||
_bloomFilter3 = 0;
|
||||
|
||||
_refTracker.Reset();
|
||||
//_refTracker.Reset();
|
||||
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
||||
|
||||
|
|
@ -1161,32 +1161,32 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#region Reference Handling
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj) => _refTracker.TrackForScanning(obj);
|
||||
//[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, BinarySerializeTypeMetadata 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 TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata 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);
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public bool ShouldWriteRef(object obj, out int refId) => _refTracker.ShouldWriteId(obj, out refId);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, int refId) => _refTracker.MarkAsWritten(obj, refId);
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public void MarkAsWritten(object obj, int refId) => _refTracker.MarkAsWritten(obj, refId);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out int refId) => _refTracker.TryGetExistingRef(obj, out refId);
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public bool TryGetExistingRef(object obj, out int refId) => _refTracker.TryGetExistingRef(obj, out refId);
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for all deserialization contexts (Binary, JSON, Toon).
|
||||
/// Provides common deserialization operations: Id lookup, object storage by Id.
|
||||
/// Derived classes are sealed for JIT devirtualization (direct call speed).
|
||||
/// </summary>
|
||||
/// <typeparam name="TMetadata">The concrete metadata type for deserialization.</typeparam>
|
||||
public abstract class DeserializationContextBase<TMetadata> : AcSerializerContextBase<TMetadata>
|
||||
where TMetadata : TypeMetadataBase
|
||||
{
|
||||
#region Object Lookup by Id (to be implemented by derived sealed classes)
|
||||
|
||||
// Future: Abstract methods for object lookup/storage
|
||||
// protected abstract object? GetByIdInt32Core(int id);
|
||||
// protected abstract object? GetByIdInt64Core(long id);
|
||||
// protected abstract object? GetByIdGuidCore(Guid id);
|
||||
// protected abstract object GetOrStoreByIdInt32Core(int id, object newObj);
|
||||
// protected abstract object GetOrStoreByIdInt64Core(long id, object newObj);
|
||||
// protected abstract object GetOrStoreByIdGuidCore(Guid id, object newObj);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Object Resolution (to be moved from AcSerializerContextBase)
|
||||
|
||||
// Future: Common lookup/storage logic
|
||||
// public object? GetById(int id) { ... }
|
||||
// public object? GetById(long id) { ... }
|
||||
// public object? GetById(Guid id) { ... }
|
||||
// public object GetOrStoreById(int id, object newObj) { ... }
|
||||
// public object GetOrStoreById(long id, object newObj) { ... }
|
||||
// public object GetOrStoreById(Guid id, object newObj) { ... }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset
|
||||
|
||||
/// <summary>
|
||||
/// Resets deserialization-specific state. Called by derived classes.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
// Future: Reset deserialization-specific state
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ public static partial class AcJsonSerializer
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class JsonSerializationContext : AcSerializerContextBase<JsonSerializeTypeMetadata>, IDisposable
|
||||
private sealed class JsonSerializationContext : SerializationContextBase<JsonSerializeTypeMetadata>, IDisposable
|
||||
{
|
||||
private readonly ArrayBufferWriter<byte> _buffer;
|
||||
public Utf8JsonWriter Writer { get; private set; }
|
||||
|
|
|
|||
|
|
@ -7,121 +7,121 @@ namespace AyCode.Core.Serializers;
|
|||
/// 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 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;
|
||||
}
|
||||
// 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>
|
||||
// /// 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>
|
||||
// /// 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>
|
||||
// /// 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>
|
||||
// /// 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>
|
||||
// /// 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>
|
||||
///// 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>(64, StringComparer.Ordinal);
|
||||
_idToObject[id] = obj;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// Registers an object with its $id.
|
||||
// /// </summary>
|
||||
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
// public void RegisterObject(string id, object obj)
|
||||
// {
|
||||
// _idToObject ??= new Dictionary<string, object>(64, 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>
|
||||
// /// 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>
|
||||
// /// Clears all tracking data for reuse.
|
||||
// /// </summary>
|
||||
// public void Clear()
|
||||
// {
|
||||
// _idToObject?.Clear();
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Reference equality comparer for object identity comparison.
|
||||
|
|
@ -130,9 +130,9 @@ public sealed class DeserializationReferenceTracker
|
|||
public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
|
||||
{
|
||||
public static readonly ReferenceEqualityComparer Instance = new();
|
||||
|
||||
|
||||
private ReferenceEqualityComparer() { }
|
||||
|
||||
|
||||
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
|
||||
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for all serialization contexts (Binary, JSON, Toon).
|
||||
/// Provides common serialization operations: Id extraction, tracking first occurrence.
|
||||
/// Derived classes are sealed for JIT devirtualization (direct call speed).
|
||||
/// </summary>
|
||||
/// <typeparam name="TMetadata">The concrete metadata type for serialization.</typeparam>
|
||||
public abstract class SerializationContextBase<TMetadata> : AcSerializerContextBase<TMetadata>
|
||||
where TMetadata : TypeMetadataBase
|
||||
{
|
||||
private const int BitArraySize = 1024;
|
||||
private const int MaxSmallId = BitArraySize * 64;
|
||||
|
||||
#region Id Extraction (to be implemented by derived sealed classes)
|
||||
|
||||
// Future: Abstract methods for Id extraction
|
||||
// protected abstract int GetIdInt32Core(object obj, TMetadata metadata);
|
||||
// protected abstract long GetIdInt64Core(object obj, TMetadata metadata);
|
||||
// protected abstract Guid GetIdGuidCore(object obj, TMetadata metadata);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tracking (to be moved from AcSerializerContextBase)
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track an object with int RefId.
|
||||
/// Use when wrapper.Metadata.IdAccessorType == Int32.
|
||||
/// Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out int refId)
|
||||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32);
|
||||
|
||||
var getter = (Func<object, int>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
|
||||
// BitArray fast path for small positive IDs
|
||||
return refId is >= 0 and < MaxSmallId ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryTrackSmallId(TypeMetadataWrapper<TMetadata> wrapper, int id)
|
||||
{
|
||||
wrapper.EnsureSmallIdBitmap();
|
||||
var bitmap = wrapper.SmallIdBitmap!;
|
||||
|
||||
var idx = (uint)id;
|
||||
var wordIdx = idx >> 6;
|
||||
var bitIdx = (int)(idx & 63);
|
||||
|
||||
var mask = 1UL << bitIdx;
|
||||
ref var word = ref bitmap[wordIdx];
|
||||
|
||||
if ((word & mask) != 0) return false;
|
||||
|
||||
word |= mask;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track an object with long RefId.
|
||||
/// Use when wrapper.Metadata.IdAccessorType == Int64.
|
||||
/// Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out long refId)
|
||||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64);
|
||||
|
||||
var getter = (Func<object, long>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
|
||||
return wrapper.TryAddKey(refId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track an object with Guid RefId.
|
||||
/// Use when wrapper.Metadata.IdAccessorType == Guid.
|
||||
/// Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out Guid refId)
|
||||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid);
|
||||
|
||||
var getter = (Func<object, Guid>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
|
||||
return wrapper.TryAddKey(refId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset
|
||||
|
||||
/// <summary>
|
||||
/// Resets serialization-specific state. Called by derived classes.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
// Future: Reset serialization-specific state
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ using System.Collections.Generic;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
|
||||
//using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ public static partial class AcToonSerializer
|
|||
/// Pooled context for Toon serialization.
|
||||
/// Handles output building, indentation, and reference tracking.
|
||||
/// </summary>
|
||||
private sealed class ToonSerializationContext : AcSerializerContextBase<ToonSerializeTypeMetadata>
|
||||
private sealed class ToonSerializationContext : SerializationContextBase<ToonSerializeTypeMetadata>
|
||||
{
|
||||
private readonly StringBuilder _builder;
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
/// <summary>
|
||||
/// Identity map for tracking. Runtime type is IdentityMap<int/long/Guid>.
|
||||
/// </summary>
|
||||
internal AcSerializerCommon.IIdentityMap? IdentityMap;
|
||||
protected AcSerializerCommon.IIdentityMap? IdentityMap;
|
||||
|
||||
/// <summary>
|
||||
/// BitArray for tracking small int32 IDs (0-65535).
|
||||
|
|
@ -77,4 +77,42 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
{
|
||||
SmallIdBitmap ??= new ulong[BitArraySize];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates the typed IdentityMap for tracking.
|
||||
/// Use: wrapper.GetOrCreateIdentityMap<int>().TryAddKey(id)
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull
|
||||
{
|
||||
if (IdentityMap is AcSerializerCommon.IdentityMap<TId> typedMap)
|
||||
return typedMap;
|
||||
|
||||
var newMap = new AcSerializerCommon.IdentityMap<TId>();
|
||||
IdentityMap = newMap;
|
||||
return newMap;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryAddKey<TId>(TId key) where TId : struct
|
||||
{
|
||||
var map = GetOrCreateIdentityMap<TId>();
|
||||
return map.TryAddKey(key);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValue<TId>(TId refId, out object? instance) where TId : struct
|
||||
{
|
||||
var map = GetOrCreateIdentityMap<TId>();
|
||||
return map.TryGetValue(refId, out instance);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreId<TId>(TId id, object newObj) where TId : struct
|
||||
{
|
||||
//if (id == default(TId)) return newObj; // Default Id - no tracking
|
||||
|
||||
var map = GetOrCreateIdentityMap<TId>();
|
||||
return map.TryGetOrAddValue(id, newObj);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue