英文:
Recursive Expression Tree Parsing With Sprache
问题
I've translated the code you provided as requested. Here it is:
public static class FilterParser
{
internal static Expression<Func<T, bool>> ParseFilter<T>(string input, IQueryKitConfiguration? config = null)
{
var parameter = Expression.Parameter(typeof(T), "x");
Expression expr;
try
{
expr = ExprParser<T>(parameter, config).End().Parse(input);
}
catch (ParseException e)
{
throw new ParsingException(e);
}
return Expression.Lambda<Func<T, bool>>(expr, parameter);
}
private static readonly Parser<string> Identifier =
from first in Parse.Letter.Once()
from rest in Parse.LetterOrDigit.XOr(Parse.Char('_')).Many()
select new string(first.Concat(rest).ToArray());
// Other parser methods...
private static Parser<Expression?>? CreateLeftExprParser(ParameterExpression parameter, IQueryKitConfiguration? config)
{
var leftIdentifierParser = Identifier.DelimitedBy(Parse.Char('.')).Token();
return leftIdentifierParser?.Select(left =>
{
// Implementation of CreateLeftExprParser...
});
}
// Other parser methods...
private static Expression CreateRightExpr(Expression leftExpr, string right)
{
var targetType = leftExpr.Type;
targetType = TransformTargetTypeIfNullable(targetType);
if (TypeConversionFunctions.TryGetValue(targetType, out var conversionFunction))
{
// Implementation of CreateRightExpr...
}
throw new InvalidOperationException($"Unsupported value '{right}' for type '{targetType.Name}'");
}
// Rest of the code...
}
Please note that I've omitted some of the repetitive parser method implementations for brevity. If you need any specific part translated or have questions about a particular section, please let me know.
英文:
so i have a parser that uses use sprache to create an expression from a string. something like PhysicalAddress.State == "GA"
has PhysicalAddress.State
parsed as the left, ==
as the operator, and "GA"
as the right expression. the parts can then be combined to an expression for use in a queryable like x => (x.PhysicalAddress.State == "GA")
.
i want to expand it to be able to dynamically handle ienumerables when building expressions like in the test below. i started working on the below for checking if i have an ienumerable (in CreateLeftExprParser
surrounded by WIP
comment), but i'm stuck on how to get the inner expression for the any. My first thought was to recursively call ParseFiler()
to get the inner expression, but that doesn't really fly since it needs the raw string to go on. maybe i could amke an overload that takes the comparison and right tokens?
regardless, open to thoughts and ideas. this is a tricky one.
[Fact]
public void simple_with_operator_for_string()
{
var input = """Ingredients.Name != "flour" """;
var filterExpression = FilterParser.ParseFilter<Recipe>(input);
filterExpression.ToString().Should()
.Be(""""x => x.Ingredients.Any(y => (y.Name != \"flour\"))"""");
}
public static class FilterParser
{
internal static Expression<Func<T, bool>> ParseFilter<T>(string input, IQueryKitConfiguration? config = null)
{
var parameter = Expression.Parameter(typeof(T), "x");
Expression expr;
try
{
expr = ExprParser<T>(parameter, config).End().Parse(input);
}
catch (ParseException e)
{
throw new ParsingException(e);
}
return Expression.Lambda<Func<T, bool>>(expr, parameter);
}
private static readonly Parser<string> Identifier =
from first in Parse.Letter.Once()
from rest in Parse.LetterOrDigit.XOr(Parse.Char('_')).Many()
select new string(first.Concat(rest).ToArray());
private static Parser<ComparisonOperator> ComparisonOperatorParser
{
get
{
var parsers = Parse.String(ComparisonOperator.EqualsOperator().Operator()).Text()
.Or(Parse.String(ComparisonOperator.NotEqualsOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.GreaterThanOrEqualOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.LessThanOrEqualOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.GreaterThanOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.LessThanOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.ContainsOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.StartsWithOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.EndsWithOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.NotContainsOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.NotStartsWithOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.NotEndsWithOperator().Operator()).Text())
.Or(Parse.String(ComparisonOperator.InOperator().Operator()).Text())
.SelectMany(op => Parse.Char('*').Optional(), (op, caseInsensitive) => new { op, caseInsensitive });
var compOperator = parsers
.Select(x => ComparisonOperator.GetByOperatorString(x.op, x.caseInsensitive.IsDefined));
return compOperator;
}
}
private static Parser<Expression> ComparisonExprParser<T>(ParameterExpression parameter, IQueryKitConfiguration? config)
{
var comparisonOperatorParser = ComparisonOperatorParser.Token();
var rightSideValueParser = RightSideValueParser.Token();
return CreateLeftExprParser(parameter, config)
.SelectMany(leftExpr => comparisonOperatorParser, (leftExpr, op) => new { leftExpr, op })
.SelectMany(temp => rightSideValueParser, (temp, right) => new { temp.leftExpr, temp.op, right })
.Select(temp =>
{
if (temp.leftExpr.NodeType == ExpressionType.Constant && ((ConstantExpression)temp.leftExpr).Value!.Equals(true))
{
return Expression.Equal(Expression.Constant(true), Expression.Constant(true));
}
var rightExpr = CreateRightExpr(temp.leftExpr, temp.right);
return temp.op.GetExpression<T>(temp.leftExpr, rightExpr);
});
}
private static Parser<Expression?>? CreateLeftExprParser(ParameterExpression parameter, IQueryKitConfiguration? config)
{
var leftIdentifierParser = Identifier.DelimitedBy(Parse.Char('.')).Token();
return leftIdentifierParser?.Select(left =>
{
// If only a single identifier is present
var leftList = left.ToList();
if (leftList.Count == 1)
{
var propName = leftList?.First();
var fullPropPath = config?.GetPropertyPathByQueryName(propName) ?? propName;
var propNames = fullPropPath?.Split('.');
var propertyExpression = propNames?.Aggregate((Expression)parameter, (expr, pn) =>
{
var propertyInfo = GetPropertyInfo(expr.Type, pn);
var actualPropertyName = propertyInfo?.Name ?? pn;
try
{
return Expression.PropertyOrField(expr, actualPropertyName);
}
catch(ArgumentException)
{
throw new UnknownFilterPropertyException(actualPropertyName);
}
});
var propertyConfig = config?.PropertyMappings?.GetPropertyInfo(fullPropPath);
if (propertyConfig != null && !propertyConfig.CanFilter)
{
return Expression.Constant(true, typeof(bool));
}
return propertyExpression;
}
// If the property is nested with a dot ('.') separator
var nestedPropertyExpression = leftList.Aggregate((Expression)parameter, (expr, propName) =>
{
var propertyInfo = GetPropertyInfo(expr.Type, propName);
// ------- WIP for IEnumerable Handling
if (propertyInfo != null && typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType) && propertyInfo.PropertyType != typeof(string))
{
// Create a parameter for the lambda in the Any method
var anyParameter = Expression.Parameter(propertyInfo.PropertyType.GetGenericArguments().First(), "y");
// Get the rest of the property path
var subProperties = leftList.Skip(leftList.IndexOf(propName) + 1);
// Get the rest of the input for the recursive call
var restOfInput = string.Join(".", subProperties);
var anyLambda = ????
// Create the Any method call
var anyMethod = typeof(Enumerable).GetMethods().First(m => m.Name == "Any" && m.GetParameters().Length == 2);
var genericAnyMethod = anyMethod.MakeGenericMethod(anyParameter.Type);
return Expression.Call(genericAnyMethod, expr, anyLambda);
}
// --------- End WIP
var mappedPropertyInfo = config?.PropertyMappings?.GetPropertyInfoByQueryName(propName);
var actualPropertyName = mappedPropertyInfo?.Name ?? propertyInfo?.Name ?? propName;
try
{
return Expression.PropertyOrField(expr, actualPropertyName);
}
catch(ArgumentException)
{
throw new UnknownFilterPropertyException(actualPropertyName);
}
});
var nestedPropertyConfig = config?.PropertyMappings?.GetPropertyInfo(leftList.Last());
if (nestedPropertyConfig != null && !nestedPropertyConfig.CanFilter)
{
return Expression.Constant(true, typeof(bool));
}
return nestedPropertyExpression;
});
}
private static PropertyInfo? GetPropertyInfo(Type type, string propertyName)
{
return type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
}
public static Parser<LogicalOperator> LogicalOperatorParser =>
from leadingSpaces in Parse.WhiteSpace.Many()
from op in Parse.String(LogicalOperator.AndOperator.Operator()).Text().Or(Parse.String(LogicalOperator.OrOperator.Operator()).Text())
from trailingSpaces in Parse.WhiteSpace.Many()
select LogicalOperator.GetByOperatorString(op);
private static Parser<string> DoubleQuoteParser
=> Parse.Char('"').Then(_ => Parse.AnyChar.Except(Parse.Char('"')).Many().Text().Then(innerValue => Parse.Char('"').Return(innerValue)));
private static Parser<string> TimeFormatParser => Parse.Regex(@"\d{2}:\d{2}:\d{2}").Text();
private static Parser<string> DateTimeFormatParser =>
from dateFormat in Parse.Regex(@"\d{4}-\d{2}-\d{2}").Text()
from timeFormat in Parse.Regex(@"T\d{2}:\d{2}:\d{2}").Text().Optional().Select(x => x.GetOrElse(""))
from timeZone in Parse.Regex(@"(Z|[+-]\d{2}(:\d{2})?)").Text().Optional().Select(x => x.GetOrElse(""))
from millis in Parse.Regex(@"\.\d{3}").Text().Optional().Select(x => x.GetOrElse(""))
select dateFormat + timeFormat + timeZone + millis;
private static Parser<string> GuidFormatParser => Parse.Regex(@"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}").Text();
private static Parser<string> RawStringLiteralParser =>
from openingQuotes in Parse.Regex("\"{3,}").Text()
let count = openingQuotes.Length
from content in Parse.AnyChar.Except(Parse.Char('"').Repeat(count)).Many().Text()
from closingQuotes in Parse.Char('"').Repeat(count).Text()
select content;
private static Parser<string> RightSideValueParser =>
from atSign in Parse.Char('@').Optional()
from leadingSpaces in Parse.WhiteSpace.Many()
from value in Parse.String("null").Text()
.Or(GuidFormatParser)
.XOr(Identifier)
.XOr(DateTimeFormatParser)
.XOr(TimeFormatParser)
.XOr(Parse.Decimal)
.XOr(Parse.Number)
.XOr(RawStringLiteralParser.Or(DoubleQuoteParser))
.XOr(SquareBracketParser)
from trailingSpaces in Parse.WhiteSpace.Many()
select atSign.IsDefined ? "@" + value : value;
private static Parser<string> SquareBracketParser =>
from openingBracket in Parse.Char('[')
from content in DoubleQuoteParser
.Or(GuidFormatParser)
.Or(Parse.Decimal)
.Or(Parse.Number)
.Or(Identifier)
.DelimitedBy(Parse.Char(',').Token())
from closingBracket in Parse.Char(']')
select "[" + string.Join(",", content) + "]";
private static Parser<Expression> AtomicExprParser<T>(ParameterExpression parameter, IQueryKitConfiguration? config = null)
=> ComparisonExprParser<T>(parameter, config)
.Or(Parse.Ref(() => ExprParser<T>(parameter, config)).Contained(Parse.Char('('), Parse.Char(')')));
private static Parser<Expression> ExprParser<T>(ParameterExpression parameter, IQueryKitConfiguration? config = null)
=> OrExprParser<T>(parameter, config);
private static readonly Dictionary<Type, Func<string, object>> TypeConversionFunctions = new()
{
{ typeof(string), value => value },
{ typeof(bool), value => bool.Parse(value) },
{ typeof(Guid), value => Guid.Parse(value) },
{ typeof(char), value => char.Parse(value) },
{ typeof(int), value => int.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(float), x => float.Parse(x, CultureInfo.InvariantCulture) },
{ typeof(double), x => double.Parse(x, CultureInfo.InvariantCulture) },
{ typeof(decimal), x => decimal.Parse(x, CultureInfo.InvariantCulture) },
{ typeof(long), value => long.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(short), value => short.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(byte), value => byte.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(DateTime), value => DateTime.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(DateTimeOffset), value => DateTimeOffset.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(DateOnly), value => DateOnly.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(TimeOnly), value => TimeOnly.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(TimeSpan), value => TimeSpan.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(uint), value => uint.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(ulong), value => ulong.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(ushort), value => ushort.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(sbyte), value => sbyte.Parse(value, CultureInfo.InvariantCulture) },
// { typeof(Enum), value => Enum.Parse(typeof(T), value) },
};
private static Expression CreateRightExpr(Expression leftExpr, string right)
{
var targetType = leftExpr.Type;
targetType = TransformTargetTypeIfNullable(targetType);
if (TypeConversionFunctions.TryGetValue(targetType, out var conversionFunction))
{
if (right == "null")
{
return Expression.Constant(null, leftExpr.Type);
}
if (right.StartsWith("[") && right.EndsWith("]"))
{
var values = right.Trim('[', ']').Split(',').Select(x => x.Trim()).ToList();
var elementType = targetType.IsArray ? targetType.GetElementType() : targetType;
var expressions = values.Select(x =>
{
if (elementType == typeof(string) && x.StartsWith("\"") && x.EndsWith("\""))
{
x = x.Trim('"');
}
var convertedValue = TypeConversionFunctions[elementType](x);
return Expression.Constant(convertedValue, elementType);
}).ToArray();
var newArrayExpression = Expression.NewArrayInit(elementType, expressions);
return newArrayExpression;
}
if (targetType == typeof(string))
{
right = right.Trim('"');
}
var convertedValue = conversionFunction(right);
if (targetType == typeof(Guid))
{
var guidParseMethod = typeof(Guid).GetMethod("Parse", new[] { typeof(string) });
return Expression.Call(guidParseMethod, Expression.Constant(convertedValue.ToString(), typeof(string)));
}
return Expression.Constant(convertedValue, leftExpr.Type);
}
throw new InvalidOperationException($"Unsupported value '{right}' for type '{targetType.Name}'");
}
private static Type TransformTargetTypeIfNullable(Type targetType)
{
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
targetType = Nullable.GetUnderlyingType(targetType);
}
return targetType;
}
private static Parser<Expression> AndExprParser<T>(ParameterExpression parameter, IQueryKitConfiguration? config = null)
=> Parse.ChainOperator(
LogicalOperatorParser.Where(x => x.Name == LogicalOperator.AndOperator.Operator()),
AtomicExprParser<T>(parameter, config),
(op, left, right) => op.GetExpression<T>(left, right)
);
private static Parser<Expression> OrExprParser<T>(ParameterExpression parameter, IQueryKitConfiguration? config = null)
=> Parse.ChainOperator(
LogicalOperatorParser.Where(x => x.Name == LogicalOperator.OrOperator.Operator()),
AndExprParser<T>(parameter, config),
(op, left, right) => op.GetExpression<T>(left, right)
);
}
答案1
得分: 1
我的第一个想法是重新编写解析器,不直接解析为 Expression
,而是使用中间的AST。然后,可以将此AST转换为 Expression
。这将把检查可枚举项和构建Any表达式的问题移到了转换阶段,在此阶段,您可以根据树的结构自由访问子项/父项/兄弟项等,而不受Expression
的限制,您可以创建与您想要解决的问题匹配的结构。
如果创建一个中间结构不是一个选项,那么我会返回一个存根Any表达式(例如,Any(y => true)),然后在您的ComparisonExprParser
方法中检查是否存在这样的存根,然后在最终的Select(temp => ...)
中构建正确的Any表达式,据我所知,您在这里拥有所有所需的信息。
英文:
My first thought would be to rewrite the parser to not parse directly to Expression
, but have an intermediary AST. This AST could then be transformed into Expression
. This will move the problem of checking for enumerables and constructing the Any-expression to this transform phase where you will have access to children/parents/siblings and so on based on how you structured your tree - and without the constraints of Expression
you are free to make a structure that matches the problem, you would like to solve.
If making an intermediary is not an option, then I'd return a stub Any-expression (e.g. Any(y => true)) and then check for such stub in your ComparisonExprParser
method - and then build the correct Any-expression in your final Select(temp => ...)
where you - as far as I can tell - have all the needed information.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论