diff --git a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs index 9c3487c..0483422 100644 --- a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs +++ b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs @@ -533,6 +533,34 @@ public async Task prevented_property_clause_by_its_query_name_under_or_does_not_ people.Should().BeEmpty(); } + [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")] diff --git a/QueryKit.UnitTests/PropertyResolverTests.cs b/QueryKit.UnitTests/PropertyResolverTests.cs index 4707579..3305c06 100644 --- a/QueryKit.UnitTests/PropertyResolverTests.cs +++ b/QueryKit.UnitTests/PropertyResolverTests.cs @@ -967,7 +967,7 @@ public void quoted_value_with_a_dot_on_the_right_side_is_a_value(string input, s } [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 => @@ -976,14 +976,13 @@ public void unknown_property_in_arithmetic_throws_when_unknown_properties_are_al config.AllowUnknownProperties = true; }); - var act = () => FilterParser.ParseFilter(input, config); + var filterExpression = FilterParser.ParseFilter(input, config); - act.Should().Throw() - .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 => @@ -992,21 +991,20 @@ public void unknown_property_on_the_right_side_of_arithmetic_throws_when_unknown config.AllowUnknownProperties = true; }); - var act = () => FilterParser.ParseFilter(input, config); + var filterExpression = FilterParser.ParseFilter(input, config); - act.Should().Throw() - .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(input); - act.Should().Throw() - .WithMessage("Property 'Nope' not found on type 'TestingPerson'"); + act.Should().Throw() + .WithMessage("The filter property 'Nope' was not recognized."); } [Theory] diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 275b5c9..551d5ff 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -896,16 +896,22 @@ private static Parser ArithmeticComparisonExprParser(ParameterExp } // Resolves each property in an arithmetic expression to its member path. - // Returns null when a property cannot be filtered, because then the parser removes the clause. + // Returns null when a property cannot be filtered or is an allowed unknown property, because then the parser removes the clause. private static ArithmeticExpression? ResolveArithmeticProperties(ArithmeticExpression expr, Type entityType, IQueryKitConfiguration? config) { switch (expr) { case PropertyArithmeticExpression property: var reference = PropertyResolver.Resolve(entityType, property.PropertyPath, config); + // Arithmetic supports only members, so a derived property or a custom operation is unknown here if (reference.Kind != PropertyReferenceKind.Member) { - return property; + if (config?.AllowUnknownProperties == true) + { + return null; + } + + throw new UnknownFilterPropertyException(reference.UnknownSegment ?? property.PropertyPath); } return reference.CanFilter ? new PropertyArithmeticExpression(reference.Path) : null; diff --git a/README.md b/README.md index 3b5ebad..7299a7f 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,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