AyCode.Core/AyCode.Core/Serializers/AcSerializerCommon.cs

1107 lines
41 KiB
C#

using System.Collections;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Expressions;
using AyCode.Core.Serializers.Jsons;
using LExpression = System.Linq.Expressions.Expression;
using LExpressionType = System.Linq.Expressions.ExpressionType;
namespace AyCode.Core.Serializers;
/// <summary>
/// Common utilities shared across all serializers (JSON, Binary).
/// Centralizes expression compilation helpers to eliminate code duplication.
/// Type references and caches remain in JsonUtilities for backward compatibility.
/// </summary>
public static class AcSerializerCommon
{
#region Type References
/// <summary>
/// Expression base type for type checking.
/// </summary>
public static readonly Type ExpressionType = typeof(LExpression);
/// <summary>
/// Generic Expression&lt;TDelegate&gt; type definition.
/// </summary>
public static readonly Type ExpressionGenericType = typeof(Expression<>);
/// <summary>
/// Generic IQueryable&lt;T&gt; type definition.
/// </summary>
public static readonly Type QueryableGenericType = typeof(IQueryable<>);
/// <summary>
/// Non-generic IQueryable type.
/// </summary>
public static readonly Type QueryableType = typeof(IQueryable);
#endregion
#region ThreadLocal Cache Helper
/// <summary>
/// Maximum local cache size before clearing.
/// Clear() is preferred over new Dictionary() because:
/// 1. Reuses already allocated internal array (no new allocation)
/// 2. Reduces GC pressure
/// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
/// </summary>
public const int MaxLocalCacheSize = 64;
/// <summary>
/// Helper class for ThreadLocal caching pattern used across serializers.
/// Provides a two-level cache: ThreadLocal (fast) + ConcurrentDictionary (shared).
/// </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
{
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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Get(TKey key)
{
// Fast path: check ThreadLocal cache first
var localCache = t_localCache;
if (localCache != null && localCache.TryGetValue(key, out var cached))
{
return cached;
}
// Slow path
return GetSlow(key);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private TValue GetSlow(TKey key)
{
var value = _globalCache.GetOrAdd(key, _factory);
// Populate ThreadLocal cache
var localCache = t_localCache ??= new Dictionary<TKey, TValue>();
// Clear when full - reuses internal array
if (localCache.Count >= MaxLocalCacheSize)
{
localCache.Clear();
}
localCache[key] = value;
return value;
}
/// <summary>
/// Clears the ThreadLocal cache for the current thread.
/// </summary>
public static void ClearLocalCache()
{
t_localCache?.Clear();
}
}
#endregion
#region Type Checking
/// <summary>
/// Checks if a type is an Expression type.
/// Shared across JSON and Binary serializers.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsExpressionType(Type type)
{
return ExpressionType.IsAssignableFrom(type);
}
/// <summary>
/// Checks if a type is Expression&lt;TDelegate&gt; (e.g., Expression&lt;Func&lt;T, bool&gt;&gt;).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsGenericExpressionType(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == ExpressionGenericType;
}
/// <summary>
/// Checks if a type is IQueryable or IQueryable&lt;T&gt;.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsQueryableType(Type type)
{
if (QueryableType.IsAssignableFrom(type))
return true;
if (type.IsGenericType && type.GetGenericTypeDefinition() == QueryableGenericType)
return true;
return false;
}
/// <summary>
/// Gets the entity type from IQueryable&lt;TEntity&gt;.
/// Returns null if not a valid queryable type.
/// </summary>
public static Type? GetQueryableEntityType(Type queryableType)
{
if (queryableType.IsGenericType && queryableType.GetGenericTypeDefinition() == QueryableGenericType)
return queryableType.GetGenericArguments()[0];
// Check interfaces for IQueryable<T>
foreach (var iface in queryableType.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == QueryableGenericType)
return iface.GetGenericArguments()[0];
}
return null;
}
/// <summary>
/// Gets the entity type from Expression&lt;Func&lt;TEntity, TResult&gt;&gt;.
/// Returns null if not a valid expression type.
/// </summary>
public static Type? GetExpressionEntityType(Type expressionType)
{
if (!IsGenericExpressionType(expressionType))
return null;
var delegateType = expressionType.GetGenericArguments()[0];
if (!delegateType.IsGenericType)
return null;
var delegateArgs = delegateType.GetGenericArguments();
return delegateArgs.Length > 0 ? delegateArgs[0] : null;
}
#endregion
#region IQueryable Serialization
/// <summary>
/// Converts an IQueryable to AcExpressionNode for serialization.
/// The query's expression tree is serialized.
/// </summary>
public static AcExpressionNode QueryableToNode(IQueryable queryable)
{
return AcExpressionConverter.ToNode(queryable.Expression);
}
/// <summary>
/// Applies an AcExpressionNode to an IQueryable source.
/// Used to rebuild the query on the server side.
/// </summary>
public static IQueryable<T> ApplyQueryFromNode<T>(IQueryable<T> source, AcExpressionNode node)
{
var expression = RebuildQueryExpression(source.Expression, node, typeof(T));
return source.Provider.CreateQuery<T>(expression);
}
/// <summary>
/// Executes an aggregate query (Count, Sum, Any, etc.) from an AcExpressionNode.
/// Returns the scalar result of the query.
/// </summary>
public static object? ExecuteQueryFromNode<T>(IQueryable<T> source, AcExpressionNode node)
{
var expression = RebuildQueryExpression(source.Expression, node, typeof(T));
return source.Provider.Execute(expression);
}
/// <summary>
/// Rebuilds a query expression, replacing the source with the provided expression.
/// </summary>
private static LExpression RebuildQueryExpression(LExpression sourceExpression, AcExpressionNode node, Type entityType)
{
if (node is { NodeType: LExpressionType.Call, MethodName: not null })
{
return RebuildMethodCallChain(sourceExpression, node, entityType);
}
// If it's just a lambda (filter expression), wrap it in a Where call
if (node.NodeType == LExpressionType.Lambda)
{
var lambda = AcExpressionRebuilder.FromNode(node, entityType);
var whereMethod = typeof(Queryable).GetMethods()
.First(m => m.Name == "Where" && m.GetParameters().Length == 2)
.MakeGenericMethod(entityType);
return LExpression.Call(whereMethod, sourceExpression, lambda);
}
throw new NotSupportedException($"Cannot apply expression of type '{node.NodeType}' to IQueryable.");
}
/// <summary>
/// Rebuilds a chain of method calls (Where, OrderBy, Skip, Take, etc.)
/// Automatically replaces the original IQueryable source with the server's source.
/// </summary>
private static LExpression RebuildMethodCallChain(LExpression sourceExpression, AcExpressionNode node, Type entityType)
{
// First, process the inner expression (the source of this method call)
LExpression currentSource;
if (node.Arguments?.Count > 0)
{
var firstArg = node.Arguments[0];
// Check if first argument is another method call (recursive chain)
if (firstArg.NodeType == LExpressionType.Call)
{
currentSource = RebuildMethodCallChain(sourceExpression, firstArg, entityType);
}
// Check if first argument is a Constant (this is the original IQueryable source - replace it)
else if (firstArg.NodeType == LExpressionType.Constant)
{
// The original source (e.g., empty list from client) - replace with server source
currentSource = sourceExpression;
}
else
{
// Other cases - use the provided source
currentSource = sourceExpression;
}
}
else
{
currentSource = sourceExpression;
}
// Now apply this method call
var methodName = node.MethodName!;
var declaringType = ResolveDeclaringType(node.DeclaringType);
// Find the method
var method = FindQueryableMethod(declaringType, methodName, node.GenericArguments, node.Arguments?.Count ?? 1);
if (method == null)
throw new InvalidOperationException($"Method '{methodName}' not found on type '{declaringType.FullName}'.");
// Apply generic type arguments
if (method.IsGenericMethodDefinition && node.GenericArguments?.Count > 0)
{
var genericTypes = node.GenericArguments.Select(t => ResolveTypeName(t) ?? entityType).ToArray();
method = method.MakeGenericMethod(genericTypes);
}
// Build arguments - first argument is always the source
var arguments = new List<LExpression> { currentSource };
// Process remaining arguments (skip first - it's the source we already handled)
if (node.Arguments?.Count > 1)
{
for (var i = 1; i < node.Arguments.Count; i++)
{
var argNode = node.Arguments[i];
if (argNode.NodeType == LExpressionType.Quote && argNode.Operand != null)
{
// Quoted lambda - unquote and deserialize
var lambda = AcExpressionRebuilder.FromNode(argNode.Operand, entityType);
arguments.Add(LExpression.Quote(lambda));
}
else if (argNode.NodeType == LExpressionType.Lambda)
{
// Lambda needs to be quoted for Queryable methods
var lambda = AcExpressionRebuilder.FromNode(argNode, entityType);
arguments.Add(LExpression.Quote(lambda));
}
else
{
// Other arguments (e.g., int for Skip/Take)
arguments.Add(AcExpressionRebuilder.FromNode(argNode, entityType));
}
}
}
return LExpression.Call(method, arguments);
}
/// <summary>
/// Resolves the declaring type from the type name.
/// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback.
/// Works with any ORM extension methods (EF Core Include, linq2db LoadWith, etc.)
/// </summary>
private static Type ResolveDeclaringType(string? typeName)
{
if (string.IsNullOrEmpty(typeName))
return typeof(Queryable);
// Try direct resolution first (works for AssemblyQualifiedName)
var type = Type.GetType(typeName);
if (type != null)
return type;
// Extract the type name without assembly info for searching
var typeNameOnly = typeName.Contains(',')
? typeName.Substring(0, typeName.IndexOf(','))
: typeName;
// Fast path for common LINQ types
if (typeNameOnly is "System.Linq.Queryable") return typeof(Queryable);
if (typeNameOnly is "System.Linq.Enumerable") return typeof(Enumerable);
// Search all loaded assemblies (works for FullName when assembly is loaded)
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(typeNameOnly);
if (type != null)
return type;
}
// Fallback to Queryable
return typeof(Queryable);
}
/// <summary>
/// Resolves a type from its name.
/// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback.
/// </summary>
private static Type? ResolveTypeName(string typeName)
{
// Try direct resolution first (works for AssemblyQualifiedName)
var type = Type.GetType(typeName);
if (type != null)
return type;
// Extract the type name without assembly info
var typeNameOnly = typeName.Contains(',')
? typeName.Substring(0, typeName.IndexOf(','))
: typeName;
// Fast path for common types
type = typeNameOnly switch
{
"System.String" or "string" => typeof(string),
"System.Int32" or "int" => typeof(int),
"System.Int64" or "long" => typeof(long),
"System.Int16" or "short" => typeof(short),
"System.Byte" or "byte" => typeof(byte),
"System.SByte" or "sbyte" => typeof(sbyte),
"System.Boolean" or "bool" => typeof(bool),
"System.Double" or "double" => typeof(double),
"System.Single" or "float" => typeof(float),
"System.Decimal" or "decimal" => typeof(decimal),
"System.DateTime" => typeof(DateTime),
"System.DateTimeOffset" => typeof(DateTimeOffset),
"System.TimeSpan" => typeof(TimeSpan),
"System.DateOnly" => typeof(DateOnly),
"System.TimeOnly" => typeof(TimeOnly),
"System.Guid" => typeof(Guid),
"System.Object" or "object" => typeof(object),
_ => null
};
if (type != null)
return type;
// Search all loaded assemblies
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(typeNameOnly);
if (type != null)
return type;
}
return null;
}
private static MethodInfo? FindQueryableMethod(Type declaringType, string methodName, List<string>? genericArgs, int argCount)
{
return declaringType.GetMethods()
.Where(m => m.Name == methodName)
.FirstOrDefault(m =>
{
var parameters = m.GetParameters();
if (parameters.Length != argCount) return false;
// Check if generic argument count matches
if (m.IsGenericMethodDefinition)
{
var genericCount = genericArgs?.Count ?? 1;
if (m.GetGenericArguments().Length != genericCount) return false;
}
return true;
});
}
#endregion
#region Expression Rebuilding
/// <summary>
/// Rebuilds an Expression from AcExpressionNode for the target Expression type.
/// Used by both JSON and Binary deserializers.
/// </summary>
/// <param name="node">The serialized expression node</param>
/// <param name="targetExpressionType">The target Expression&lt;TDelegate&gt; type</param>
/// <returns>The rebuilt Expression, or null if conversion fails</returns>
public static object? RebuildExpression(AcExpressionNode node, Type targetExpressionType)
{
if (node == null)
return null;
var entityType = GetExpressionEntityType(targetExpressionType);
return AcExpressionRebuilder.FromNode(node, entityType);
}
#endregion
#region Expression Compilation Helpers
/// <summary>
/// Creates a compiled getter for a property using expression trees.
/// Shared across all TypeMetadata implementations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
{
var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
var boxed = LExpression.Convert(propAccess, typeof(object));
return LExpression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
/// <summary>
/// Creates a compiled setter for a property using expression trees.
/// Handles nullable value types correctly, including null values.
/// </summary>
public static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop)
{
var targetParam = LExpression.Parameter(typeof(object), "target");
var valueParam = LExpression.Parameter(typeof(object), "value");
var castTarget = LExpression.Convert(targetParam, declaringType);
var propertyAccess = LExpression.Property(castTarget, prop);
Expression castValue;
var propertyType = prop.PropertyType;
var underlyingType = Nullable.GetUnderlyingType(propertyType);
if (underlyingType != null)
{
// Nullable value type: handle null case with conditional
// if (value == null) property = default; else property = (T?)Unbox(value)
var nullCheck = LExpression.Equal(valueParam, LExpression.Constant(null, typeof(object)));
var nullValue = LExpression.Default(propertyType);
var unboxed = LExpression.Unbox(valueParam, underlyingType);
var converted = LExpression.Convert(unboxed, propertyType);
castValue = LExpression.Condition(nullCheck, nullValue, converted);
}
else if (propertyType.IsValueType)
{
// Non-nullable value type: use Unbox for proper unboxing
castValue = LExpression.Unbox(valueParam, propertyType);
}
else
{
// Reference type: use TypeAs for safe casting
castValue = LExpression.TypeAs(valueParam, propertyType);
}
var assign = LExpression.Assign(propertyAccess, castValue);
return LExpression.Lambda<Action<object, object?>>(assign, targetParam, valueParam).Compile();
}
/// <summary>
/// Creates a compiled parameterless constructor for a type.
/// Returns null if type is abstract or has no parameterless constructor.
/// </summary>
public static Func<object>? CreateCompiledConstructor(Type type)
{
if (type.IsAbstract) return null;
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null);
if (ctor == null) return null;
var newExpr = LExpression.New(ctor);
var convert = LExpression.Convert(newExpr, typeof(object));
return LExpression.Lambda<Func<object>>(convert).Compile();
}
/// <summary>
/// Creates a typed getter delegate to avoid boxing for value types.
/// </summary>
public static Func<object, TProperty> CreateTypedGetter<TProperty>(Type declaringType, PropertyInfo prop)
{
var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
var convertExpr = LExpression.Convert(propAccess, typeof(TProperty));
return LExpression.Lambda<Func<object, TProperty>>(convertExpr, objParam).Compile();
}
/// <summary>
/// Creates an enum getter that returns int to avoid boxing.
/// </summary>
public static Func<object, int> CreateEnumGetter(Type declaringType, PropertyInfo prop)
{
var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
var convertToInt = LExpression.Convert(propAccess, typeof(int));
return LExpression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
}
/// <summary>
/// Creates a typed setter delegate to avoid boxing for value types.
/// </summary>
public static Action<object, TProperty> CreateTypedSetter<TProperty>(Type declaringType, PropertyInfo prop)
{
var objParam = LExpression.Parameter(typeof(object), "obj");
var valueParam = LExpression.Parameter(typeof(TProperty), "value");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
var assign = LExpression.Assign(propAccess, valueParam);
return LExpression.Lambda<Action<object, TProperty>>(assign, objParam, valueParam).Compile();
}
/// <summary>
/// Creates an enum setter that accepts int to avoid boxing.
/// </summary>
public static Action<object, int> CreateEnumSetter(Type declaringType, PropertyInfo prop)
{
var objParam = LExpression.Parameter(typeof(object), "obj");
var valueParam = LExpression.Parameter(typeof(int), "value");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
var convertToEnum = LExpression.Convert(valueParam, prop.PropertyType);
var assign = LExpression.Assign(propAccess, convertToEnum);
return LExpression.Lambda<Action<object, int>>(assign, objParam, valueParam).Compile();
}
#endregion
#region Chain Reference Tracking
/// <summary>
/// Tracks IId objects across chain deserializations to maintain reference identity.
/// Used internally by IBinaryDeserializeChain.ThenPopulate to ensure same object references.
/// </summary>
public sealed class ChainReferenceTracker
{
private readonly Dictionary<(Type, object), object> _idToObject = new();
/// <summary>
/// Registers an IId object for later retrieval.
/// </summary>
public bool TryRegisterIIdObject(object obj)
{
if (obj == null) return false;
var type = obj.GetType();
var idProp = type.GetProperty("Id");
if (idProp == null) return false;
var id = idProp.GetValue(obj);
if (id == null) return false;
// Create a normalized key
var key = (type, NormalizeId(id));
_idToObject[key] = obj;
return true;
}
/// <summary>
/// Tries to get a previously registered object by type and ID.
/// </summary>
public bool TryGetObject(object id, out object? obj)
{
obj = null;
if (id == null) return false;
var normalizedId = NormalizeId(id);
// Search by normalized ID (ignoring type for simplicity in lookup)
foreach (var kvp in _idToObject)
{
if (Equals(kvp.Key.Item2, normalizedId))
{
obj = kvp.Value;
return true;
}
}
return false;
}
/// <summary>
/// Tries to get a previously registered object by exact type and ID.
/// </summary>
public bool TryGetObject(Type type, object id, out object? obj)
{
var key = (type, NormalizeId(id));
return _idToObject.TryGetValue(key, out obj);
}
/// <summary>
/// Clears all tracked references.
/// </summary>
public void Clear() => _idToObject.Clear();
/// <summary>
/// Normalizes the ID value for consistent dictionary lookups.
/// Handles boxed value type comparisons.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object NormalizeId(object id)
{
// Convert common ID types to a consistent representation
return id switch
{
int i => i,
long l => l,
Guid g => g,
string s => s,
_ => id
};
}
}
#endregion
#region Serialization Reference Tracking
/// <summary>
/// Common reference tracking for serialization.
/// Used by both JSON and Binary serializers to track multi-referenced objects.
/// Uses int IDs for efficiency (no string allocation).
/// </summary>
public sealed class SerializationReferenceTracker
{
private const int InitialReferenceCapacity = 64;
private const int InitialMultiRefCapacity = 32;
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, int>? _writtenRefs;
private HashSet<object>? _multiReferenced;
private int _nextRefId = 1;
/// <summary>
/// Resets the tracker for reuse.
/// </summary>
public void Reset()
{
_nextRefId = 1;
_scanOccurrences?.Clear();
_writtenRefs?.Clear();
_multiReferenced?.Clear();
}
/// <summary>
/// Ensures internal collections are initialized.
/// Call once before scanning when reference handling is enabled.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EnsureInitialized()
{
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
_writtenRefs ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
}
/// <summary>
/// Tracks an object during reference scanning phase.
/// Returns true if this is the first occurrence (continue scanning).
/// Returns false if already seen (object is multi-referenced, stop scanning this branch).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanning(object obj)
{
if (_scanOccurrences == null) return true;
ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
if (exists)
{
count++;
_multiReferenced!.Add(obj);
return false;
}
count = 1;
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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteId(object obj, out int refId)
{
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj))
{
refId = _nextRefId++;
return true;
}
refId = 0;
return false;
}
/// <summary>
/// Marks object as written with its reference ID.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, int refId)
{
_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)
{
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
{
return true;
}
refId = 0;
return false;
}
}
#endregion
#region Property Mapping for Cross-Type Operations
/// <summary>
/// Maps property indices or names from source type to destination type.
/// Supports both name-based matching and custom PropertyMapper callbacks.
/// Used by cross-type deserialization (Deserialize&lt;TSource, TDest&gt;).
/// Thread-safe and cached for performance.
/// </summary>
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));
}
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 mappings = new List<(PropertyInfo Source, PropertyInfo Dest)>();
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);
}
if (destProp != null && AreTypesCompatible(sourceProp.PropertyType, destProp.PropertyType))
{
mappings.Add((sourceProp, destProp));
}
}
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;
// Assignable (inheritance, interfaces)
if (destType.IsAssignableFrom(sourceType)) return true;
// Unwrap nullable types
var sourceUnderlying = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
var destUnderlying = Nullable.GetUnderlyingType(destType) ?? destType;
if (sourceUnderlying == destUnderlying) return true;
// Numeric conversions (int -> long, float -> double, etc.)
if (IsNumericType(sourceUnderlying) && IsNumericType(destUnderlying))
return true;
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);
}
}
/// <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; }
}
/// <summary>
/// Binary-specific index-to-index mapping for cross-type deserialization.
/// Maps source PropertyIndex to destination PropertyIndex.
/// Thread-safe and cached.
/// </summary>
public sealed class BinaryIndexMappingCache
{
private readonly ConcurrentDictionary<(Type Source, Type Dest), int[]> _cache = new();
/// <summary>
/// Gets or builds index mapping from source type to destination type.
/// Returns array where index is source PropertyIndex, value is destination PropertyIndex (-1 if no mapping).
/// </summary>
public int[] GetOrBuild(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, PropertyInfo[]> getOrderedProperties)
{
var key = (sourceType, destType);
return _cache.GetOrAdd(key, _ => BuildIndexMapping(sourceType, destType, customMapper, getOrderedProperties));
}
private static int[] BuildIndexMapping(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, PropertyInfo[]> getOrderedProperties)
{
var sourceProps = getOrderedProperties(sourceType);
var destProps = getOrderedProperties(destType);
// Build lookup: property name -> destination index
var destIndexByName = new Dictionary<string, int>(destProps.Length, StringComparer.Ordinal);
for (int i = 0; i < destProps.Length; i++)
{
destIndexByName[destProps[i].Name] = i;
}
// Map source index -> destination index
var mapping = new int[sourceProps.Length];
for (int srcIdx = 0; srcIdx < sourceProps.Length; srcIdx++)
{
var sourceProp = sourceProps[srcIdx];
PropertyInfo? destProp = null;
// Use custom mapper if provided
if (customMapper != null)
{
destProp = customMapper(sourceProp, destType);
}
else
{
// Default: match by name
if (destIndexByName.TryGetValue(sourceProp.Name, out var destIdx))
{
destProp = destProps[destIdx];
}
}
if (destProp != null && destIndexByName.TryGetValue(destProp.Name, out var mappedIdx))
{
mapping[srcIdx] = mappedIdx;
}
else
{
mapping[srcIdx] = -1; // Skip this property
}
}
return mapping;
}
}
#endregion
#region Deserialization Reference Tracking
/// <summary>
/// Common reference tracking for deserialization.
/// Used by both JSON and Binary deserializers to resolve $id/$ref references.
/// Uses int IDs for efficiency.
/// </summary>
public sealed class DeserializationReferenceTracker
{
private Dictionary<int, object>? _idToObject;
/// <summary>
/// Resets the tracker for reuse.
/// </summary>
public void Reset()
{
_idToObject?.Clear();
}
/// <summary>
/// Registers an object with its reference ID.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(int id, object obj)
{
_idToObject ??= new Dictionary<int, object>(64);
_idToObject[id] = obj;
}
/// <summary>
/// Tries to get a previously registered object by reference ID.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetReferencedObject(int id, out object? obj)
{
if (_idToObject != null && _idToObject.TryGetValue(id, out obj!))
{
return true;
}
obj = null;
return false;
}
}
#endregion
#region Type Name Formatting
/// <summary>
/// Gets the C# display name for a type (e.g., "int", "List&lt;string&gt;", "Dictionary&lt;int, string&gt;").
/// Handles primitives, nullables, generics, enums, collections, and dictionaries.
/// Thread-safe with circular reference protection.
/// </summary>
/// <param name="type">The type to get the name for.</param>
/// <param name="useShortNames">If true, uses C# keywords (int, string). If false, uses full names (Int32, String).</param>
/// <returns>C# display name of the type.</returns>
public static string GetCSharpTypeName(Type type, bool useShortNames = true)
{
// Handle nullable types
var underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
return GetCSharpTypeName(underlying, useShortNames) + "?";
}
// Handle arrays
if (type.IsArray)
{
var elementType = type.GetElementType()!;
return GetCSharpTypeName(elementType, useShortNames) + "[]";
}
// IMPORTANT: Check for enum BEFORE primitive type check!
// Enum TypeCode returns the underlying type's code (e.g., Int32),
// which would incorrectly return "int" instead of the enum name.
if (type.IsEnum)
{
return type.Name;
}
// Handle primitive types with C# keywords
if (useShortNames)
{
var typeCode = Type.GetTypeCode(type);
var primitiveName = typeCode switch
{
TypeCode.Boolean => "bool",
TypeCode.Byte => "byte",
TypeCode.SByte => "sbyte",
TypeCode.Char => "char",
TypeCode.Int16 => "short",
TypeCode.UInt16 => "ushort",
TypeCode.Int32 => "int",
TypeCode.UInt32 => "uint",
TypeCode.Int64 => "long",
TypeCode.UInt64 => "ulong",
TypeCode.Single => "float",
TypeCode.Double => "double",
TypeCode.Decimal => "decimal",
TypeCode.String => "string",
_ => null
};
if (primitiveName != null) return primitiveName;
}
// Handle common BCL types
if (type == typeof(object)) return "object";
if (type == typeof(void)) return "void";
// Handle generic types
if (type.IsGenericType)
{
var genericDef = type.GetGenericTypeDefinition();
var args = type.GetGenericArguments();
// Special handling for common generic types
if (genericDef == typeof(List<>))
return $"List<{GetCSharpTypeName(args[0], useShortNames)}>";
if (genericDef == typeof(Dictionary<,>))
return $"Dictionary<{GetCSharpTypeName(args[0], useShortNames)}, {GetCSharpTypeName(args[1], useShortNames)}>";
if (genericDef == typeof(IEnumerable<>))
return $"IEnumerable<{GetCSharpTypeName(args[0], useShortNames)}>";
if (genericDef == typeof(ICollection<>))
return $"ICollection<{GetCSharpTypeName(args[0], useShortNames)}>";
if (genericDef == typeof(IList<>))
return $"IList<{GetCSharpTypeName(args[0], useShortNames)}>";
// Generic type with name mangling (e.g., SomeType`2)
var baseName = type.Name;
var backtickIndex = baseName.IndexOf('`');
if (backtickIndex > 0)
{
baseName = baseName.Substring(0, backtickIndex);
}
var argNames = string.Join(", ", args.Select(a => GetCSharpTypeName(a, useShortNames)));
return $"{baseName}<{argNames}>";
}
// Simple type name (class, struct, enum, etc.)
return type.Name;
}
#endregion
}