Conversation
This was referenced Oct 1, 2026
pdevito3
force-pushed
the
fm/qk-breaking-query-names
branch
from
October 1, 2026 20:59
e5dbe6b to
b8b4ae9
Compare
pdevito3
added a commit
that referenced
this pull request
Oct 1, 2026
The public method ReplaceAliasesWithPropertyPaths does not replace a query name after a dot anymore. A query name after a dot is a segment of a nested path of another type. For example, with the query name "name" on Title, "Author.Name" stays "Author.Name". The filter parser calls an internal overload that keeps the v1.14.2 match. Filters do not change. #154 owns the filter change for a query name after a dot. BREAKING CHANGE: ReplaceAliasesWithPropertyPaths keeps a query name after a dot. It also does not throw InvalidOperationException for a fully prevented query name after a dot.
QueryKit no longer replaces query names in the filter text before the parse. The grammar reads a query name as a property, and the property resolver maps it to its property path. As a result, a query name works in a property list and in arithmetic, and text inside a quoted value does not change. A property query name matches with the case rules of the current culture, like the alias regex of v1.14.2. A derived property or custom operation query name still ignores case with the invariant rules. When the grammar reads such a query name where v1.14.2 read an unknown identifier path, a filter that fails still throws the v1.14.2 exception. A property list skips a property that has PreventFilter by its query name too. Every configured property has its name as its default query name, so a property list also skips a prevented property in another letter case. A property that has PreventFilter and PreventSort still throws InvalidOperationException by its query name in front of an operator. BREAKING CHANGE: a query name inside a quoted value is not replaced anymore. A query name after a dot is not replaced anymore. A query name in a property list or in arithmetic resolves to its property instead of throwing. A property list skips a prevented property in another letter case.
pdevito3
force-pushed
the
fm/qk-breaking-query-names
branch
from
October 1, 2026 21:44
b8b4ae9 to
8b21edc
Compare
pdevito3
added a commit
that referenced
this pull request
Oct 1, 2026
The public method ReplaceAliasesWithPropertyPaths does not replace a query name after a dot anymore. A query name after a dot is a segment of a nested path of another type. For example, with the query name "name" on Title, "Author.Name" stays "Author.Name". The filter parser calls an internal overload that keeps the v1.14.2 match. Filters do not change. #154 owns the filter change for a query name after a dot. BREAKING CHANGE: ReplaceAliasesWithPropertyPaths keeps a query name after a dot. It also does not throw InvalidOperationException for a fully prevented query name after a dot.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For later consideration in a major version. Do not merge now. #134 restored the v1.14.2 behavior to keep v1.x compatible. This PR re-applies the query-name behavior of
3c85253(#113). The captain decides on this PR separately.This PR combines two items from #134:
f0f48be): QueryKit replaced query names in the filter text before the parse.5e7d918): QueryKit resolved a query name only in front of an operator.J5 and J-bis stay in one PR. They can not be separated, because they change the same code:
PropertyPathParser) and map them to a property path (PropertyResolver.Resolve).PropertyPathParserand the same query-name mapping in property lists and in arithmetic.PropertyPathParser,QueryName, and the query-name lookups. After one PR merges, the other PR conflicts in all of this code.QN1 (a query name after a dot)
This PR owns QN1. A filter such as
Author.name == "Ann"(query namenameonTitle) is not rewritten toAuthor.Titleanymore. QN1 can not be separated from this PR, because QN1 comes from the text rewrite that J5 removes.#127 now changes only the result of the public method
QueryKitPropertyMappings.ReplaceAliasesWithPropertyPaths. It does not change filters.Summary
ParseFilterno longer rewrites query names in the filter text before the parse. The operator and logical alias passes stay.PropertyPathParser(config)comes back. It reads a configured query name first (longest first), then a normal identifier path. The left side of a comparison and a property list use this parser.PropertyResolver.Resolvemaps a query name to the property path of its mapping before the depth check and the member lookup.ResolveArithmeticQueryNames).QueryKitPropertyMappings.PropertyQueryNamesandDerivedOrCustomOperationQueryNames(internal) hold the query names that the grammar reads.PreventFiltersetting by the query name first, then by the property path. This closes a bypass that the new property-list support opens (see Security).PreventFilter()andPreventSort()still throwsInvalidOperationExceptionby its query name in front of an operator.EnsureNoQueryNameOfAPropertyPreventedForFilterAndSortkeeps this v1.14.2 throw. fix(filter)!: ignore a fully prevented clause by its query name #152 is the PR that removes the throw.The parser from
bc70b3a(derived-property and custom-operation query names with a hyphen or a space) is now part ofPropertyPathParser. Its tests stay and pass.bc70b3ahas no PR of its own.v1.14.2 behavior (and main)
QueryKit replaces each query name in front of an operator with its property path. This replacement runs on the raw filter text, before the parse. As a result:
Title == "first == x"comparesTitlewithFirstName == x.UnknownFilterPropertyException.ArgumentException("Property 'stars' not found on type 'TestingPerson'").Author.name == "Lee"(query namenameonTitle) becomesAuthor.Titleand throwsUnknownFilterPropertyException: 'Title'.UnknownFilterPropertyExceptionfor the query name.New behavior
The parser reads a query name as a property name. The resolver maps it to its property path.
Author.name == "Lee"givesx => (x.Author.Name == "Lee"), like main before fix: restore v1.14.2 behavior for every breaking change on main #134.Example
HasQueryName("first")onFirstName, andHasQueryName("stars")onRating:Title == "first == x"x => (x.Title == "FirstName == x")x => (x.Title == "first == x")(first, LastName) == "Paul"UnknownFilterPropertyException: 'first'x => ((x.FirstName == "Paul") OrElse (x.LastName == "Paul"))(stars + 0) > 3ArgumentException: Property 'stars' not found on type 'TestingPerson'x => ((x.Rating + Convert(0, Nullable`1)) > Convert(3, Nullable`1))Some inputs that fail on main and on this PR throw a different exception type.
HasQueryName("years")onAge, and a derived property withHasQueryName("double age"):years + 1 > 3UnknownFilterPropertyException: The filter property 'years' was not recognized.ParsingException(likeAge + 1 > 3on all trees)first eq "x"(noeqalias)UnknownFilterPropertyException: The filter property 'first' was not recognized.ParsingException(double age, Age) > 6UnknownFilterPropertyException: The filter property 'double' was not recognized.ParsingExceptionThe cause: the grammar now reads the query name as a property. As a result, the parse fails at the next token, and not at the query name.
These inputs give the same result on main and on this PR:
first == "x",first eq "x"with aneqoperator alias, andTitle == "x"withHasQueryName("Title")onFirstName.hidden == "x", withHasQueryName("hidden").PreventFilter().PreventSort()onFirstName:InvalidOperationException.Justification
The filter text is a grammar. A text replacement before the parse can not know if a word is a property, a value, or a part of a path. As a result, v1.14.2 changes user data inside quoted values, and a query name works in some positions only. This PR gives one rule: a query name is a property name in each position where a property can occur.
Security
These notes compare the risk on v1.14.2 (and main) with this PR.
PreventFiltersetting by the exact property path only.(title, FirstName) == "x", withPreventFilter()onTitle, givesx => ((x.Title == "x") OrElse (x.FirstName == "x")). The client filters on the prevented property. On this PR, the result isx => (x.FirstName == "x").(hidden, Title) == "x", withHasQueryName("hidden").PreventFilter()onFirstName, filters onFirstName. This PR skips the clause and givesx => (x.Title == "x"). Main throwsUnknownFilterPropertyExceptionfor this input.PreventFilter.(Rating + 0) > 3filters on a preventedRating. This PR adds the query name as one more name for this known gap:(stars + 0) > 3filters on a preventedRatingtoo. fix(filter)!: apply PreventFilter and PreventSort to every property path #136 closes the gap for both names.Migration
UnknownFilterPropertyExceptionfor a failed filter that starts with a query name, also catchParsingException. The examples are in the second table above.UnknownFilterPropertyExceptionorArgumentExceptionfor a query name in a property list or in arithmetic, this exception no longer occurs. The query name filters by its property.IgnoredClauseBehaviorcontrols the skipped part.Author.name), use the member path. The resolver maps a full query name only.README
The Property Settings section gets a new paragraph. It says that a query name works on the left side of a comparison, in a property list, and in an arithmetic expression. It also says that QueryKit does not change text inside a quoted value.
Rebase on main
This PR is rebased on current main, after the restore PRs #155 to #160 and after #167 to #169. The rebase keeps the v1.14.2 results of these restores:
PreventFiltersetting of a left-side property is found by the property path of its query name. The filter text is not rewritten now, so the lookup maps the query name first.TIP > 3with the query nametıpfilters by its property, like main.TIPALPHA > 3with the query nametipalphathrowsUnknownFilterPropertyException, like main.double age > 6throwsUnknownFilterPropertyExceptionfordouble.Interaction with other PRs
PropertyResolver.Resolve, in the property-list check (I usesreference.CanFilter), and in the tests. In a combinedResolve, the depth check uses the mapped path, andResolveWithoutDepthCheckmaps the query name too. With both PRs, I's arithmetic check finds the setting through the query-name mapping of this PR.query_name_in_arithmetic_is_not_recognized. This gives a text conflict. If both merge, keep the version of this PR: the query name resolves.ReplaceAliasesWithPropertyPaths. If both merge,EnsureNoQueryNameOfAPropertyPreventedForFilterAndSortdoes nothing. Delete it then.ParseFilter. If both merge, keep the deletion. Then delete the 127 pinfilter_with_a_query_name_in_a_nested_path_still_throws_for_the_replaced_path, because this PR changes that result.ParseFilter, so the two PRs do not overlap in code.Tests
Unit (
QueryKit.UnitTests/PropertyResolverTests.cs), from main before #134:query_name_in_a_value_is_replacedbecomesquery_name_in_a_value_is_not_replacedagain.query_name_with_a_hyphen_in_a_value_is_replacedbecomesquery_name_with_a_hyphen_in_a_value_is_not_replacedagain.query_name_in_a_property_list_throwsbecomesquery_name_in_a_property_list_resolves_to_its_propertyagain.query_name_with_a_hyphen_in_a_property_list_throwsbecomesquery_name_with_a_hyphen_in_a_property_list_resolves_to_its_propertyagain.query_name_in_arithmetic_throwsbecomesquery_name_in_arithmetic_resolves_to_its_propertyagain.prevented_property_in_a_list_in_another_case_is_still_filteredbecomesprevented_property_in_a_list_is_skipped_in_any_caseagain. It expectsx => (x.FirstName == "x").query_name_of_a_prevented_property_in_a_property_list_throwsbecomesquery_name_of_a_prevented_property_in_a_property_list_is_skipped. It expectsx => (x.Title == "x").query_name_with_a_hyphen_before_an_operator_alias_filters_by_its_propertyand thebc70b3atests stay unchanged and pass.Integration (
QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs):query_name_in_a_property_list_is_filteredandquery_name_in_arithmetic_is_filteredcome back.New unit tests:
query_name_in_arithmetic_without_parentheses_throws_like_its_property(years + 1 > 3) andquery_name_matches_with_the_case_rules_of_tr_tr(3 cases).dotnet build: 0 errors.dotnet teston the rebased branch: 474 unit tests and 299 Postgres integration tests (Testcontainers) pass, 0 failures.