239 lines
8.4 KiB
C#
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
|
|
}
|