Filtering

Hot Chocolate generates filter input types from your .NET models and translates client-supplied filter arguments into native database queries. The default implementation builds expression trees that apply to IQueryable, but you can customize filters for other data sources.

Given a model like User with a Name property, Hot Chocolate generates a UserFilterInput with string operations such as eq, contains, startsWith, and more. Clients use the where argument to filter results.

Getting Started#

Filtering is part of the HotChocolate.Data package.

Bash
dotnet add package HotChocolate.Data
Warning
All HotChocolate.* packages need to have the same version.

Register filtering on the schema:

C#
builder
    .AddGraphQL()
    .AddFiltering();

Apply the [UseFiltering] attribute to a resolver that returns IQueryable<T> or IEnumerable<T>:

Clients can now filter users:

GraphQL
query {
  users(where: { name: { contains: "Alice" } }) {
    name
    email
  }
}
Warning

Middleware order matters. When combining multiple middleware, apply them in this order: UsePaging > UseProjection > UseFiltering > UseSorting.

Filter Types#

Hot Chocolate generates filter operations based on the .NET type of each property.

String Filters#

Operations: eq, neq, contains, ncontains, in, nin, startsWith, nstartsWith, endsWith, nendsWith.

Boolean Filters#

Operations: eq, neq.

Comparable Filters#

For numeric types (int, long, float, double, decimal), Guid, DateTime, DateTimeOffset, and TimeSpan.

Operations: eq, neq, in, nin, gt, ngt, gte, ngte, lt, nlt, lte, nlte.

Enum Filters#

Operations: eq, neq, in, nin.

Object Filters#

Filters are generated for nested objects, supporting filtering across database relationships:

C#
public class User
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string City { get; set; }
}
GraphQL
query {
  users(where: { address: { city: { eq: "Berlin" } } }) {
    name
  }
}

List Filters#

For collection properties, Hot Chocolate generates all, none, some, and any operations:

GraphQL
query {
  users(where: { orders: { some: { total: { gt: 100 } } } }) {
    name
  }
}

Combining Filters with "and" / "or"#

Every filter input type includes and and or fields for composing multiple conditions:

GraphQL
query {
  users(
    where: {
      or: [{ name: { contains: "Alice" } }, { name: { contains: "Bob" } }]
    }
  ) {
    name
  }
}

The or field must be used at the top level of the filter. Placing it inside a field operation results in and semantics instead.

Removing "and" / "or"#

Disable these combinators in a custom filter type:

C#
public class UserFilterType : FilterInputType<User>
{
    protected override void Configure(IFilterInputTypeDescriptor<User> descriptor)
    {
        descriptor.AllowAnd(false).AllowOr(false);
    }
}

Custom Filter Types#

Customize which fields are filterable and which operations are available by extending FilterInputType<T>:

C#
public class UserFilterType : FilterInputType<User>
{
    protected override void Configure(IFilterInputTypeDescriptor<User> descriptor)
    {
        descriptor.BindFieldsExplicitly();
        descriptor.Field(f => f.Name);
        descriptor.Field(f => f.Email).Type<CustomStringOperationFilterInputType>();
    }
}

Restrict operations on a field by defining a custom operation type:

C#
public class CustomStringOperationFilterInputType : StringOperationFilterInputType
{
    protected override void Configure(IFilterInputTypeDescriptor descriptor)
    {
        descriptor.Operation(DefaultFilterOperations.Equals).Type<StringType>();
        descriptor.Operation(DefaultFilterOperations.Contains).Type<StringType>();
    }
}

Apply the custom filter type:

Filter Conventions#

Filter conventions let you change filtering behavior globally across your schema.

Setting Up a Convention#

Extend FilterConvention and override Configure, or use FilterConventionExtension to build on top of the defaults:

C#
public class CustomFilterConvention : FilterConvention
{
    protected override void Configure(IFilterConventionDescriptor descriptor)
    {
        descriptor.AddDefaults();
        descriptor.ArgumentName("filter");
    }
}
C#
builder
    .AddGraphQL()
    .AddConvention<IFilterConvention, CustomFilterConvention>();

Binding Filter Types Globally#

Bind custom filter types to .NET types through the convention:

C#
public class CustomFilterConvention : FilterConvention
{
    protected override void Configure(IFilterConventionDescriptor descriptor)
    {
        descriptor.AddDefaults();
        descriptor.BindRuntimeType<User, UserFilterType>();
    }
}

Custom Scalar Filters#

When you use custom scalars (including those from HotChocolate.Types.Scalars), you must create and bind a filter type for each scalar:

C#
public class EmailAddressOperationFilterInputType : FilterInputType
{
    protected override void Configure(IFilterInputTypeDescriptor descriptor)
    {
        descriptor.Operation(DefaultFilterOperations.Equals).Type<EmailAddressType>();
        descriptor.Operation(DefaultFilterOperations.NotEquals).Type<EmailAddressType>();
        descriptor.Operation(DefaultFilterOperations.Contains).Type<EmailAddressType>();
    }
}
C#
builder
    .AddGraphQL()
    .AddFiltering(x => x
        .AddDefaults()
        .BindRuntimeType<string, EmailAddressOperationFilterInputType>());

Next Steps#

Edit this page on GitHub
Last updated on by Tobias Tengler