What are filters?
With the Hot Chocolate filters you are able to expose complex filter object through your GraphQL API that translate to native database queries.
The default filter implementation translates filters to expression trees that are applied on IQueryable
.
Using Filters
Filters by default work on IQueryable
but you can also easily customize them to use other interfaces.
Hot Chocolate by default will inspect your .NET model and infer from that the possible filter operations.
The following type would yield the following filter operations:
public class Foo{ public string Bar { get; set; }}
input FooFilter { bar: String bar_contains: String bar_ends_with: String bar_in: [String] bar_not: String bar_not_contains: String bar_not_ends_with: String bar_not_in: [String] bar_not_starts_with: String bar_starts_with: String AND: [FooFilter!] OR: [FooFilter!]}
So how can we get started with filters?
Getting started with filters is very easy and if you do not want to explicitly define filters or customize anything then filters are super easy to use, lets have a look at that.
public class QueryType : ObjectType<Query>{ protected override void Configure(IObjectTypeDescriptor<Query> descriptor) { descriptor.Field(t => t.GetPersons(default)) .Type<ListType<NonNullType<PersonType>>>() .UseFiltering(); }}
public class Query{ public IQueryable<Person> GetPersons([Service]IPersonRepository repository) { repository.GetPersons(); }}
⚠️ Note: Be sure to install the
HotChocolate.Types.Filters
NuGet package.
In the above example the person resolver just returns the IQueryable
representing the data source. The IQueryable
represents a not executed database query on which we are able to apply filters.
The next thing to note is the UseFiltering
extension method which adds the filter argument to the field and a middleware that can apply those filters to the IQueryable
. The execution engine will in the end execute the IQueryable
and fetch the data.
Customizing Filters
The filter objects can be customized and you can rename and remove operations from it or define operations explicitly.
Filters are input objects and are defined through a FilterInputType<T>
. In order to define and customize a filter we have to inherit from FilterInputType<T>
and configure it like any other type.
public class PersonFilterType : FilterInputType<Person>{ protected override void Configure( IFilterInputTypeDescriptor<Person> descriptor) { descriptor .BindFieldsExplicitly() .Filter(t => t.Name) .BindOperationsExplicitly() .AllowEquals().Name("equals").And() .AllowContains().Name("contains").And() .AllowIn().Name("in"); }}
The above type defines explicitly for what fields filter operations are allowed and what filter operations are allowed. Also the filter renames the equals filter to equals
.
In order to apply this filter type we just have to provide the UseFiltering
extension method with the filter type as type argument.
public class QueryType : ObjectType<Query>{ protected override void Configure(IObjectTypeDescriptor<Query> descriptor) { descriptor.Field(t => t.GetPerson(default)) .Type<ListType<NonNullType<PersonType>>>(); .UseFiltering<PersonFilterType>() }}
AND / OR Filter
There are two built in fields.
AND
: Every condition has to be validOR
: At least one condition has to be valid
Example:
query { posts( first: 5 where: { OR: [{ title_contains: "Doe" }, { title_contains: "John" }] } ) { edges { node { id title } } }}
⚠️ OR does not work when you use it like this:
query { posts( first: 5 where: { title_contains: "John", OR: { title_contains: "Doe" } } ) { edges { node { id title } } }}
In this case the filters are applied like title_contains: "John" AND title_contains: "Doe"
Customizing Filter Transformation
With our filter solution you can write your own filter transformation which is fairly easy once you wrapped your head around transforming graphs with visitors.
We provide a FilterVisitorBase
which is the base of our QueryableFilterVisitor
and it is basically just implementing an new visitor that walks the filter graph and translates it into any other query syntax.
Sorting
Like with filter support you can add sorting support to your database queries.
public class QueryType : ObjectType<Query>{ protected override void Configure(IObjectTypeDescriptor<Query> descriptor) { descriptor.Field(t => t.GetPerson(default)) .Type<ListType<NonNullType<PersonType>>>(); .UseSorting() }}
Example:
query { person(order_by: { name: DESC }) { name age }}
⚠️ Note: Be sure to install the
HotChocolate.Types.Sorting
NuGet package.
If you want to combine for instance paging, filtering and sorting make sure that the order is like follows:
public class QueryType : ObjectType<Query>{ protected override void Configure(IObjectTypeDescriptor<Query> descriptor) { descriptor.Field(t => t.GetPerson(default)) .UsePaging<PersonType>() .UseFiltering() .UseSorting(); }}
Example:
query { person(order_by: { name: DESC }) { edges { node { id name } } }}
Why is the order important?
Paging, filtering and sorting are modular middleware which form the field resolver pipeline.
The above example basically forms the following pipeline:
Paging -> Filtering -> Sorting -> Field Resolver
The paging middleware will first delegate to the next middleware, which is filtering.
The filtering middleware will also first delegate to the next middleware, which is sorting.
The sorting middleware will again first delegate to the next middleware, which is the actual field resolver.
The field resolver will call GetPerson
which returns in this example an IQueryable<Person>
. The queryable represents a not yet executed database query.
After the resolver has been executed and put its result onto the middleware context the sorting middleware will apply the sort order on the query.
After the sorting middleware has been executed and updated the result on the middleware context the filtering middleware will apply its filters on the queryable and updates the result on the middleware context.
After the paging middleware has been executed and updated the result on the middleware context the paging middleware will slice the data and execute the queryable which will then actually pull in data from the data source.
So, if we for instance applied paging as our last middleware the data set would have been sliced first and then filtered which in most cases is not what we actually want.