Skip to content

Extensible query string functions #1286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 23, 2023
Merged

Extensible query string functions #1286

merged 16 commits into from
Jul 23, 2023

Conversation

bkoelman
Copy link
Member

@bkoelman bkoelman commented Jul 5, 2023

This PR enables you to plug in your own functions in query string parameters. For example, after defining the isUpperCase filter function, you can use:

GET /blogs/1/posts?filter=and(isUpperCase(caption),not(isUpperCase(summary)))

Likewise, after you've added the sum function, you can use the following request:

GET /blogPosts?filter=greaterThan(sum(comments,numStars),'4')

or compose with other functions, such as count:

GET /blogPosts?filter=lessThan(sum(comments,numStars),sum(contributors,count(children)))

It works for sorting too. After adding the length function, the following request can be used:

GET /blogs?sort=length(title)&include=posts&sort[posts]=-length(caption)

To make this all work, some preliminary changes were needed. First of all, move the existing validation callbacks into the query string parsers so that they can become injectable. Until now, we've had the pubternal FieldChainRequirements enum, a set of fuzzy flags to indicate the expected shape of resource field chains (like owner.firstName and posts.comments.votes). This has been replaced with a pattern matcher, so that developers can define their own shapes of expected field chains. Such a pattern is a simplified form of a regular expression and is matched against the resource graph. For example, the pattern O*A means to match zero or more to-one relationships, followed by an attribute. The hard part is producing helpful errors on match failure, which is what the original code did. The query string parsers now track the failure position to make this easier to comprehend. For example:

GET /calendars?filter=and(equals(showWeekNumbers,'true'),has(appointments))

Now fails by reporting the position, including the ^ marker to point to the error location:

{
  "errors": [
    {
      "status": "400",
      "title": "The specified filter is invalid.",
      "detail": "Filtering on relationship 'appointments' is not allowed. Failed at position 40: and(equals(showWeekNumbers,'true'),has(^appointments))",
      "source": {
        "parameter": "filter"
      }
    }
  ]
}

As it turns out, this is pretty nice for brace matching as well:

) expected. Failed at position 56: or(equals(archivedAt,null),not(equals(archivedAt,null))^
End of expression expected. Failed at position 57: or(equals(archivedAt,null),not(equals(archivedAt,null)))^)

With FieldChainRequirements out of the way, the parsers and LINQ builders have been moved out of the *.Internal namespaces. New interfaces enable to plug in your own parts, such as IFilterParser and IWhereClauseBuilder. The built-in implementations are public non-sealed and provide virtual methods to override the needed parts.

For example, to implement the isUpperCase filter function, you'd:

  1. Define the expression: public class IsUpperCaseExpression : FilterExpression
  2. Inherit from the built-in filter parser: public class IsUpperCaseFilterParser : FilterParser and override the ParseFilter method, delegating to base if the keyword is not "isUpperCase".
  3. Inherit from the built-in LINQ builder: public class IsUpperCaseWhereClauseBuilder : WhereClauseBuilder and override the DefaultVisit method, delegating to your code if the expression type is IsUpperCaseExpression.
  4. Register IsUpperCaseFilterParser and IsUpperCaseWhereClauseBuilder in the IoC container.

Working implementations with tests for the functions mentioned above are provided in this PR. See the \test\JsonApiDotNetCoreTests\IntegrationTests\QueryStrings\CustomFunctions directory.

Fixes #1283.
Fixes #1277.

QUALITY CHECKLIST

@bkoelman bkoelman force-pushed the query-string-extensibility branch 2 times, most recently from a3eac3c to d82ea66 Compare July 5, 2023 12:01
@codecov
Copy link

codecov bot commented Jul 5, 2023

Codecov Report

Merging #1286 (76ca65b) into master (fe64052) will increase coverage by 0.02%.
The diff coverage is 95.13%.

❗ Current head 76ca65b differs from pull request most recent head 38a4a83. Consider uploading reports for the commit 38a4a83 to get more accurate results

@@            Coverage Diff             @@
##           master    #1286      +/-   ##
==========================================
+ Coverage   92.97%   93.00%   +0.02%     
==========================================
  Files         255      268      +13     
  Lines        8259     8776     +517     
==========================================
+ Hits         7679     8162     +483     
- Misses        580      614      +34     
Impacted Files Coverage Δ
...tityFrameworkExample/Repositories/TagRepository.cs 0.00% <0.00%> (ø)
...piDotNetCore.Annotations/Resources/Identifiable.cs 100.00% <ø> (ø)
...Core.Annotations/Resources/RuntimeTypeConverter.cs 100.00% <ø> (ø)
...DotNetCore/AtomicOperations/OperationsProcessor.cs 96.87% <ø> (ø)
src/JsonApiDotNetCore/CollectionExtensions.cs 63.33% <0.00%> (-12.67%) ⬇️
...JsonApiDotNetCore/Queries/EvaluatedIncludeCache.cs 100.00% <ø> (ø)
...ApiDotNetCore/Queries/Expressions/AnyExpression.cs 73.33% <ø> (ø)
...etCore/Queries/Expressions/ComparisonExpression.cs 85.00% <ø> (ø)
...ApiDotNetCore/Queries/Expressions/HasExpression.cs 88.46% <ø> (ø)
...re/Queries/Expressions/IncludeElementExpression.cs 67.74% <ø> (ø)
... and 82 more

... and 2 files with indirect coverage changes

@bkoelman bkoelman marked this pull request as ready for review July 5, 2023 13:55
@bkoelman bkoelman requested a review from maurei July 5, 2023 13:55
@bkoelman bkoelman force-pushed the query-string-extensibility branch from d82ea66 to 38a4a83 Compare July 23, 2023 08:39
@bkoelman bkoelman merged commit 99f106f into master Jul 23, 2023
@bkoelman bkoelman deleted the query-string-extensibility branch July 23, 2023 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

No error when comparing count() with null How to implement a custom filter
1 participant