357 lines
15 KiB
C#
357 lines
15 KiB
C#
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace AyCode.Blazor.Components.Services.ExpressionHelpers;
|
|
|
|
/// <summary>
|
|
/// Deserializes AcExpressionNode DTO back to Expression tree.
|
|
/// </summary>
|
|
public class AcExpressionDeserializer
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
Converters = { new JsonStringEnumConverter() }
|
|
};
|
|
|
|
private readonly Dictionary<int, ParameterExpression> _parameters = new();
|
|
|
|
/// <summary>
|
|
/// Deserializes JSON to Expression.
|
|
/// </summary>
|
|
public static Expression ExpressionFromJson(string json, Type? entityType = null)
|
|
{
|
|
var node = JsonSerializer.Deserialize<AcExpressionNode>(json, JsonOptions)
|
|
?? throw new ArgumentException("Invalid expression JSON", nameof(json));
|
|
|
|
var deserializer = new AcExpressionDeserializer();
|
|
return deserializer.Deserialize(node, entityType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes JSON to typed Expression.
|
|
/// </summary>
|
|
public static Expression<Func<T, TResult>> ExpressionFromJson<T, TResult>(string json)
|
|
{
|
|
var expression = ExpressionFromJson(json, typeof(T));
|
|
return (Expression<Func<T, TResult>>)expression;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes AcExpressionNode to Expression.
|
|
/// </summary>
|
|
public Expression Deserialize(AcExpressionNode node, Type? entityType = null)
|
|
{
|
|
return node.NodeType switch
|
|
{
|
|
ExpressionType.Lambda => DeserializeLambda(node, entityType),
|
|
ExpressionType.Parameter => DeserializeParameter(node),
|
|
ExpressionType.Constant => DeserializeConstant(node),
|
|
ExpressionType.MemberAccess => DeserializeMemberAccess(node, entityType),
|
|
ExpressionType.Call => DeserializeMethodCall(node, entityType),
|
|
ExpressionType.Conditional => DeserializeConditional(node, entityType),
|
|
ExpressionType.New => DeserializeNew(node, entityType),
|
|
ExpressionType.MemberInit => DeserializeMemberInit(node, entityType),
|
|
ExpressionType.NewArrayInit or ExpressionType.NewArrayBounds => DeserializeNewArray(node, entityType),
|
|
ExpressionType.Invoke => DeserializeInvocation(node, entityType),
|
|
ExpressionType.TypeIs or ExpressionType.TypeAs => DeserializeTypeBinary(node, entityType),
|
|
|
|
// Unary expressions
|
|
ExpressionType.Not or ExpressionType.Negate or ExpressionType.NegateChecked or
|
|
ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.ArrayLength or
|
|
ExpressionType.Quote or ExpressionType.UnaryPlus
|
|
=> DeserializeUnary(node, entityType),
|
|
|
|
// Binary expressions
|
|
ExpressionType.Add or ExpressionType.AddChecked or ExpressionType.Subtract or
|
|
ExpressionType.SubtractChecked or ExpressionType.Multiply or ExpressionType.MultiplyChecked or
|
|
ExpressionType.Divide or ExpressionType.Modulo or ExpressionType.Power or
|
|
ExpressionType.And or ExpressionType.AndAlso or ExpressionType.Or or ExpressionType.OrElse or
|
|
ExpressionType.ExclusiveOr or ExpressionType.Equal or ExpressionType.NotEqual or
|
|
ExpressionType.LessThan or ExpressionType.LessThanOrEqual or
|
|
ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or
|
|
ExpressionType.Coalesce or ExpressionType.ArrayIndex or
|
|
ExpressionType.LeftShift or ExpressionType.RightShift
|
|
=> DeserializeBinary(node, entityType),
|
|
|
|
_ => throw new NotSupportedException($"Expression type '{node.NodeType}' is not supported.")
|
|
};
|
|
}
|
|
|
|
#region Deserialize Methods
|
|
|
|
private LambdaExpression DeserializeLambda(AcExpressionNode node, Type? entityType)
|
|
{
|
|
// Create parameters
|
|
var parameters = new List<ParameterExpression>();
|
|
if (node.Parameters != null)
|
|
{
|
|
foreach (var paramNode in node.Parameters)
|
|
{
|
|
var paramType = entityType ?? ResolveType(paramNode.TypeName);
|
|
var param = Expression.Parameter(paramType, paramNode.Name);
|
|
_parameters[paramNode.Index] = param;
|
|
parameters.Add(param);
|
|
|
|
// Use entityType only for first parameter
|
|
entityType = null;
|
|
}
|
|
}
|
|
|
|
var body = Deserialize(node.Body!, null);
|
|
return Expression.Lambda(body, parameters);
|
|
}
|
|
|
|
private ParameterExpression DeserializeParameter(AcExpressionNode node)
|
|
{
|
|
if (node.ParameterIndex.HasValue && _parameters.TryGetValue(node.ParameterIndex.Value, out var param))
|
|
return param;
|
|
|
|
throw new InvalidOperationException($"Parameter '{node.ParameterName}' not found.");
|
|
}
|
|
|
|
private static ConstantExpression DeserializeConstant(AcExpressionNode node)
|
|
{
|
|
var type = ResolveType(node.TypeName ?? "System.Object");
|
|
|
|
if (node.Value == null)
|
|
return Expression.Constant(null, type);
|
|
|
|
var value = JsonSerializer.Deserialize(node.Value, type, JsonOptions);
|
|
return Expression.Constant(value, type);
|
|
}
|
|
|
|
private Expression DeserializeMemberAccess(AcExpressionNode node, Type? entityType)
|
|
{
|
|
if (node.Object == null)
|
|
{
|
|
// Static member access
|
|
var declaringType = ResolveType(node.DeclaringType!);
|
|
var member = declaringType.GetMember(node.MemberName!, BindingFlags.Public | BindingFlags.Static).FirstOrDefault()
|
|
?? throw new InvalidOperationException($"Static member '{node.MemberName}' not found on type '{declaringType.Name}'.");
|
|
return Expression.MakeMemberAccess(null, member);
|
|
}
|
|
|
|
var obj = Deserialize(node.Object, entityType);
|
|
var memberInfo = obj.Type.GetMember(node.MemberName!, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase).FirstOrDefault()
|
|
?? throw new InvalidOperationException($"Member '{node.MemberName}' not found on type '{obj.Type.Name}'.");
|
|
|
|
return Expression.MakeMemberAccess(obj, memberInfo);
|
|
}
|
|
|
|
private Expression DeserializeMethodCall(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var arguments = node.Arguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
|
|
var argumentTypes = arguments.Select(a => a.Type).ToArray();
|
|
|
|
var declaringType = node.DeclaringType != null ? ResolveType(node.DeclaringType) : null;
|
|
var instance = node.Object != null ? Deserialize(node.Object, entityType) : null;
|
|
|
|
MethodInfo? method = null;
|
|
|
|
if (instance != null)
|
|
{
|
|
// Instance method
|
|
method = FindMethod(instance.Type, node.MethodName!, argumentTypes, isStatic: false);
|
|
}
|
|
else if (declaringType != null)
|
|
{
|
|
// Static method (including extension methods)
|
|
method = FindMethod(declaringType, node.MethodName!, argumentTypes, isStatic: true);
|
|
}
|
|
|
|
if (method == null)
|
|
throw new InvalidOperationException($"Method '{node.MethodName}' not found.");
|
|
|
|
// Handle generic methods
|
|
if (method.IsGenericMethodDefinition && node.GenericArguments?.Count > 0)
|
|
{
|
|
var genericTypes = node.GenericArguments.Select(ResolveType).ToArray();
|
|
method = method.MakeGenericMethod(genericTypes);
|
|
}
|
|
|
|
return instance != null
|
|
? Expression.Call(instance, method, arguments)
|
|
: Expression.Call(method, arguments);
|
|
}
|
|
|
|
private Expression DeserializeBinary(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var left = Deserialize(node.Left!, entityType);
|
|
var right = Deserialize(node.Right!, entityType);
|
|
|
|
// Handle type mismatches (e.g., nullable comparisons)
|
|
if (left.Type != right.Type)
|
|
{
|
|
if (Nullable.GetUnderlyingType(left.Type) == right.Type)
|
|
right = Expression.Convert(right, left.Type);
|
|
else if (Nullable.GetUnderlyingType(right.Type) == left.Type)
|
|
left = Expression.Convert(left, right.Type);
|
|
}
|
|
|
|
return Expression.MakeBinary(node.NodeType, left, right);
|
|
}
|
|
|
|
private Expression DeserializeUnary(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var operand = Deserialize(node.Operand!, entityType);
|
|
var type = node.TypeName != null ? ResolveType(node.TypeName) : null;
|
|
|
|
return node.NodeType switch
|
|
{
|
|
ExpressionType.Convert or ExpressionType.ConvertChecked when type != null
|
|
=> Expression.Convert(operand, type),
|
|
_ => Expression.MakeUnary(node.NodeType, operand, type)
|
|
};
|
|
}
|
|
|
|
private Expression DeserializeConditional(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var test = Deserialize(node.Test!, entityType);
|
|
var ifTrue = Deserialize(node.IfTrue!, entityType);
|
|
var ifFalse = Deserialize(node.IfFalse!, entityType);
|
|
return Expression.Condition(test, ifTrue, ifFalse);
|
|
}
|
|
|
|
private Expression DeserializeNew(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var type = ResolveType(node.TypeName!);
|
|
var args = node.ConstructorArguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
|
|
var argTypes = args.Select(a => a.Type).ToArray();
|
|
var ctor = type.GetConstructor(argTypes)
|
|
?? throw new InvalidOperationException($"Constructor not found for type '{type.Name}'.");
|
|
return Expression.New(ctor, args);
|
|
}
|
|
|
|
private Expression DeserializeMemberInit(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var type = ResolveType(node.TypeName!);
|
|
var args = node.ConstructorArguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
|
|
var argTypes = args.Select(a => a.Type).ToArray();
|
|
var ctor = type.GetConstructor(argTypes) ?? type.GetConstructor(Type.EmptyTypes)
|
|
?? throw new InvalidOperationException($"Constructor not found for type '{type.Name}'.");
|
|
|
|
var newExpr = Expression.New(ctor, args);
|
|
var bindings = node.MemberBindings?.Select(b => DeserializeMemberBinding(b, type, entityType)).ToList()
|
|
?? [];
|
|
|
|
return Expression.MemberInit(newExpr, bindings);
|
|
}
|
|
|
|
private MemberBinding DeserializeMemberBinding(MemberBindingNode node, Type declaringType, Type? entityType)
|
|
{
|
|
var member = declaringType.GetMember(node.MemberName, BindingFlags.Public | BindingFlags.Instance).FirstOrDefault()
|
|
?? throw new InvalidOperationException($"Member '{node.MemberName}' not found on type '{declaringType.Name}'.");
|
|
|
|
return node.BindingType switch
|
|
{
|
|
MemberBindingType.Assignment => Expression.Bind(member, Deserialize(node.Expression!, entityType)),
|
|
MemberBindingType.MemberBinding => Expression.MemberBind(member,
|
|
node.Bindings?.Select(b => DeserializeMemberBinding(b, GetMemberType(member), entityType)) ?? []),
|
|
MemberBindingType.ListBinding => Expression.ListBind(member,
|
|
node.Initializers?.Select(args => Expression.ElementInit(
|
|
GetAddMethod(GetMemberType(member)),
|
|
args.Select(a => Deserialize(a, entityType)))) ?? []),
|
|
_ => throw new NotSupportedException($"Binding type '{node.BindingType}' is not supported.")
|
|
};
|
|
}
|
|
|
|
private Expression DeserializeNewArray(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var elementType = ResolveType(node.TypeName!).GetElementType()
|
|
?? throw new InvalidOperationException("Cannot determine array element type.");
|
|
var elements = node.Elements?.Select(e => Deserialize(e, entityType)).ToArray() ?? [];
|
|
return Expression.NewArrayInit(elementType, elements);
|
|
}
|
|
|
|
private Expression DeserializeInvocation(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var expression = Deserialize(node.Object!, entityType);
|
|
var arguments = node.Arguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
|
|
return Expression.Invoke(expression, arguments);
|
|
}
|
|
|
|
private Expression DeserializeTypeBinary(AcExpressionNode node, Type? entityType)
|
|
{
|
|
var expression = Deserialize(node.Operand!, entityType);
|
|
var type = ResolveType(node.TypeName!);
|
|
return node.NodeType == ExpressionType.TypeIs
|
|
? Expression.TypeIs(expression, type)
|
|
: Expression.TypeAs(expression, type);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static MethodInfo? FindMethod(Type type, string methodName, Type[] argumentTypes, bool isStatic)
|
|
{
|
|
var bindingFlags = BindingFlags.Public | (isStatic ? BindingFlags.Static : BindingFlags.Instance);
|
|
|
|
// Try exact match first
|
|
var method = type.GetMethod(methodName, bindingFlags, null, argumentTypes, null);
|
|
if (method != null) return method;
|
|
|
|
// Try finding by name and parameter count
|
|
var candidates = type.GetMethods(bindingFlags)
|
|
.Where(m => m.Name == methodName && m.GetParameters().Length == argumentTypes.Length)
|
|
.ToList();
|
|
|
|
return candidates.FirstOrDefault();
|
|
}
|
|
|
|
private static Type GetMemberType(MemberInfo member) => member switch
|
|
{
|
|
PropertyInfo pi => pi.PropertyType,
|
|
FieldInfo fi => fi.FieldType,
|
|
_ => throw new InvalidOperationException($"Cannot get type for member '{member.Name}'.")
|
|
};
|
|
|
|
private static MethodInfo GetAddMethod(Type collectionType)
|
|
{
|
|
return collectionType.GetMethod("Add")
|
|
?? throw new InvalidOperationException($"Add method not found on type '{collectionType.Name}'.");
|
|
}
|
|
|
|
private static Type ResolveType(string typeName)
|
|
{
|
|
var type = typeName 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.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.DateOnly" => typeof(DateOnly),
|
|
"System.TimeOnly" => typeof(TimeOnly),
|
|
"System.TimeSpan" => typeof(TimeSpan),
|
|
"System.Guid" => typeof(Guid),
|
|
"System.Object" or "object" => typeof(object),
|
|
_ => Type.GetType(typeName)
|
|
};
|
|
|
|
if (type == null && typeName.Contains("Nullable"))
|
|
{
|
|
var match = System.Text.RegularExpressions.Regex.Match(typeName, @"System\.Nullable`1\[\[(.+?),");
|
|
if (match.Success)
|
|
{
|
|
var underlyingType = ResolveType(match.Groups[1].Value);
|
|
type = typeof(Nullable<>).MakeGenericType(underlyingType);
|
|
}
|
|
}
|
|
|
|
return type ?? throw new InvalidOperationException($"Cannot resolve type '{typeName}'.");
|
|
}
|
|
|
|
#endregion
|
|
}
|