-
-
Notifications
You must be signed in to change notification settings - Fork 159
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
Composable filters and deeply nested queries #792
Composable filters and deeply nested queries #792
Conversation
The coming weeks I won't be available to respond, but please feel free to try things out and provide feedback. Although still draft, this PR is feature complete, fully working and covered by tests. |
Hi Bart All this looks very clean and powerful. The following may not be directly related to this PR but maybe a bigger issue. I can open up another issue on the main fork if that makes sense. Anyway... I tested against my model which does not have a default "Id" field in the underlying table. Attempting to work around this by creating a non mapped Id field will get past the above problem, but will incorrectly try and filter on an Id field in the database. In short, there seems to be code that always expects Identifiable instead of IIdentifiable |
Hi @ClintGood thanks for your feedback. As far as I know, there has always been an implicit requirement for a typed Id property to exist. This is something the json:api specification depends on. I've added a comment at #797 that may solve your problem. |
…inition. This hides forced included fields from output, but still fetches them.
Impressive work @bart-degreed. 👍 |
General feedback item: all items in |
…and now that it is integrated parsers can easily be reused from `ResourceDefinition`s without needing to know what type of relationships are allowed at various stages.
…-comments on several types.
@maurei I've moved several types out of Internal namespace and added doc-comments. Please let me know specific locations where you believe more explanation would help. I'll address overview documentation later. |
test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterTests.cs
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/Queries/Parsing/ResolveFieldChainCallback.cs
Outdated
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/Queries/Parsing/ResolveFieldChainCallback.cs
Outdated
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/Queries/Parsing/ResolveFieldChainCallback.cs
Outdated
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryTokenizer.cs
Outdated
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/Queries/Expressions/LogicalOperator.cs
Outdated
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/Queries/Parsing/ResolveFieldChainCallback.cs
Outdated
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/QueryStrings/FilterQueryStringParameterReader.cs
Show resolved
Hide resolved
src/JsonApiDotNetCore/Internal/Queries/Parsing/ResourceFieldChainResolver.cs
Show resolved
Hide resolved
… Renamed QueryParser to QueryExpressionParser, because that is the ultimate base expression type being produced.
"Everybody knew it was impossible, until someone came along that didn't know."
Overview
I'm proud to present this PR, which brings the following enhancements:
1. Composable filter expressions
Examples:
Returns all users with last name O'Brian and age in range 25-35.
Returns users whose last name either ends in Conner or equals Smith, Williams or Miller.
Returns blogs that have at least one article. Only includes articles that have a publication date.
Returns articles from blog 1 where the number of upvotes exceeds the number of downvotes.
2. Deeply nested filtering, pagination, sorting, and sparse fieldsets
Examples:
Returns at most 10 blogs at page 2. Per blog, returns at most 5 included articles, all at page 3. Per blog, per article, returns included comments at page 4 using default page size.
Sorts blogs by title, then ID in descending order. Sorts included articles descending by publication date.
Returns article summaries from blog with ID 1. Returns titles and usernames from included comments.
3. New extensibility points in ResourceDefinitions to adapt, replace or discard query string input
virtual IReadOnlyCollection<IncludeElementExpression> OnApplyIncludes(IReadOnlyCollection<IncludeElementExpression> existingIncludes)
virtual FilterExpression OnApplyFilter(FilterExpression existingFilter)
virtual PaginationExpression OnApplyPagination(PaginationExpression existingPagination)
virtual SortExpression OnApplySort(SortExpression existingSort)
virtual SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet)
But the best part is:
All of these get translated into SQL queries, nothing is evaluated in memory!
4. Service/repository separation
IQueryable<T>
is only used internally in repository, enabling service reuse for alternate data stores.5. Additional minor enhancements
~AttrCapabilities.AllowView
to omit attribute from responses; if turned off, fails with HTTP 400 when requested in?fields=
ICurrentRequest.IsReadOnly
enables to use a different connection string for read-only requestsId
columnIntegrationTestContext
) and cascaded table truncate (ClearTableAsync
)So how does it work?
The query pipeline roughly looks like this:
Processing a request involves the following steps:
JsonApiMiddleware
collects resource info from routing data for the current request. (unchanged)JsonApiReader
transforms json request body into objects. (unchanged)JsonApiController
accepts get/post/patch/delete verb and delegates to service. (unchanged)IQueryStringParameterReader
s delegate toQueryParser
s that transform query string text intoQueryExpression
objects. (new)IQueryConstraintProvider
, which exposes expressions throughExpressionInScope
objects.QueryLayerComposer
(used fromJsonApiResourceService
) collects all query constraints. (new)ResourceDefinition
overrides and composes a tree ofQueryLayer
objects.JsonApiResourceService
contains no more usage ofIQueryable
.EntityFrameworkCoreRepository
delegates toQueryableBuilder
to transform theQueryLayer
tree intoIQueryable
expression trees. (new)QueryBuilder
depends onQueryClauseBuilder
implementations that visit the tree nodes, transforming them toSystem.Linq.Expression
equivalents.The
IQueryable
expression trees are executed by EF Core, which produces SQL statements out of them.Example
To get a sense of what this all looks like, let's look at an example query string:
After parsing, the set of scoped expressions is transformed into the following tree by
QueryLayerComposer
:Next, the repository translates this into a LINQ query that the following C# code would represent:
Breaking changes
options.EnableLegacyFilterNotation
totrue
to allow legacy filtersTo use new notation, prefix with "expr:", for example:
?filter=expr:equals(lastName,'Smith')
ResourceDefinition<T>.HideFields()
has been replaced byResourceDefinition<T>.OnApplySparseFieldSet()
ResourceDefinition<T>.GetQueryFilters()
has been replaced byResourceDefinition<T>.OnRegisterQueryableHandlersForQueryStringParameters
These are no longer tied to only filters. For example:
?filter[isHighRisk]=true
now uses:?isHighRisk=true
DefaultResourceService
->JsonApiResourceService
DefaultResourceRepository
->EntityFrameworkCoreRepository
BaseJsonApiController.GetRelationshipsAsync
->GetRelationshipAsync
BaseJsonApiController.GetRelationshipAsync
->GetSecondaryAsync
AttrCapabilities.AllowMutate
->AllowChange
Default
prefix was removed from various class namesLegacy filter conversion table
?filter[attribute]=value
?filter=equals(attribute,'value')
?filter[attribute]=ne:value
?filter=not(equals(attribute,'value'))
?filter[attribute]=lt:10
?filter=lessThan(attribute,'10')
?filter[attribute]=gt:10
?filter=greaterThan(attribute,'10')
?filter[attribute]=le:10
?filter=lessOrEqual(attribute,'10')
?filter[attribute]=ge:10
?filter=greaterOrEqual(attribute,'10')
?filter[attribute]=like:value
?filter=contains(attribute,'value')
?filter[attribute]=in:value1,value2
?filter=any(attribute,'value1,'value2')
?filter[attribute]=nin:value1,value2
?filter=not(any(attribute,'value1,'value2'))
?filter[attribute]=isnull:
?filter=equals(attribute,null)
?filter[attribute]=isnotnull:
?filter=not(equals(attribute,null))
Fixed issues
Fixes #788
Fixes #787
Fixes #764
Fixes #761
Fixes #758
Fixes #757
Fixes #751
Fixes #748
Fixes #747
Fixes #738
Fixes #551
Fixes #444
Fixes #217
Fixes #183
Fixes #176
This PR supersedes the following PRs: #705, #782.
Known limitations
These bugs in EF Core prevent some scenarios from working correctly. Please upvote them if they are important for you.