Filtering
since v4.0
Resources can be filtered by attributes using the filter
query string parameter.
By default, all attributes are filterable.
The filtering strategy we have selected, uses the following form.
?filter=expression
Expressions are composed using the following functions:
Operation | Function | Example |
---|---|---|
Equality | equals |
?filter=equals(lastName,'Smith') |
Less than | lessThan |
?filter=lessThan(age,'25') |
Less than or equal to | lessOrEqual |
?filter=lessOrEqual(lastModified,'2001-01-01') |
Greater than | greaterThan |
?filter=greaterThan(duration,'6:12:14') |
Greater than or equal to | greaterOrEqual |
?filter=greaterOrEqual(percentage,'33.33') |
Contains text | contains |
?filter=contains(description,'cooking') |
Starts with text | startsWith |
?filter=startsWith(description,'The') |
Ends with text | endsWith |
?filter=endsWith(description,'End') |
Equals one value from set | any |
?filter=any(chapter,'Intro','Summary','Conclusion') |
Collection contains items | has |
?filter=has(articles) |
Type-check derived type (v5) | isType |
?filter=isType(,men) |
Negation | not |
?filter=not(equals(lastName,null)) |
Conditional logical OR | or |
?filter=or(has(orders),has(invoices)) |
Conditional logical AND | and |
?filter=and(has(orders),has(invoices)) |
Comparison operators compare an attribute against a constant value (between quotes), null or another attribute:
GET /users?filter=equals(displayName,'Brian O''Connor') HTTP/1.1
GET /users?filter=equals(displayName,null) HTTP/1.1
GET /users?filter=equals(displayName,lastName) HTTP/1.1
Comparison operators can be combined with the count
function, which acts on to-many relationships:
GET /blogs?filter=lessThan(count(owner.articles),'10') HTTP/1.1
GET /customers?filter=greaterThan(count(orders),count(invoices)) HTTP/1.1
When filters are used multiple times on the same resource, they are combined using an OR operator. The next request returns all customers that have orders -or- whose last name is Smith.
GET /customers?filter=has(orders)&filter=equals(lastName,'Smith') HTTP/1.1
Aside from filtering on the resource being requested (which would be blogs in /blogs and articles in /blogs/1/articles), filtering on to-many relationships can be done using bracket notation:
GET /articles?include=author,tags&filter=equals(author.lastName,'Smith')&filter[tags]=any(label,'tech','design') HTTP/1.1
In the above request, the first filter is applied on the collection of articles, while the second one is applied on the nested collection of tags.
Warning
The request above does not hide articles without any matching tags! Use the has
function with a filter condition (see below) to accomplish that.
Putting it all together, you can build quite complex filters, such as:
GET /blogs?include=owner.articles.revisions&filter=and(or(equals(title,'Technology'),has(owner.articles)),not(equals(owner.lastName,null)))&filter[owner.articles]=equals(caption,'Two')&filter[owner.articles.revisions]=greaterThan(publishTime,'2005-05-05') HTTP/1.1
since v4.2
The has
function takes an optional filter condition as second parameter, for example:
GET /customers?filter=has(orders,not(equals(status,'Paid'))) HTTP/1.1
Which returns only customers that have at least one unpaid order.
since v5.0
Use the isType
filter function to perform a type check on a derived type. You can pass a nested filter, where the derived fields are accessible.
Only return men:
GET /humans?filter=isType(,men) HTTP/1.1
Only return men with beards:
GET /humans?filter=isType(,men,equals(hasBeard,'true')) HTTP/1.1
The first parameter of isType
can be used to perform the type check on a to-one relationship path.
Only return people whose best friend is a man with children:
GET /humans?filter=isType(bestFriend,men,has(children)) HTTP/1.1
Only return people who have at least one female married child:
GET /humans?filter=has(children,isType(,woman,not(equals(husband,null)))) HTTP/1.1
Legacy filters
The next section describes how filtering worked in versions prior to v4.0. They are always applied on the set of resources being requested (no nesting). Legacy filters use the following form.
?filter[attribute]=value
For operations other than equality, the query can be prefixed with an operation identifier. Examples can be found in the table below.
Operation | Prefix | Example | Equivalent form in v4.0 |
---|---|---|---|
Equality | eq |
?filter[lastName]=eq:Smith |
?filter=equals(lastName,'Smith') |
Non-equality | ne |
?filter[lastName]=ne:Smith |
?filter=not(equals(lastName,'Smith')) |
Less than | lt |
?filter[age]=lt:25 |
?filter=lessThan(age,'25') |
Less than or equal to | le |
?filter[lastModified]=le:2001-01-01 |
?filter=lessOrEqual(lastModified,'2001-01-01') |
Greater than | gt |
?filter[duration]=gt:6:12:14 |
?filter=greaterThan(duration,'6:12:14') |
Greater than or equal to | ge |
?filter[percentage]=ge:33.33 |
?filter=greaterOrEqual(percentage,'33.33') |
Contains text | like |
?filter[description]=like:cooking |
?filter=contains(description,'cooking') |
Equals one value from set | in |
?filter[chapter]=in:Intro,Summary,Conclusion |
?filter=any(chapter,'Intro','Summary','Conclusion') |
Equals none from set | nin |
?filter[chapter]=nin:one,two,three |
?filter=not(any(chapter,'one','two','three')) |
Equal to null | isnull |
?filter[lastName]=isnull: |
?filter=equals(lastName,null) |
Not equal to null | isnotnull |
?filter[lastName]=isnotnull: |
?filter=not(equals(lastName,null)) |
Filters can be combined and will be applied using an OR operator. This used to be AND in versions prior to v4.0.
Attributes to filter on can optionally be prefixed with to-one relationships, for example:
GET /api/articles?include=author&filter[caption]=like:marketing&filter[author.lastName]=Smith HTTP/1.1
Legacy filter notation can still be used in v4.0 by setting options.EnableLegacyFilterNotation
to true
.
If you want to use the new filter notation in that case, prefix the parameter value with expr:
, for example:
GET /articles?filter[caption]=tech&filter=expr:equals(caption,'cooking')) HTTP/1.1
Custom Filters
There are multiple ways you can add custom filters:
- Implementing
IResourceDefinition.OnApplyFilter
(see here) and injectIRequestQueryStringAccessor
, which works at all depths, but filter operations are constrained to whatFilterExpression
provides - Implementing
IResourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters
as described here, which enables the full range ofIQueryable<T>
functionality, but only works on primary endpoints - Add an implementation of
IQueryConstraintProvider
to supply additionalFilterExpression
s, which are combined with existing filters using AND operator - Override
EntityFrameworkCoreRepository.ApplyQueryLayer
to adapt theIQueryable<T>
expression just before execution - Take a deep dive and plug into reader/parser/tokenizer/visitor/builder for adding additional general-purpose filter operators
Filter syntax
For reference, we provide the EBNF grammar for filter expressions below (in ANTLR4 style):
grammar Filter;
filterExpression:
notExpression
| logicalExpression
| comparisonExpression
| matchTextExpression
| anyExpression
| hasExpression;
notExpression:
'not' LPAREN filterExpression RPAREN;
logicalExpression:
( 'and' | 'or' ) LPAREN filterExpression ( COMMA filterExpression )* RPAREN;
comparisonExpression:
( 'equals' | 'greaterThan' | 'greaterOrEqual' | 'lessThan' | 'lessOrEqual' ) LPAREN (
countExpression | fieldChain
) COMMA (
countExpression | literalConstant | 'null' | fieldChain
) RPAREN;
matchTextExpression:
( 'contains' | 'startsWith' | 'endsWith' ) LPAREN fieldChain COMMA literalConstant RPAREN;
anyExpression:
'any' LPAREN fieldChain ( COMMA literalConstant )+ RPAREN;
hasExpression:
'has' LPAREN fieldChain ( COMMA filterExpression )? RPAREN;
countExpression:
'count' LPAREN fieldChain RPAREN;
fieldChain:
FIELD ( '.' FIELD )*;
literalConstant:
ESCAPED_TEXT;
LPAREN: '(';
RPAREN: ')';
COMMA: ',';
fragment OUTER_FIELD_CHARACTER: [A-Za-z0-9];
fragment INNER_FIELD_CHARACTER: [A-Za-z0-9_-];
FIELD: OUTER_FIELD_CHARACTER ( INNER_FIELD_CHARACTER* OUTER_FIELD_CHARACTER )?;
ESCAPED_TEXT: '\'' ( ~['] | '\'\'' )* '\'' ;
LINE_BREAKS: [\r\n]+ -> skip;