Skip to content

Commit a103a51

Browse files
committed
JsonConverter implementation; Implementations and extensions to the DynamicLinq filters (converts from and to Filter); improving tests
1 parent a1b540e commit a103a51

File tree

18 files changed

+2781
-15
lines changed

18 files changed

+2781
-15
lines changed

Deveel.Filters.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{606D3076-7
2323
EndProject
2424
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiltersBenchmark", "test\FiltersBenchmark\FiltersBenchmark.csproj", "{1247AF41-1F85-4A82-9202-941150DEA649}"
2525
EndProject
26+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Filters.DynamicLinq.XUnit", "test\Deveel.Filters.DynamicLinq.XUnit\Deveel.Filters.DynamicLinq.XUnit.csproj", "{02BBF660-1703-4A52-A85E-0DD4BCA562BD}"
27+
EndProject
2628
Global
2729
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2830
Debug|Any CPU = Debug|Any CPU
@@ -61,6 +63,10 @@ Global
6163
{1247AF41-1F85-4A82-9202-941150DEA649}.Debug|Any CPU.Build.0 = Debug|Any CPU
6264
{1247AF41-1F85-4A82-9202-941150DEA649}.Release|Any CPU.ActiveCfg = Release|Any CPU
6365
{1247AF41-1F85-4A82-9202-941150DEA649}.Release|Any CPU.Build.0 = Release|Any CPU
66+
{02BBF660-1703-4A52-A85E-0DD4BCA562BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67+
{02BBF660-1703-4A52-A85E-0DD4BCA562BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
68+
{02BBF660-1703-4A52-A85E-0DD4BCA562BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
69+
{02BBF660-1703-4A52-A85E-0DD4BCA562BD}.Release|Any CPU.Build.0 = Release|Any CPU
6470
EndGlobalSection
6571
GlobalSection(SolutionProperties) = preSolution
6672
HideSolutionNode = FALSE
@@ -74,6 +80,7 @@ Global
7480
{BF5581E9-C71C-466D-81F0-50BAD6A6627E} = {ED0B8843-6F66-42BA-BE86-E5F2305BF4AF}
7581
{9E6A5BBA-45E0-4820-8647-9C6E2AA7777A} = {606D3076-7E5C-4536-84AD-26D8670381ED}
7682
{1247AF41-1F85-4A82-9202-941150DEA649} = {606D3076-7E5C-4536-84AD-26D8670381ED}
83+
{02BBF660-1703-4A52-A85E-0DD4BCA562BD} = {606D3076-7E5C-4536-84AD-26D8670381ED}
7784
EndGlobalSection
7885
GlobalSection(ExtensibilityGlobals) = postSolution
7986
SolutionGuid = {5254BD5D-FD17-421A-B033-5CA8EDEB2E50}

src/Deveel.Filter.DynamicLinq/Deveel.Filter.DynamicLinq.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<RootNamespace>Deveel</RootNamespace>
88
<IsPackable>false</IsPackable>
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.2" />
12+
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.6" />
1313
</ItemGroup>
1414

1515
<ItemGroup>
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
using System.Linq.Dynamic.Core;
2+
using System.Linq.Expressions;
3+
4+
namespace Deveel.Filters
5+
{
6+
/// <summary>
7+
/// Provides functionality to parse string expressions into Filter objects using DynamicExpressionParser.
8+
/// </summary>
9+
public static class FilterExpressionParser
10+
{
11+
public static readonly ParsingConfig DefaultParsingConfig = new ParsingConfig
12+
{
13+
// Default configuration can be customized here if needed
14+
// e.g., CustomTypeProvider, etc.
15+
};
16+
17+
/// <summary>
18+
/// Parses a string expression into a Filter object.
19+
/// </summary>
20+
/// <param name="expression">The string expression to parse (e.g., "Name == \"John\" && Age > 25")</param>
21+
/// <param name="parameterType">The type of the parameter in the expression</param>
22+
/// <param name="parameterName">The name of the parameter (default: "x")</param>
23+
/// <param name="config">Optional parsing configuration</param>
24+
/// <returns>A Filter object representing the parsed expression</returns>
25+
/// <exception cref="ArgumentException">Thrown when the expression is null or empty</exception>
26+
/// <exception cref="FilterException">Thrown when the expression cannot be parsed or converted</exception>
27+
public static Filter Parse(string expression, Type parameterType, string parameterName = "x", ParsingConfig? config = null)
28+
{
29+
if (string.IsNullOrWhiteSpace(expression))
30+
throw new ArgumentException("Expression cannot be null or empty", nameof(expression));
31+
32+
try
33+
{
34+
// Parse the string expression into a LambdaExpression using DynamicExpressionParser
35+
var parameter = Expression.Parameter(parameterType, parameterName);
36+
var lambda = DynamicExpressionParser.ParseLambda(config, new[] { parameter }, typeof(bool), expression, parameterName);
37+
38+
// Navigate the expression tree and convert to Filter
39+
var visitor = new ExpressionToFilterVisitor(parameterName);
40+
return visitor.ConvertToFilter(lambda.Body);
41+
}
42+
catch (Exception ex) when (!(ex is FilterException))
43+
{
44+
throw new FilterException($"Unable to parse expression '{expression}' into a Filter", ex);
45+
}
46+
}
47+
48+
/// <summary>
49+
/// Parses a string expression into a Filter object with a typed parameter.
50+
/// </summary>
51+
/// <typeparam name="T">The type of the parameter in the expression</typeparam>
52+
/// <param name="expression">The string expression to parse</param>
53+
/// <param name="parameterName">The name of the parameter (default: "x")</param>
54+
/// <param name="config">Optional parsing configuration</param>
55+
/// <returns>A Filter object representing the parsed expression</returns>
56+
public static Filter Parse<T>(string expression, string parameterName = "x", ParsingConfig? config = null)
57+
{
58+
return Parse(expression, typeof(T), parameterName, config);
59+
}
60+
}
61+
62+
/// <summary>
63+
/// Visitor class that navigates Expression trees and converts them to Filter objects.
64+
/// </summary>
65+
internal class ExpressionToFilterVisitor
66+
{
67+
private readonly string _parameterName;
68+
69+
public ExpressionToFilterVisitor(string parameterName)
70+
{
71+
_parameterName = parameterName ?? throw new ArgumentNullException(nameof(parameterName));
72+
}
73+
74+
/// <summary>
75+
/// Converts an Expression to a Filter object.
76+
/// </summary>
77+
public Filter ConvertToFilter(Expression expression)
78+
{
79+
return expression switch
80+
{
81+
BinaryExpression binary => ConvertBinaryExpression(binary),
82+
UnaryExpression unary => ConvertUnaryExpression(unary),
83+
ConstantExpression constant => ConvertConstantExpression(constant),
84+
MemberExpression member => ConvertMemberExpression(member),
85+
ParameterExpression parameter => ConvertParameterExpression(parameter),
86+
MethodCallExpression methodCall => ConvertMethodCallExpression(methodCall),
87+
NewExpression newExpr => ConvertNewExpression(newExpr),
88+
_ => throw new FilterException($"Unsupported expression type: {expression.GetType().Name}")
89+
};
90+
}
91+
92+
private Filter ConvertNewExpression(NewExpression newExpr)
93+
{
94+
// check if any of the arguments are not constant or variable
95+
if (newExpr.Arguments.Any(arg => !(arg is ConstantExpression)))
96+
throw new FilterException("New expressions must only contain constant expressions");
97+
if (newExpr.Constructor == null)
98+
throw new FilterException("New expression must have a valid constructor");
99+
100+
// Create a variable filter for the new expression
101+
var args = newExpr.Arguments
102+
.OfType<ConstantExpression>()
103+
.Select(arg => arg.Value)
104+
.ToArray();
105+
106+
// var obj = newExpr.Constructor.Invoke(null, args);
107+
var obj = Activator.CreateInstance(newExpr.Type, args);
108+
return Filter.Constant(obj);
109+
}
110+
111+
private Filter ConvertBinaryExpression(BinaryExpression binary)
112+
{
113+
var left = ConvertToFilter(binary.Left);
114+
var right = ConvertToFilter(binary.Right);
115+
116+
var filterType = binary.NodeType switch
117+
{
118+
ExpressionType.Equal => FilterType.Equal,
119+
ExpressionType.NotEqual => FilterType.NotEqual,
120+
ExpressionType.GreaterThan => FilterType.GreaterThan,
121+
ExpressionType.GreaterThanOrEqual => FilterType.GreaterThanOrEqual,
122+
ExpressionType.LessThan => FilterType.LessThan,
123+
ExpressionType.LessThanOrEqual => FilterType.LessThanOrEqual,
124+
ExpressionType.AndAlso => FilterType.And,
125+
ExpressionType.OrElse => FilterType.Or,
126+
_ => throw new FilterException($"Unsupported binary expression type: {binary.NodeType}")
127+
};
128+
129+
return Filter.Binary(left, right, filterType);
130+
}
131+
132+
private Filter ConvertUnaryExpression(UnaryExpression unary)
133+
{
134+
var operand = ConvertToFilter(unary.Operand);
135+
136+
var filterType = unary.NodeType switch
137+
{
138+
ExpressionType.Not => FilterType.Not,
139+
_ => throw new FilterException($"Unsupported unary expression type: {unary.NodeType}")
140+
};
141+
142+
return Filter.Unary(operand, filterType);
143+
}
144+
145+
private Filter ConvertConstantExpression(ConstantExpression constant)
146+
{
147+
return Filter.Constant(constant.Value);
148+
}
149+
150+
private Filter ConvertMemberExpression(MemberExpression member)
151+
{
152+
// Build the variable name by traversing the member access chain
153+
var variableName = BuildVariableName(member);
154+
return Filter.Variable(variableName);
155+
}
156+
157+
private Filter ConvertParameterExpression(ParameterExpression parameter)
158+
{
159+
return Filter.Variable(parameter.Name ?? _parameterName);
160+
}
161+
162+
private Filter ConvertMethodCallExpression(MethodCallExpression methodCall)
163+
{
164+
// Handle method calls as function filters
165+
if (methodCall.Object != null)
166+
{
167+
var variable = ConvertToFilter(methodCall.Object);
168+
if (variable is not VariableFilter variableFilter)
169+
throw new FilterException("Function calls must be made on variable expressions");
170+
171+
var arguments = methodCall.Arguments.Select(ConvertToFilter).ToArray();
172+
return Filter.Function(variableFilter, methodCall.Method.Name, arguments);
173+
}
174+
175+
throw new FilterException("Static method calls are not supported");
176+
}
177+
178+
private string BuildVariableName(MemberExpression member)
179+
{
180+
var parts = new List<string>();
181+
Expression current = member;
182+
183+
while (current != null)
184+
{
185+
switch (current)
186+
{
187+
case MemberExpression memberExpr:
188+
parts.Insert(0, memberExpr.Member.Name);
189+
current = memberExpr.Expression;
190+
break;
191+
case ParameterExpression paramExpr:
192+
// Don't include the parameter name if it matches our expected parameter name
193+
if (!String.IsNullOrWhiteSpace(paramExpr.Name) && !String.Equals(paramExpr.Name, _parameterName, StringComparison.Ordinal))
194+
parts.Insert(0, paramExpr.Name ?? _parameterName);
195+
current = null;
196+
break;
197+
default:
198+
current = null;
199+
break;
200+
}
201+
}
202+
203+
if (parts.Count == 0)
204+
throw new FilterException("Unable to build variable name from member expression");
205+
206+
return string.Join(".", parts);
207+
}
208+
}
209+
}

0 commit comments

Comments
 (0)