From 495358a77b29577545487ab5c308ab27c0445cab Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Sat, 3 Oct 2026 14:58:36 +0300 Subject: [PATCH] fix(filter): ignore a fully prevented clause by its query name A filter that used the query name of a property with PreventFilter and PreventSort threw InvalidOperationException. The member name of the same property gives an ignored clause. Now the query name gives the same ignored clause as the member name, and IgnoredClauseBehavior controls it. Behavior change: this filter no longer throws InvalidOperationException. QueryKitPropertyMappings.ReplaceAliasesWithPropertyPaths no longer throws for this query name. Examples (Title has HasQueryName("name").PreventFilter().PreventSort(), default ReplaceWithTrue): name == "x" && Age > 100 before: InvalidOperationException: 'Title' is not allowed for filtering or sorting. after: (True == True) AndAlso (x.Age > 100) name == "x" || Age > 100 (IgnoredClauseBehavior.Remove) before: InvalidOperationException after: x.Age > 100 From #152 on v2. --- .../Tests/PropertyResolverTests.cs | 29 +++++++++++++++++++ QueryKit.UnitTests/PropertyResolverTests.cs | 27 +++++++++++++++++ QueryKit/QueryKitPropertyMappings.cs | 5 ---- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs index f7d12af..48566c4 100644 --- a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs +++ b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs @@ -445,6 +445,35 @@ public async Task query_name_in_arithmetic_is_filtered() people.Should().ContainSingle(x => x.Id == fakePerson.Id); } + [Fact] + public async Task prevented_property_clause_by_its_query_name_under_or_does_not_return_every_row() + { + // Arrange + var testingServiceScope = new TestingServiceScope(); + var title = new Faker().Lorem.Sentence(); + var fakePerson = new FakeTestingPersonBuilder() + .WithTitle(title) + .WithFirstName("Paul") + .WithAge(30) + .Build(); + await testingServiceScope.InsertAsync(fakePerson); + + 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(); + }); + + // 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 15b3236..04a9bcd 100644 --- a/QueryKit.UnitTests/PropertyResolverTests.cs +++ b/QueryKit.UnitTests/PropertyResolverTests.cs @@ -781,6 +781,33 @@ public void property_prevented_for_filter_and_sort_is_removed_by_its_query_name_ filterExpression.ToDisplayString().Should().Be("x => (x.Age > 100)"); } + [Fact] + public void query_name_of_a_property_prevented_for_filter_and_sort_in_a_quoted_value_is_not_changed() + { + var input = """Title == "name == x" """; + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.FirstName!).HasQueryName("name").PreventFilter().PreventSort(); + }); + + var filterExpression = FilterParser.ParseFilter(input, config); + + filterExpression.ToDisplayString().Should().Be("""x => (x.Title == "name == x")"""); + } + + [Fact] + public void alias_replacement_does_not_throw_for_a_property_prevented_for_filter_and_sort() + { + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Title!).HasQueryName("name").PreventFilter().PreventSort(); + }); + + var input = config.PropertyMappings.ReplaceAliasesWithPropertyPaths("""name == "x" """); + + input.Should().Be("""Title == "x" """); + } + [Theory] [InlineData("==")] [InlineData("_=")] diff --git a/QueryKit/QueryKitPropertyMappings.cs b/QueryKit/QueryKitPropertyMappings.cs index d914ca8..16ecdf8 100644 --- a/QueryKit/QueryKitPropertyMappings.cs +++ b/QueryKit/QueryKitPropertyMappings.cs @@ -153,11 +153,6 @@ public string ReplaceAliasesWithPropertyPaths(string input) // (e.g. the `^` in `^^` would otherwise be read as a start-of-line anchor). var regex = AliasRegexCache.Get($@"\b{Regex.Escape(queryKitPropertyInfo.QueryName!)}\b(?=\s*{Regex.Escape(op)})"); - if (queryKitPropertyInfo is { CanSort: false, CanFilter: false} && regex.IsMatch(input)) - { - throw new InvalidOperationException($"'{queryKitPropertyInfo.Name}' is not allowed for filtering or sorting."); - } - input = regex.Replace(input, propertyPath); } }