Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,34 @@ public async Task property_is_not_prevented_by_another_property_whose_query_name
people[0].Id.Should().Be(fakePerson.Id);
}

[Fact]
public async Task unknown_property_in_arithmetic_removes_the_clause_when_unknown_properties_are_allowed()
{
// Arrange
var testingServiceScope = new TestingServiceScope();
var title = new Faker().Lorem.Sentence();
var fakePerson = new FakeTestingPersonBuilder()
.WithTitle(title)
.WithAge(30)
.Build();
await testingServiceScope.InsertAsync(fakePerson);

var input = $"""Title == "{title}" && ((Nope + 1) > 3 || Age > 100)""";
var config = new QueryKitConfiguration(config =>
{
config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove;
config.AllowUnknownProperties = true;
});

// Act
var people = await testingServiceScope.DbContext().People
.ApplyQueryKitFilter(input, config)
.ToListAsync();

// Assert
people.Should().BeEmpty();
}

[Theory]
[InlineData("first-name")]
[InlineData("_first")]
Expand Down
25 changes: 12 additions & 13 deletions QueryKit.UnitTests/PropertyResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ public void query_name_of_a_prevented_property_in_a_property_list_throws()
}

[Fact]
public void query_name_in_arithmetic_throws()
public void query_name_in_arithmetic_is_not_recognized()
{
var input = """(stars + 0) > 3""";
var config = new QueryKitConfiguration(config =>
Expand All @@ -433,7 +433,8 @@ public void query_name_in_arithmetic_throws()

var act = () => FilterParser.ParseFilter<TestingPerson>(input, config);

act.Should().ThrowExactly<ArgumentException>();
act.Should().Throw<UnknownFilterPropertyException>()
.WithMessage("The filter property 'stars' was not recognized.");
}

[Theory]
Expand Down Expand Up @@ -710,7 +711,7 @@ public void unquoted_dotted_word_on_the_right_side_throws()
}

[Fact]
public void unknown_property_in_arithmetic_throws_when_unknown_properties_are_allowed()
public void unknown_property_in_arithmetic_removes_the_clause_when_unknown_properties_are_allowed()
{
var input = """(Nope + 1) > 3 || Age > 100""";
var config = new QueryKitConfiguration(config =>
Expand All @@ -719,14 +720,13 @@ public void unknown_property_in_arithmetic_throws_when_unknown_properties_are_al
config.AllowUnknownProperties = true;
});

var act = () => FilterParser.ParseFilter<TestingPerson>(input, config);
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, config);

act.Should().Throw<ArgumentException>()
.WithMessage("Property 'Nope' not found on type 'TestingPerson'");
filterExpression.ToDisplayString().Should().Be("x => (x.Age > 100)");
}

[Fact]
public void unknown_property_on_the_right_side_of_arithmetic_throws_when_unknown_properties_are_allowed()
public void unknown_property_on_the_right_side_of_arithmetic_removes_the_clause_when_unknown_properties_are_allowed()
{
var input = """(Age + 0) > Nope || Title == "a" """;
var config = new QueryKitConfiguration(config =>
Expand All @@ -735,21 +735,20 @@ public void unknown_property_on_the_right_side_of_arithmetic_throws_when_unknown
config.AllowUnknownProperties = true;
});

var act = () => FilterParser.ParseFilter<TestingPerson>(input, config);
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input, config);

act.Should().Throw<ArgumentException>()
.WithMessage("Property 'Nope' not found on type 'TestingPerson'");
filterExpression.ToDisplayString().Should().Be("""x => (x.Title == "a")""");
}

[Fact]
public void unknown_property_in_arithmetic_throws_an_argument_exception()
public void unknown_property_in_arithmetic_is_not_recognized()
{
var input = """(Nope + 1) > 3""";

var act = () => FilterParser.ParseFilter<TestingPerson>(input);

act.Should().Throw<ArgumentException>()
.WithMessage("Property 'Nope' not found on type 'TestingPerson'");
act.Should().Throw<UnknownFilterPropertyException>()
.WithMessage("The filter property 'Nope' was not recognized.");
}

[Theory]
Expand Down
27 changes: 27 additions & 0 deletions QueryKit/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,18 @@ private static Parser<Expression> ArithmeticComparisonExprParser<T>(ParameterExp
.SelectMany(temp => parenthesizedArithmetic.Or(rightSideValueParser.Select(value => CreateArithmeticFromValue(value.Value))), (temp, rightSide) => new { temp.leftArithmetic, temp.op, rightSide })
.Select(temp =>
{
var unknownSegment = FindUnknownArithmeticSegment(temp.leftArithmetic, typeof(T), config)
?? FindUnknownArithmeticSegment(temp.rightSide, typeof(T), config);
if (unknownSegment != null)
{
if (config?.AllowUnknownProperties == true)
{
return IgnoredClause(config);
}

throw new UnknownFilterPropertyException(unknownSegment);
}

var leftExpr = temp.leftArithmetic.ToLinqExpression(parameter, typeof(T));
var rightExpr = temp.rightSide.ToLinqExpression(parameter, typeof(T));

Expand All @@ -815,6 +827,21 @@ private static Parser<Expression> ArithmeticComparisonExprParser<T>(ParameterExp
});
}

// Returns the first segment that is not a member, for the first property in an arithmetic expression that is not a member.
// Arithmetic supports only members, so a derived property or a custom operation is unknown here.
private static string? FindUnknownArithmeticSegment(ArithmeticExpression expr, Type entityType, IQueryKitConfiguration? config)
{
return expr switch
{
PropertyArithmeticExpression property => PropertyResolver.ResolveWithoutDepthCheck(entityType, property.PropertyPath, config) is { Kind: not PropertyReferenceKind.Member } reference
? reference.UnknownSegment
: null,
BinaryArithmeticExpression binary => FindUnknownArithmeticSegment(binary.Left, entityType, config) ?? FindUnknownArithmeticSegment(binary.Right, entityType, config),
GroupedArithmeticExpression grouped => FindUnknownArithmeticSegment(grouped.Inner, entityType, config),
_ => null
};
}

private static bool ContainsArithmeticOperator(ArithmeticExpression expr)
{
return expr switch
Expand Down
5 changes: 5 additions & 0 deletions QueryKit/PropertyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ internal static class PropertyResolver
internal static PropertyReference Resolve(Type rootType, string reference, IQueryKitConfiguration? config)
{
config?.ValidatePropertyDepth(reference);
return ResolveWithoutDepthCheck(rootType, reference, config);
}

// Arithmetic does not apply MaxPropertyDepth.
internal static PropertyReference ResolveWithoutDepthCheck(Type rootType, string reference, IQueryKitConfiguration? config)
{
var memberPath = ResolveMemberPath(rootType, reference, out var unknownSegment);
if (memberPath != null)
{
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ var input = "((Revenue - Expenses) / Revenue) > 0.1";
- **Parentheses**: Use parentheses to control calculation order and group expressions
- **Entity Framework Translation**: All arithmetic expressions are translated to efficient SQL queries
- **Property-to-Property**: Can mix property references with literal values in the same expression
- **Unknown Properties**: An unknown property in an arithmetic expression throws `UnknownFilterPropertyException`, like any other filter property. When `AllowUnknownProperties` is `true`, QueryKit ignores the clause. Arithmetic supports only entity members, so a derived property or a custom operation name is unknown here

#### Examples

Expand Down
Loading