430 lines
12 KiB
C#
430 lines
12 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>
|
|
/// Expression visitor that serializes an Expression tree to AcExpressionNode DTO.
|
|
/// Handles all common expression types recursively.
|
|
/// </summary>
|
|
public class AcExpressionSerializerVisitor : ExpressionVisitor
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
Converters = { new JsonStringEnumConverter() }
|
|
};
|
|
|
|
private readonly Dictionary<ParameterExpression, int> _parameterIndexes = new();
|
|
private int _nextParameterIndex;
|
|
|
|
// Stack to collect converted nodes
|
|
private readonly Stack<AcExpressionNode> _nodeStack = new();
|
|
|
|
/// <summary>
|
|
/// Converts an Expression to an AcExpressionNode DTO.
|
|
/// </summary>
|
|
public AcExpressionNode Convert(Expression expression)
|
|
{
|
|
_nodeStack.Clear();
|
|
_parameterIndexes.Clear();
|
|
_nextParameterIndex = 0;
|
|
|
|
VisitAndConvert(expression);
|
|
|
|
return _nodeStack.Count != 1 ? throw new InvalidOperationException($"Expected 1 node on stack, found {_nodeStack.Count}") : _nodeStack.Pop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serializes an Expression to JSON string.
|
|
/// </summary>
|
|
public string ToJson(Expression expression)
|
|
{
|
|
var node = Convert(expression);
|
|
return JsonSerializer.Serialize(node, JsonOptions);
|
|
}
|
|
|
|
private void VisitAndConvert(Expression expression)
|
|
{
|
|
Visit(expression);
|
|
}
|
|
|
|
protected override Expression VisitBinary(BinaryExpression node)
|
|
{
|
|
VisitAndConvert(node.Left);
|
|
var left = _nodeStack.Pop();
|
|
|
|
VisitAndConvert(node.Right);
|
|
var right = _nodeStack.Pop();
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = node.NodeType,
|
|
TypeName = node.Type.FullName,
|
|
Left = left,
|
|
Right = right
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitUnary(UnaryExpression node)
|
|
{
|
|
VisitAndConvert(node.Operand);
|
|
var operand = _nodeStack.Pop();
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = node.NodeType,
|
|
TypeName = node.Type.FullName,
|
|
Operand = operand
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitLambda<T>(Expression<T> node)
|
|
{
|
|
// Register parameters with indexes
|
|
var parameters = new List<ParameterNode>();
|
|
foreach (var param in node.Parameters)
|
|
{
|
|
var index = _nextParameterIndex++;
|
|
_parameterIndexes[param] = index;
|
|
parameters.Add(new ParameterNode
|
|
{
|
|
Name = param.Name ?? $"p{index}",
|
|
TypeName = param.Type.FullName ?? param.Type.Name,
|
|
Index = index
|
|
});
|
|
}
|
|
|
|
VisitAndConvert(node.Body);
|
|
var body = _nodeStack.Pop();
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.Lambda,
|
|
TypeName = node.Type.FullName,
|
|
Body = body,
|
|
Parameters = parameters
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitParameter(ParameterExpression node)
|
|
{
|
|
var index = _parameterIndexes.GetValueOrDefault(node, -1);
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.Parameter,
|
|
TypeName = node.Type.FullName,
|
|
ParameterName = node.Name,
|
|
ParameterIndex = index
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitMember(MemberExpression node)
|
|
{
|
|
// Check if this is a closure variable access (captured variable)
|
|
if (IsClosureAccess(node))
|
|
{
|
|
var value = EvaluateClosureValue(node);
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.Constant,
|
|
TypeName = node.Type.FullName,
|
|
Value = SerializeValue(value)
|
|
});
|
|
return node;
|
|
}
|
|
|
|
AcExpressionNode? objectNode = null;
|
|
if (node.Expression != null)
|
|
{
|
|
VisitAndConvert(node.Expression);
|
|
objectNode = _nodeStack.Pop();
|
|
}
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.MemberAccess,
|
|
TypeName = node.Type.FullName,
|
|
MemberName = node.Member.Name,
|
|
Object = objectNode,
|
|
DeclaringType = node.Member.DeclaringType?.FullName
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitConstant(ConstantExpression node)
|
|
{
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.Constant,
|
|
TypeName = node.Type.FullName,
|
|
Value = SerializeValue(node.Value)
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitMethodCall(MethodCallExpression node)
|
|
{
|
|
AcExpressionNode? objectNode = null;
|
|
if (node.Object != null)
|
|
{
|
|
VisitAndConvert(node.Object);
|
|
objectNode = _nodeStack.Pop();
|
|
}
|
|
|
|
var arguments = new List<AcExpressionNode>();
|
|
foreach (var arg in node.Arguments)
|
|
{
|
|
VisitAndConvert(arg);
|
|
arguments.Add(_nodeStack.Pop());
|
|
}
|
|
|
|
var genericArgs = node.Method.IsGenericMethod
|
|
? node.Method.GetGenericArguments().Select(t => t.FullName ?? t.Name).ToList()
|
|
: null;
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.Call,
|
|
TypeName = node.Type.FullName,
|
|
MethodName = node.Method.Name,
|
|
Object = objectNode,
|
|
Arguments = arguments,
|
|
DeclaringType = node.Method.DeclaringType?.FullName,
|
|
GenericArguments = genericArgs
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitConditional(ConditionalExpression node)
|
|
{
|
|
VisitAndConvert(node.Test);
|
|
var test = _nodeStack.Pop();
|
|
|
|
VisitAndConvert(node.IfTrue);
|
|
var ifTrue = _nodeStack.Pop();
|
|
|
|
VisitAndConvert(node.IfFalse);
|
|
var ifFalse = _nodeStack.Pop();
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.Conditional,
|
|
TypeName = node.Type.FullName,
|
|
Test = test,
|
|
IfTrue = ifTrue,
|
|
IfFalse = ifFalse
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitNew(NewExpression node)
|
|
{
|
|
var arguments = new List<AcExpressionNode>();
|
|
foreach (var arg in node.Arguments)
|
|
{
|
|
VisitAndConvert(arg);
|
|
arguments.Add(_nodeStack.Pop());
|
|
}
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.New,
|
|
TypeName = node.Type.FullName,
|
|
ConstructorArguments = arguments
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitMemberInit(MemberInitExpression node)
|
|
{
|
|
var arguments = new List<AcExpressionNode>();
|
|
foreach (var arg in node.NewExpression.Arguments)
|
|
{
|
|
VisitAndConvert(arg);
|
|
arguments.Add(_nodeStack.Pop());
|
|
}
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.MemberInit,
|
|
TypeName = node.Type.FullName,
|
|
ConstructorArguments = arguments,
|
|
MemberBindings = node.Bindings.Select(ConvertMemberBinding).ToList()
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitNewArray(NewArrayExpression node)
|
|
{
|
|
var elements = new List<AcExpressionNode>();
|
|
foreach (var expr in node.Expressions)
|
|
{
|
|
VisitAndConvert(expr);
|
|
elements.Add(_nodeStack.Pop());
|
|
}
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = node.NodeType,
|
|
TypeName = node.Type.FullName,
|
|
Elements = elements
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
|
|
{
|
|
VisitAndConvert(node.Expression);
|
|
var operand = _nodeStack.Pop();
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = node.NodeType,
|
|
TypeName = node.TypeOperand.FullName,
|
|
Operand = operand
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
protected override Expression VisitInvocation(InvocationExpression node)
|
|
{
|
|
VisitAndConvert(node.Expression);
|
|
var objectNode = _nodeStack.Pop();
|
|
|
|
var arguments = new List<AcExpressionNode>();
|
|
foreach (var arg in node.Arguments)
|
|
{
|
|
VisitAndConvert(arg);
|
|
arguments.Add(_nodeStack.Pop());
|
|
}
|
|
|
|
_nodeStack.Push(new AcExpressionNode
|
|
{
|
|
NodeType = ExpressionType.Invoke,
|
|
TypeName = node.Type.FullName,
|
|
Object = objectNode,
|
|
Arguments = arguments
|
|
});
|
|
|
|
return node;
|
|
}
|
|
|
|
#region Helper Methods
|
|
|
|
private MemberBindingNode ConvertMemberBinding(MemberBinding binding)
|
|
{
|
|
return binding switch
|
|
{
|
|
MemberAssignment assignment => ConvertMemberAssignment(assignment),
|
|
MemberMemberBinding memberBinding => new MemberBindingNode
|
|
{
|
|
MemberName = memberBinding.Member.Name,
|
|
BindingType = MemberBindingType.MemberBinding,
|
|
Bindings = memberBinding.Bindings.Select(ConvertMemberBinding).ToList()
|
|
},
|
|
MemberListBinding listBinding => new MemberBindingNode
|
|
{
|
|
MemberName = listBinding.Member.Name,
|
|
BindingType = MemberBindingType.ListBinding,
|
|
Initializers = listBinding.Initializers
|
|
.Select(i => i.Arguments.Select(ConvertArgument).ToList())
|
|
.ToList()
|
|
},
|
|
_ => throw new NotSupportedException($"Member binding type '{binding.BindingType}' is not supported.")
|
|
};
|
|
}
|
|
|
|
private MemberBindingNode ConvertMemberAssignment(MemberAssignment assignment)
|
|
{
|
|
VisitAndConvert(assignment.Expression);
|
|
var expr = _nodeStack.Pop();
|
|
|
|
return new MemberBindingNode
|
|
{
|
|
MemberName = assignment.Member.Name,
|
|
BindingType = MemberBindingType.Assignment,
|
|
Expression = expr
|
|
};
|
|
}
|
|
|
|
private AcExpressionNode ConvertArgument(Expression expression)
|
|
{
|
|
VisitAndConvert(expression);
|
|
return _nodeStack.Pop();
|
|
}
|
|
|
|
private static bool IsClosureAccess(MemberExpression node)
|
|
{
|
|
return node.Expression switch
|
|
{
|
|
ConstantExpression => true,
|
|
MemberExpression nested => IsClosureAccess(nested),
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
private static object? EvaluateClosureValue(MemberExpression node)
|
|
{
|
|
var objectStack = new Stack<MemberExpression>();
|
|
Expression? current = node;
|
|
|
|
while (current is MemberExpression me)
|
|
{
|
|
objectStack.Push(me);
|
|
current = me.Expression;
|
|
}
|
|
|
|
if (current is not ConstantExpression constant)
|
|
throw new InvalidOperationException("Expected constant at root of closure access.");
|
|
|
|
object? value = constant.Value;
|
|
|
|
while (objectStack.Count > 0)
|
|
{
|
|
var me = objectStack.Pop();
|
|
value = me.Member switch
|
|
{
|
|
FieldInfo fi => fi.GetValue(value),
|
|
PropertyInfo pi => pi.GetValue(value),
|
|
_ => throw new InvalidOperationException($"Unsupported member type: {me.Member.GetType()}")
|
|
};
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
private static string? SerializeValue(object? value)
|
|
{
|
|
if (value == null) return null;
|
|
|
|
// Handle IQueryable source - serialize as placeholder
|
|
if (value is IQueryable)
|
|
return null;
|
|
|
|
return JsonSerializer.Serialize(value, JsonOptions);
|
|
}
|
|
|
|
#endregion
|
|
}
|