AyCode.Blazor/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionHelper.cs

239 lines
8.4 KiB
C#

using System.Linq.Expressions;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AyCode.Blazor.Components.Services.ExpressionHelpers;
/// <summary>
/// Helper class for serializing and deserializing Expression trees and IQueryable queries.
/// Uses visitor pattern to handle all expression types automatically.
/// </summary>
public static class AcExpressionHelper
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
#region Expression Serialization
/// <summary>
/// Serializes an Expression to AcExpressionNode DTO.
/// </summary>
public static AcExpressionNode ExpressionToNode(Expression expression)
{
var visitor = new AcExpressionSerializerVisitor();
return visitor.Convert(expression);
}
/// <summary>
/// Serializes an Expression to JSON string.
/// </summary>
public static string ExpressionToJson(Expression expression)
{
var node = ExpressionToNode(expression);
return JsonSerializer.Serialize(node, JsonOptions);
}
/// <summary>
/// Serializes a typed Expression to JSON string.
/// </summary>
public static string ExpressionToJson<TEntity, TResult>(Expression<Func<TEntity, TResult>> expression)
{
return ExpressionToJson((Expression)expression);
}
#endregion
#region Expression Deserialization
/// <summary>
/// Deserializes AcExpressionNode DTO to Expression.
/// </summary>
public static Expression ExpressionFromNode(AcExpressionNode node, Type? entityType = null)
{
var deserializer = new AcExpressionDeserializer();
return deserializer.Deserialize(node, entityType);
}
/// <summary>
/// Deserializes JSON to Expression.
/// </summary>
public static Expression ExpressionFromJson(string json, Type? entityType = null)
{
return AcExpressionDeserializer.ExpressionFromJson(json, entityType);
}
/// <summary>
/// Deserializes JSON to typed Expression.
/// </summary>
public static Expression<Func<TEntity, TResult>> ExpressionFromJson<TEntity, TResult>(string json)
{
return AcExpressionDeserializer.ExpressionFromJson<TEntity, TResult>(json);
}
#endregion
#region IQueryable Serialization
/// <summary>
/// Serializes an IQueryable's expression tree to JSON.
/// </summary>
public static string QueryToJson<T>(IQueryable<T> query)
{
return ExpressionToJson(query.Expression);
}
/// <summary>
/// Serializes an IQueryable's expression tree to AcExpressionNode.
/// </summary>
public static AcExpressionNode QueryToNode<T>(IQueryable<T> query)
{
return ExpressionToNode(query.Expression);
}
#endregion
#region IQueryable Deserialization
/// <summary>
/// Applies a serialized query expression to an IQueryable source.
/// </summary>
public static IQueryable<T> ApplyQueryFromJson<T>(IQueryable<T> source, string json)
{
var node = JsonSerializer.Deserialize<AcExpressionNode>(json, JsonOptions)
?? throw new ArgumentException("Invalid query JSON", nameof(json));
return ApplyQueryFromNode(source, node);
}
/// <summary>
/// Applies an AcExpressionNode query to an IQueryable source.
/// </summary>
public static IQueryable<T> ApplyQueryFromNode<T>(IQueryable<T> 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<T>(expression);
}
/// <summary>
/// Rebuilds a query expression, replacing the source with the provided expression.
/// </summary>
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.");
}
/// <summary>
/// Rebuilds a chain of method calls (Where, OrderBy, Skip, Take, etc.)
/// </summary>
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<Expression> { 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<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
}