QueryKit 0.3.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package QueryKit --version 0.3.0                
NuGet\Install-Package QueryKit -Version 0.3.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="QueryKit" Version="0.3.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add QueryKit --version 0.3.0                
#r "nuget: QueryKit, 0.3.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install QueryKit as a Cake Addin
#addin nuget:?package=QueryKit&version=0.3.0

// Install QueryKit as a Cake Tool
#tool nuget:?package=QueryKit&version=0.3.0                

QueryKit 🎛️

QueryKit is a .NET library that makes it easier to query your data by providing a fluent and intuitive syntax for filtering and sorting. The main use case is a lighter weight subset of OData or GraphQL for parsing external filtering and sorting inputs to provide more granular consumption (e.g. a React UI provides filtering controls to filter a worklist). It's inspired by Sieve, but with some differences.

Getting Started

dotnet add package QueryKit

If we wanted to apply a filter to a DbSet called People, we just have to do something like this:

var filterInput = """FirstName == "Jane" && Age > 10""";
var people = _dbContext.People
  	.ApplyQueryKitFilter(filterInput)
  	.ToList();

QueryKit will automatically translate this into an expression for you. You can even customize your property names:

var filterInput = """first == "Jane" && Age > 10""";
var config = new QueryKitConfiguration(config =>
{
    config.Property<Person>(x => x.FirstName).HasQueryName("first");
});
var people = _dbContext.People
  	.ApplyQueryKitFilter(filterInput, config)
  	.ToList();

Sorting works too:

var filterInput = """first == "Jane" && Age > 10""";
var config = new QueryKitConfiguration(config =>
{
    config.Property<Person>(x => x.FirstName).HasQueryName("first");
});
var people = _dbContext.People
  	.ApplyQueryKitFilter(filterInput, config)
  	.ApplyQueryKitSort("first, Age desc", config)
  	.ToList();

And that's the basics! There's no services to inject or global set up to worry about, just apply what you want and call it a day. For a full list of capables, see below.

Filtering

Usage

To apply filters to your queryable, you just need to pass an input string with your filtering input to ApplyQueryKitFilter off of a queryable:

var people = _dbContext.People.ApplyQueryKitFilter("Age < 10").ToList();

You can also pass a configuration like this. More on configuration options below.

var config = new QueryKitConfiguration(config =>
{
    config.Property<SpecialPerson>(x => x.FirstName)
     	 		.HasQueryName("first")
      		.PreventSort();
});
var people = _dbContext.People
  		.ApplyQueryKitFilter("first == "Jane" && Age < 10", config)
  		.ToList();

Logical Operators

When filtering, you can use logical operators && for and as well as || for or. For example:

var input = """FirstName == "Jane" && Age < 10""";
var input = """FirstName == "Jane" || FirstName == "John" """;

Additionally, you can use ^^ for an in operator. You can add an * and use ^^* for case-insensitivity as well:

var input = """(Age ^^ [20, 30, 40]) && (BirthMonth ^^* ["January", "February", "March"]) || (Id ^^ ["6d623e92-d2cf-4496-a2df-f49fa77328ee"])""";

Order of Operations

You can use order of operation with parentheses like this:

var input = """(FirstName == "Jane" && Age < 10) || FirstName == "John" """;

Comparison Operators

There's a wide variety of comparison operators that use the same base syntax as Sieve's operators. To do a case-insensitive operation, just append a * at the end of the operator.

Name Operator Case Insensitive Operator
Equals == ==*
Not Equals != !=
Greater Than > N/A
Less Than < N/A
Greater Than Or Equal >= N/A
Less Than Or Equal N/A
Starts With _= _=*
Does Not Start With !_= !_=*
Ends With _-= _-=*
Does Not End With !_-= !_-=*
Contains @= @=*
Does Not Contain !@= !@=*

Filtering Notes

  • string and guid properties should be wrapped in double quotes

    • null doesn't need quotes: var input = "Title == null";

    • Double quotes can be escaped by using a similar syntax to raw-string literals introduced in C#11:

      var input = """""Title == """lamb is great on a "gee-ro" not a "gy-ro" sandwich""" """"";
      // OR 
      var input = """""""""Title == """"lamb is great on a "gee-ro" not a "gy-ro" sandwich"""" """"""""";
      
  • Dates and times use ISO format:

    • DateOnly: var filterInput = "Birthday == 2022-07-01";

    • DateTimeOffset:

      • var filterInput = "Birthday == 2022-07-01T00:00:03Z";
    • DateTime: var filterInput = "Birthday == 2022-07-01";

      • var filterInput = "Birthday == 2022-07-01T00:00:03";
      • var filterInput = "Birthday == 2022-07-01T00:00:03+01:00";
    • TimeOnly: var filterInput = "Time == 12:30:00";

  • bool properties need to use == true, == false, or the same using the != operator. they can not be standalone properies:

    • var input = """Title == "chicken & waffles" && Favorite""";
    • var input = """Title == "chicken & waffles" && Favorite == true""";
Complex Example
var input = """(Title == "lamb" && ((Age >= 25 && Rating < 4.5) || (SpecificDate <= 2022-07-01T00:00:03Z && Time == 00:00:03)) && (Favorite == true || Email.Value _= "hello@gmail.com"))""";

Settings

Property Settings

Filtering is set up to create an expression using the property names you have on your entity, but you can pass in a config to customize things a bit when needed.

  • HasQueryName() to create a custom alias for a property. For exmaple, we can make FirstName aliased to first.
  • PreventFilter() to prevent filtering on a given property
var input = $"""first == "Jane" || Age > 10""";
var config = new QueryKitConfiguration(config =>
{
    config.Property<SpecialPerson>(x => x.FirstName)
     	 		.HasQueryName("first");
    config.Property<SpecialPerson>(x => x.Age)
      		.PreventFilter();
});
Custom Operators

You can also add custom comparison operators to your config if you'd like:

var config = new QueryKitConfiguration(config =>
{
    config.EqualsOperator = "@@$";
    config.CaseInsensitiveAppendix = "$";
    config.AndOperator = "and";
});

If you want to use it globally, you can make a base implementation like this:

public class CustomQueryKitConfiguration : QueryKitConfiguration
{
    public CustomQueryKitConfiguration(Action<QueryKitSettings>? configureSettings = null)
        : base(settings => 
        {
            settings.EqualsOperator = "eq";
            settings.NotEqualsOperator = "neq";
            settings.GreaterThanOperator = "gt";
            settings.GreaterThanOrEqualOperator = "gte";
            settings.LessThanOperator = "lt";
            settings.LessThanOrEqualOperator = "lte";
            settings.ContainsOperator = "ct";
            settings.StartsWithOperator = "sw";
            settings.EndsWithOperator = "ew";
            settings.NotContainsOperator = "nct";
            settings.NotStartsWithOperator = "nsw";
            settings.NotEndsWithOperator = "new";
            settings.AndOperator = "and";
            settings.OrOperator = "or";
            settings.CaseInsensitiveAppendix = "i";

            configureSettings?.Invoke(settings);
        })
    {
    }
}

// ---

var input = """Title eq$ "Pancakes" and Rating gt 10""";
var config = new CustomQueryKitConfiguration();
var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);

Note Spaces must be used around the comparison operator when using custom values. Title @@$ "titilating"Title@@$"titilating"

Allow Unknown Properties

By default, QueryKit will throw an error if it doesn't recognize a property name, If you want to loosen the reigns here a bit, you can set AllowUnknownProperties to true in your config. When active, unknown properties will be ignored in the expression resolution.

var config = new QueryKitConfiguration(config =>
{
    config.AllowUnknownProperties = true;
});
var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);

Nested Objects

Say we have a nested object like this:


public class SpecialPerson
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public EmailAddress Email { get; set; }
}

public class EmailAddress : ValueObject
{
    public EmailAddress(string? value)
    {
        Value = value;
    }
    
    public string? Value { get; private set; }
}

To actually use the nested properties, you can do something like this:

var input = $"""Email.Value == "{value}" """;

// or with an alias...
var input = $"""email == "hello@gmail.com" """;
var config = new QueryKitConfiguration(config =>
{
    config.Property<SpecialPerson>(x => x.Email.Value).HasQueryName("email");
});

Note, with EF core, your config might look like this:

public sealed class PersonConfiguration : IEntityTypeConfiguration<SpecialPerson>
{
    public void Configure(EntityTypeBuilder<SpecialPerson> builder)
    {
        builder.HasKey(x => x.Id);
        
      // Option 1
        builder.Property(x => x.Email)
            .HasConversion(x => x.Value, x => new EmailAddress(x))
            .HasColumnName("email");      
      
        // Option 2      
        builder.OwnsOne(x => x.Email, opts =>
        {
            opts.Property(x => x.Value).HasColumnName("email");
        }).Navigation(x => x.Email)
            .IsRequired();
    }
}

Warning EF properties configured with HasConversion are not supported at this time -- if this is a blocker for you, i'd love to hear your use case

Sorting

Sorting is a more simplistic flow. It's just an input with a comma delimited list of properties to sort by.

Rules

  • use asc or desc to designate if you want it to be ascending or descending. If neither is used, QueryKit will assume asc
  • You can use Sieve syntax as well by prefixing a property with - to designate it as desc
  • Spaces after commas are optional

So all of these are valid:

var input = "Title";
var input = "Title, Age desc";
var input = "Title desc, Age desc";
var input = "Title, Age";
var input = "Title asc, -Age";
var input = "Title, -Age";

Property Settings

Sorting is set up to create an expression using the property names you have on your entity, but you can pass in a config to customize things a bit when needed.

  • Just as with filtering, HasQueryName() to create a custom alias for a property. For exmaple, we can make FirstName aliased to first.
  • PreventSort() to prevent filtering on a given property
var input = $"""Age desc, first"";
var config = new QueryKitConfiguration(config =>
{
    config.Property<SpecialPerson>(x => x.FirstName)
          .HasQueryName("first")
          .PreventSort();
});

Error Handling

If you want to capture errors to easily throw a 400, you can add error handling aronud these exceptions:

  • A FilterParsingException will be thrown when there is an invalid operator or bad syntax is used (e.g. not using double quotes around a string or guid).
  • An UnknownFilterPropertyException will be thrown if a property is not recognized during filtering
  • A SortParsingException will be thrown if a property or operation is not recognized during sorting
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on QueryKit:

Package Downloads
KayordKit

KayordKit to guide the way

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.5.0 496 12/5/2024
1.4.4 2,153 11/2/2024
1.4.3 2,382 9/7/2024
1.4.2 1,726 8/14/2024
1.4.1 2,256 7/19/2024
1.4.0 6,394 6/23/2024
1.3.3 4,198 4/16/2024
1.3.2 646 4/12/2024
1.3.1 104 4/12/2024
1.3.0 1,611 3/24/2024
1.2.1 10,453 1/20/2024
1.2.0 325 1/16/2024
1.1.0 7,221 12/22/2023
1.0.0 432 11/15/2023
0.6.1 1,277 9/8/2023
0.6.0 688 7/24/2023
0.5.0-pre002 163 7/19/2023
0.5.0-pre001 100 7/19/2023
0.4.0 195 7/11/2023
0.3.0 148 6/2/2023
0.2.0 155 5/1/2023
0.1.1 146 5/1/2023
0.1.0 156 4/29/2023