From e2cbe3a79f869556b407ee6f81c4003054bf69d6 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Fri, 9 Oct 2026 23:46:17 +0300 Subject: [PATCH] fix(filter): report an unknown property in arithmetic as unknown An unknown property in arithmetic threw ArgumentException, also when AllowUnknownProperties was true. An API that maps QueryKitException to 400 returned 500. An unknown property in arithmetic now gets the same result as in any other clause: UnknownFilterPropertyException by default, and an ignored clause with AllowUnknownProperties. IgnoredClauseBehavior controls the ignored clause. Behavior change: catch UnknownFilterPropertyException or QueryKitException instead of ArgumentException. With AllowUnknownProperties, the clause no longer throws. Examples: (Nope + 1) > 3 before: ArgumentException: Property 'Nope' not found on type 'TestingPerson' after: UnknownFilterPropertyException: The filter property 'Nope' was not recognized. (Nope + 1) > 3 || Age > 100 (AllowUnknownProperties, IgnoredClauseBehavior.Remove) before: ArgumentException after: x => (x.Age > 100) --- .../Tests/PropertyResolverTests.cs | 28 +++++++++++++++++++ QueryKit.UnitTests/PropertyResolverTests.cs | 20 ++++++------- QueryKit/FilterParser.cs | 10 +++++-- README.md | 1 + 4 files changed, 46 insertions(+), 13 deletions(-) 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