From 6654e11a781b3118b45fef604c2571ded71bdfa6 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Wed, 30 Sep 2026 17:05:33 +0300 Subject: [PATCH] feat(filter)!: remove prevented and unknown clauses by default A clause on a PreventFilter property, or on an unknown property with AllowUnknownProperties, became (true == true). Under an OR, this made the whole filter match every row, so Name == "Ann" || Secret == "s" returned everyone. IgnoredClauseBehavior now defaults to Remove, so QueryKit drops the clause and keeps the rest of the filter. Set it to ReplaceWithTrue to get the v1.14.2 behavior. BREAKING CHANGE: A prevented or unknown clause is now removed, not replaced with (true == true). Under an OR, the filter returns fewer rows. A filter with only ignored clauses becomes x => True. Set IgnoredClauseBehavior to ReplaceWithTrue to keep the v1.14.2 behavior. --- .../Tests/PropertyResolverTests.cs | 8 ++---- .../CustomFilterPropertyTests.cs | 10 +++---- QueryKit.UnitTests/PropertyResolverTests.cs | 27 +++++-------------- .../Configuration/IgnoredClauseBehavior.cs | 4 +-- QueryKit/Configuration/QueryKitSettings.cs | 5 ++-- QueryKit/FilterParser.cs | 6 +++-- 6 files changed, 22 insertions(+), 38 deletions(-) diff --git a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs index 3a42f8e..4e97d76 100644 --- a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs +++ b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs @@ -12,7 +12,7 @@ namespace QueryKit.IntegrationTests.Tests; public class PropertyResolverTests : TestBase { [Fact] - public async Task unknown_property_clause_under_or_is_true_by_default() + public async Task unknown_property_clause_under_or_is_true_when_replaced_with_true() { // Arrange var testingServiceScope = new TestingServiceScope(); @@ -26,6 +26,7 @@ public async Task unknown_property_clause_under_or_is_true_by_default() var input = $"""Title == "{title}" && (Nope == "x" || Age > 100)"""; var config = new QueryKitConfiguration(config => { + config.IgnoredClauseBehavior = IgnoredClauseBehavior.ReplaceWithTrue; config.AllowUnknownProperties = true; }); @@ -53,7 +54,6 @@ public async Task unknown_property_clause_under_or_does_not_return_every_row() var input = $"""Title == "{title}" && (Nope == "x" || Age > 100)"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.AllowUnknownProperties = true; }); @@ -82,7 +82,6 @@ public async Task prevented_property_clause_under_or_does_not_return_every_row() var input = $"""Title == "{title}" && (Rating == 1 || Age > 100)"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Rating).PreventFilter(); }); @@ -111,7 +110,6 @@ public async Task prevented_property_clause_by_its_query_name_under_or_does_not_ var input = $"""Title == "{title}" && (first == "Paul" || Age > 100)"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.FirstName).HasQueryName("first").PreventFilter().PreventSort(); }); @@ -168,7 +166,6 @@ public async Task prevented_property_on_the_right_side_is_not_compared() var input = $"""Title == "{title}" && (FirstName == LastName || Age > 100)"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.LastName).PreventFilter(); }); @@ -429,7 +426,6 @@ public async Task unknown_property_in_arithmetic_removes_the_clause_when_unknown var input = $"""Title == "{title}" && ((Nope + 1) > 3 || Age > 100)"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.AllowUnknownProperties = true; }); diff --git a/QueryKit.UnitTests/CustomFilterPropertyTests.cs b/QueryKit.UnitTests/CustomFilterPropertyTests.cs index 70ed347..a41cbdf 100644 --- a/QueryKit.UnitTests/CustomFilterPropertyTests.cs +++ b/QueryKit.UnitTests/CustomFilterPropertyTests.cs @@ -142,7 +142,7 @@ public void can_have_custom_prop_excluded_from_filter() config.Property(x => x.Id).PreventFilter(); }); var filterExpression = FilterParser.ParseFilter(input, config); - filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "{stringValue}") OrElse (True == True))"""); + filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "{stringValue}")"""); } [Fact] @@ -159,7 +159,7 @@ public void can_have_custom_prop_excluded_from_filter_with_custom_propname() config.Property(x => x.Id).HasQueryName("identifier").PreventFilter(); }); var filterExpression = FilterParser.ParseFilter(input, config); - filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "{stringValue}") OrElse (True == True))"""); + filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "{stringValue}")"""); } [Fact] @@ -243,7 +243,7 @@ public void can_have_derived_prop_work_with_collection_filters() } [Fact] - public void filter_prevented_props_always_have_true_equals_true_regardless_of_comparison() + public void filter_prevented_props_are_removed_regardless_of_comparison() { var faker = new Faker(); var filterOperator = faker.PickRandom(ComparisonOperator.List.Where(x => x != ComparisonOperator.EqualsOperator()).ToList()); @@ -255,7 +255,7 @@ public void filter_prevented_props_always_have_true_equals_true_regardless_of_co config.Property(x => x.Id).PreventFilter(); }); var filterExpression = FilterParser.ParseFilter(input, config); - filterExpression.ToDisplayString().Should().Be("x => (True == True)"); + filterExpression.ToDisplayString().Should().Be("x => True"); } [Fact] @@ -284,6 +284,6 @@ public void can_handle_nonexistent_property() config.AllowUnknownProperties = true; }); var filterExpression = FilterParser.ParseFilter(input, config); - filterExpression.ToDisplayString().Should().Be("x => (True == True)"); + filterExpression.ToDisplayString().Should().Be("x => True"); } } \ No newline at end of file diff --git a/QueryKit.UnitTests/PropertyResolverTests.cs b/QueryKit.UnitTests/PropertyResolverTests.cs index 294b368..34e764e 100644 --- a/QueryKit.UnitTests/PropertyResolverTests.cs +++ b/QueryKit.UnitTests/PropertyResolverTests.cs @@ -10,11 +10,12 @@ namespace QueryKit.UnitTests; public class PropertyResolverTests { [Fact] - public void unknown_property_clause_is_true_equals_true_by_default() + public void unknown_property_clause_is_true_equals_true_when_replaced_with_true() { var input = """Nope == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { + config.IgnoredClauseBehavior = IgnoredClauseBehavior.ReplaceWithTrue; config.AllowUnknownProperties = true; }); @@ -24,11 +25,12 @@ public void unknown_property_clause_is_true_equals_true_by_default() } [Fact] - public void prevented_property_clause_is_true_equals_true_by_default() + public void prevented_property_clause_is_true_equals_true_when_replaced_with_true() { var input = """FirstName == "Ann" || Title == "s" """; var config = new QueryKitConfiguration(config => { + config.IgnoredClauseBehavior = IgnoredClauseBehavior.ReplaceWithTrue; config.Property(x => x.Title).PreventFilter(); }); @@ -38,11 +40,12 @@ public void prevented_property_clause_is_true_equals_true_by_default() } [Fact] - public void property_list_with_only_prevented_properties_is_true_by_default() + public void property_list_with_only_prevented_properties_is_true_when_replaced_with_true() { var input = """(Title, FirstName) == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { + config.IgnoredClauseBehavior = IgnoredClauseBehavior.ReplaceWithTrue; config.Property(x => x.Title).PreventFilter(); config.Property(x => x.FirstName).PreventFilter(); }); @@ -58,7 +61,6 @@ public void unknown_property_clause_is_removed_under_or() var input = """Nope == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.AllowUnknownProperties = true; }); @@ -73,7 +75,6 @@ public void unknown_property_clause_is_removed_under_and() var input = """Age > 100 && Nope == "x" """; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.AllowUnknownProperties = true; }); @@ -88,7 +89,6 @@ public void prevented_property_clause_is_removed_under_or() var input = """Rating == 1 || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Rating).PreventFilter(); }); @@ -103,7 +103,6 @@ public void removed_clause_in_a_group_is_removed_from_the_group() var input = """Title == "a" && (Nope == "x" || Age > 100)"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.AllowUnknownProperties = true; }); @@ -118,7 +117,6 @@ public void property_list_with_only_prevented_properties_is_removed() var input = """(Title, FirstName) == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Title).PreventFilter(); config.Property(x => x.FirstName).PreventFilter(); }); @@ -134,7 +132,6 @@ public void prevented_property_in_arithmetic_removes_the_clause() var input = """(Age + 0) > 10 || Title == "a" """; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Age).PreventFilter(); }); @@ -149,7 +146,6 @@ public void prevented_property_on_the_right_side_of_arithmetic_removes_the_claus var input = """(Age + 0) > (Rating * 2)"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Rating).PreventFilter(); }); @@ -178,7 +174,6 @@ public void prevented_property_on_the_right_side_removes_the_clause() var input = """FirstName == Title || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Title).PreventFilter(); }); @@ -193,7 +188,6 @@ public void prevented_property_on_the_right_side_removes_the_clause_in_any_case( var input = """FirstName == title || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Title).PreventFilter(); }); @@ -222,7 +216,6 @@ public void prevented_property_removes_the_clause_in_any_case() var input = """title == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Title).PreventFilter(); }); @@ -237,7 +230,6 @@ public void prevented_property_with_a_query_name_removes_the_clause_when_written var input = """title == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Title).HasQueryName("t").PreventFilter(); }); @@ -295,7 +287,6 @@ public void prevented_derived_property_removes_the_clause() var input = """full == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.DerivedProperty(x => x.FirstName + " " + x.LastName).HasQueryName("full").PreventFilter(); }); @@ -324,7 +315,6 @@ public void prevented_custom_operation_removes_the_clause() var input = """adult == true || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.CustomOperation((x, op, value) => x.Age > 17).HasQueryName("adult").PreventFilter(); }); @@ -530,7 +520,6 @@ public void property_prevented_for_filter_and_sort_is_removed_by_its_query_name( var input = """name == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Title).HasQueryName("name").PreventFilter().PreventSort(); }); @@ -545,7 +534,6 @@ public void property_prevented_for_filter_and_sort_is_removed_by_its_member_name var input = """Title == "x" || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Title).HasQueryName("name").PreventFilter().PreventSort(); }); @@ -597,7 +585,6 @@ public void prevented_property_path_on_the_right_side_removes_the_clause() var input = """Title == Author.Name || Directions == "x" """; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.Property(x => x.Author.Name).PreventFilter(); }); @@ -612,7 +599,6 @@ public void unknown_property_in_arithmetic_removes_the_clause_when_unknown_prope var input = """(Nope + 1) > 3 || Age > 100"""; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.AllowUnknownProperties = true; }); @@ -627,7 +613,6 @@ public void unknown_property_on_the_right_side_of_arithmetic_removes_the_clause_ var input = """(Age + 0) > Nope || Title == "a" """; var config = new QueryKitConfiguration(config => { - config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; config.AllowUnknownProperties = true; }); diff --git a/QueryKit/Configuration/IgnoredClauseBehavior.cs b/QueryKit/Configuration/IgnoredClauseBehavior.cs index 742e1e6..31e6873 100644 --- a/QueryKit/Configuration/IgnoredClauseBehavior.cs +++ b/QueryKit/Configuration/IgnoredClauseBehavior.cs @@ -6,9 +6,9 @@ namespace QueryKit.Configuration; /// public enum IgnoredClauseBehavior { - /// Default. Replaces the clause with (true == true). Under an OR, the whole OR is then true. + /// Replaces the clause with (true == true), the same as v1.14.2. Under an OR, the whole OR is then true. ReplaceWithTrue = 0, - /// Removes the clause. A logical operator with a removed side keeps only its other side. + /// Default. Removes the clause. A logical operator with a removed side keeps only its other side. Remove = 1 } diff --git a/QueryKit/Configuration/QueryKitSettings.cs b/QueryKit/Configuration/QueryKitSettings.cs index e79ba3c..bb6960d 100644 --- a/QueryKit/Configuration/QueryKitSettings.cs +++ b/QueryKit/Configuration/QueryKitSettings.cs @@ -51,9 +51,10 @@ public class QueryKitSettings /// /// What the filter parser does with a clause on a prevented or unknown property. The default is - /// , the same as v1.14.2. + /// . Use + /// for the v1.14.2 behavior. /// - public IgnoredClauseBehavior IgnoredClauseBehavior { get; set; } = IgnoredClauseBehavior.ReplaceWithTrue; + public IgnoredClauseBehavior IgnoredClauseBehavior { get; set; } = IgnoredClauseBehavior.Remove; public QueryKitPropertyMapping Property(Expression>? propertySelector) { diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 04eb070..93fbee9 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -1253,9 +1253,11 @@ private static Parser OrExprParser(ParameterExpression parameter, ); private static bool RemovesIgnoredClauses(IQueryKitConfiguration? config) - => config is QueryKitConfiguration { IgnoredClauseBehavior: IgnoredClauseBehavior.Remove }; + => ((config as QueryKitConfiguration)?.IgnoredClauseBehavior ?? IgnoredClauseBehavior.Remove) + == IgnoredClauseBehavior.Remove; - // A clause on a prevented or unknown property. By default it becomes true == true, the same as v1.14.2. + // A clause on a prevented or unknown property. By default the parser removes it. With ReplaceWithTrue + // it becomes true == true, the same as v1.14.2. private static Expression IgnoredClause(IQueryKitConfiguration? config) => RemovesIgnoredClauses(config) ? RemovedClauseExpression.Instance