using System.Linq.Expressions; using System.Text.Json; using System.Text.Json.Serialization; namespace AyCode.Blazor.Components.Services.ExpressionHelpers; /// /// Helper class for serializing and deserializing Expression trees and IQueryable queries. /// Uses visitor pattern to handle all expression types automatically. /// public static class AcExpressionHelper { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new JsonStringEnumConverter() } }; #region Expression Serialization /// /// Serializes an Expression to AcExpressionNode DTO. /// public static AcExpressionNode ExpressionToNode(Expression expression) { var visitor = new AcExpressionSerializerVisitor(); return visitor.Convert(expression); } /// /// Serializes an Expression to JSON string. /// public static string ExpressionToJson(Expression expression) { var node = ExpressionToNode(expression); return JsonSerializer.Serialize(node, JsonOptions); } /// /// Serializes a typed Expression to JSON string. /// public static string ExpressionToJson(Expression> expression) { return ExpressionToJson((Expression)expression); } #endregion #region Expression Deserialization /// /// Deserializes AcExpressionNode DTO to Expression. /// public static Expression ExpressionFromNode(AcExpressionNode node, Type? entityType = null) { var deserializer = new AcExpressionDeserializer(); return deserializer.Deserialize(node, entityType); } /// /// Deserializes JSON to Expression. /// public static Expression ExpressionFromJson(string json, Type? entityType = null) { return AcExpressionDeserializer.ExpressionFromJson(json, entityType); } /// /// Deserializes JSON to typed Expression. /// public static Expression> ExpressionFromJson(string json) { return AcExpressionDeserializer.ExpressionFromJson(json); } #endregion #region IQueryable Serialization /// /// Serializes an IQueryable's expression tree to JSON. /// public static string QueryToJson(IQueryable query) { return ExpressionToJson(query.Expression); } /// /// Serializes an IQueryable's expression tree to AcExpressionNode. /// public static AcExpressionNode QueryToNode(IQueryable query) { return ExpressionToNode(query.Expression); } #endregion #region IQueryable Deserialization /// /// Applies a serialized query expression to an IQueryable source. /// public static IQueryable ApplyQueryFromJson(IQueryable source, string json) { var node = JsonSerializer.Deserialize(json, JsonOptions) ?? throw new ArgumentException("Invalid query JSON", nameof(json)); return ApplyQueryFromNode(source, node); } /// /// Applies an AcExpressionNode query to an IQueryable source. /// public static IQueryable ApplyQueryFromNode(IQueryable source, AcExpressionNode node) { // If the node is a method call (Where, OrderBy, etc.), we need to rebuild it // with the source expression replaced var expression = RebuildQueryExpression(source.Expression, node, typeof(T)); return source.Provider.CreateQuery(expression); } /// /// Rebuilds a query expression, replacing the source with the provided expression. /// private static Expression RebuildQueryExpression(Expression sourceExpression, AcExpressionNode node, Type entityType) { if (node is { NodeType: ExpressionType.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 == ExpressionType.Lambda) { var deserializer = new AcExpressionDeserializer(); var lambda = deserializer.Deserialize(node, entityType); var whereMethod = typeof(Queryable).GetMethods() .First(m => m.Name == "Where" && m.GetParameters().Length == 2) .MakeGenericMethod(entityType); return Expression.Call(whereMethod, sourceExpression, lambda); } throw new NotSupportedException($"Cannot apply expression of type '{node.NodeType}' to IQueryable."); } /// /// Rebuilds a chain of method calls (Where, OrderBy, Skip, Take, etc.) /// private static Expression RebuildMethodCallChain(Expression sourceExpression, AcExpressionNode node, Type entityType) { // First, process the inner expression (the source of this method call) Expression currentSource; if (node.Arguments?.Count > 0 && node.Arguments[0].NodeType == ExpressionType.Call) { // Recursive: rebuild the inner chain first currentSource = RebuildMethodCallChain(sourceExpression, node.Arguments[0], entityType); } else { // Base case: use the provided source currentSource = sourceExpression; } // Now apply this method call var methodName = node.MethodName!; var declaringType = node.DeclaringType != null ? Type.GetType(node.DeclaringType) : typeof(Queryable); // Find the method var method = FindQueryableMethod(declaringType!, methodName, node.GenericArguments, node.Arguments?.Count ?? 1); if (method == null) throw new InvalidOperationException($"Method '{methodName}' not found."); // Apply generic type arguments if (method.IsGenericMethodDefinition && node.GenericArguments?.Count > 0) { var genericTypes = node.GenericArguments.Select(t => Type.GetType(t) ?? entityType).ToArray(); method = method.MakeGenericMethod(genericTypes); } // Build arguments var deserializer = new AcExpressionDeserializer(); var arguments = new List { currentSource }; // Skip first argument (it's the source) and deserialize the rest if (node.Arguments?.Count > 1) { for (var i = 1; i < node.Arguments.Count; i++) { var argNode = node.Arguments[i]; if (argNode.NodeType == ExpressionType.Quote && argNode.Operand != null) { // Quoted lambda - unquote and deserialize var lambda = deserializer.Deserialize(argNode.Operand, entityType); arguments.Add(Expression.Quote(lambda)); } else if (argNode.NodeType == ExpressionType.Lambda) { var lambda = deserializer.Deserialize(argNode, entityType); arguments.Add(Expression.Quote(lambda)); } else { arguments.Add(deserializer.Deserialize(argNode, entityType)); } } } return Expression.Call(method, arguments); } private static System.Reflection.MethodInfo? FindQueryableMethod(Type declaringType, string methodName, List? 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 }