1107 lines
41 KiB
C#
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<TDelegate> type definition.
|
|
/// </summary>
|
|
public static readonly Type ExpressionGenericType = typeof(Expression<>);
|
|
|
|
/// <summary>
|
|
/// Generic IQueryable<T> 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<TDelegate> (e.g., Expression<Func<T, bool>>).
|
|
/// </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<T>.
|
|
/// </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<TEntity>.
|
|
/// 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<Func<TEntity, TResult>>.
|
|
/// 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<TDelegate> 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<TSource, TDest>).
|
|
/// 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<string>", "Dictionary<int, string>").
|
|
/// 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
|
|
} |