QueryKit

repository·main·Indexed 41 days ago

https://github.com/pdevito3/querykit

A .NET library that parses lightweight, string-based filtering and sorting inputs and translates them into LINQ expressions for use with IQueryable, DbSet, or IEnumerable<T>. It supports complex operations including arithmetic expressions, property list grouping, collection filtering, and custom property mapping via QueryKitConfiguration.

Tokens
4.7K
Snippets
17
Records
18
Agent score
2%

What's inside QueryKit

  1. Perform arithmetic expressions in filters

    main

    You can perform calculations directly within your queries using standard arithmetic operators: +, -, *, /, and % (modulo). These expressions are translated to efficient SQL by Entity Framework.

    Examples:

    // Find records where Age + Rating is greater than 50
    var input = "(Age + Rating) > 50";
    
    // Calculate profit margin and filter
    var profitableItems = _dbContext.Products
        .ApplyQueryKitFilter("((Price - Cost) / Price) > 0.2")
        .ToList();
    var input = "(Age + Rating) > 50";
  2. Compare properties to each other

    main

    QueryKit allows comparing one property directly to another property on the same entity (or nested properties). This is useful for dynamic filtering where the comparison value is a field rather than a literal.

    Examples:

    var input = """FirstName == LastName""";
    
    // Compare child property to root property
    var input = """Author.Name == Title""";
    // Compare two string properties
    var input = """FirstName == LastName""";
  3. Use Property List Grouping for multi-field searches

    main

    Property list grouping allows you to apply a single comparison across multiple properties using the syntax (Prop1, Prop2, ...) Operator Value.

    Logic Rules:

    • Positive Operators (==, @=, _=, ^$, ^^, etc.): Uses OR logic. Matches if the condition is true for at least one property.
    • Negative Operators (!=, !@=, !_=, !^$, !^^, etc.): Uses AND logic. Matches only if the condition is true for every property.

    Examples:

    // Search for "paul" in either FirstName OR LastName
    var input = """(FirstName, LastName) @=* \"paul\" """;
    
    // Search for "test" in neither FirstName nor LastName (AND logic)
    var input = """(FirstName, LastName) !@=* \"test\" """;
    // Search for "paul" in either FirstName OR LastName
    var input = """(FirstName, LastName) @=* \"paul\" """;
  4. Filter collections and nested collections

    main

    QueryKit can filter into collections.

    • Default Behavior: Uses Any logic (matches if any element satisfies the condition).
    • All Logic: Prefix the operator with % to use All logic (e.g., %>=).
    • Primitive Collections (List<string>, etc.): Use ^$ (Has) or !^$ (Does Not Have).
    • Collection Counts: Prefix operators with # (e.g., #>=).

    Examples:

    // Filter recipes that have an ingredient named "salt"
    var input = """Ingredients.Name == \"salt\" """;
    
    // Filter for recipes where all ingredients have stock >= 1
    var input = """Ingredients.Stock %>= 1""";
    
    // Filter primitive string collection for "winner"
    var input = """Tags ^$ \"winner\" """;
    
    // Get recipes with more than 0 ingredients
    var input = """Ingredients #>= 0""";
    var input = """Ingredients.Name == \"salt\" """;
  5. Configure SoundEx for DbContext

    main

    The Sounds Like (~~) and Does Not Sound Like operators require a SoundEx configuration on your DbContext. You must define a static method in your DbContext that acts as a pointer to the database's soundex function using the [DbFunction] attribute.

    Note for Postgres users: You must add modelBuilder.HasPostgresExtension("fuzzystrmatch"); in OnModelCreating and include the extension in a migration.

    Usage with QueryKit: Because QueryKit cannot reliably extract the DbContext from an IQueryable, you must explicitly provide the DbContextType in the QueryKitConfiguration when using SoundEx operators.

    // 1. Define the DbContext with SoundEx support
    public class ExampleDbContext : DbContext
    {
        public ExampleDbContext(DbContextOptions<ExampleDbContext> options) : base(options) { }
        
        [DbFunction (Name = "SOUNDEX", IsBuiltIn = true)]
        public static string SoundsLike(string query) => throw new NotImplementedException();
    
        public DbSet<People> MyPeople { get; set; }
        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.HasPostgresExtension("fuzzystrmatch");
        }
    }
    
    // 2. Use it in a QueryKit filter
    var input = $"""LastName ~~ \"devito\""" ";
    var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, new QueryKitConfiguration(o =>
    {
        o.DbContextType = typeof(ExampleDbContext);
    }));
  6. Sort data using SortOrder strings

    main

    Sorting in QueryKit is performed using a comma-delimited list of properties. You can specify the direction using asc (ascending) or desc (descending). If no direction is specified, asc is the default. You can also use Sieve syntax by prefixing a property with a minus sign (-) to indicate descending order. Spaces after commas are optional.

    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";
  7. Use QueryKit on IEnumerable<T>

    main

    QueryKit can be used on in-memory collections (IEnumerable<T>) by using the ApplyQueryKitFilter and ApplyQueryKitSort extension methods. This treats the input string as a parser for expressions against the enumerable items.

    var recipeOne = new FakeRecipeBuilder().Build();
    var recipeTwo = new FakeRecipeBuilder().Build();
    var listOfRecipes = new List<Recipe> { recipeOne, recipeTwo };
    
    var input = $"""{nameof(Recipe.Title)} == \"{recipeOne.Title}\" """ ";
    
    var filteredRecipes = listOfRecipes.ApplyQueryKitFilter(input).ToList();
  8. Handle EF Core HasConversion properties in QueryKit

    main

    If an EF Core property is configured using .HasConversion(...), you must configure it in QueryKit using the parent property and call .HasConversion<TTarget>().

    Crucial Rule: Do NOT point to the nested .Value property when using HasConversion. Point to the property that holds the conversion.

    Correct Pattern:

    builder.Property(x => x.Email)
        .HasConversion(x => x.Value, x => new EmailAddress(x));
    
    // QueryKit Configuration
    var config = new QueryKitConfiguration(config =>
    {
        config.Property<SpecialPerson>(x => x.Email) // Point to Email, NOT Email.Value
            .HasQueryName("email")
            .HasConversion<string>();
    });
    
    // Filter usage
    var input = """email == \"hello@gmail.com\"""";
    var config = new QueryKitConfiguration(config =>
    {
        config.Property<SpecialPerson>(x => x.Email)  // Point to Email property, NOT Email.Value
            .HasQueryName("email")
            .HasConversion<string>(); // Specify the target type used in HasConversion
    });
  9. Configure QueryKit via QueryKitConfiguration

    main

    Use QueryKitConfiguration to customize how filters are resolved.

    Key Settings:

    • HasQueryName(string): Create an alias for a property.
    • PreventFilter(): Disable filtering on a specific property.
    • DerivedProperty<T>(expression): Expose a computed property for querying.
    • CustomOperation<T>(...): Define complex business logic as a queryable operation.
    • AllowUnknownProperties = true: Ignore unknown property names instead of throwing an error.
    • MaxPropertyDepth: Limit how deep nested property access can go (e.g., Author.Address.City).
    • CaseInsensitiveComparison: Set global mode to CaseInsensitiveMode.Upper or Lower.

    Example: Custom Operation

    {
        config.CustomOperation<Book>((x, op, value) => 
            x.Orders.Where(y => y.OrderDate > DateTime.UtcNow.AddDays(-10))
                   .Sum(o => o.Quantity) > (int)value)
            .HasQueryName("SoldUnitsMoreThan10Days");
    });
    // Usage: "SoldUnitsMoreThan10Days > 100"
    {
        config.Property<SpecialPerson>(x => x.FirstName)
                .HasQueryName("first")
          	.PreventSort();
    });
  10. Apply filters to a DbSet using ApplyQueryKitFilter

    main

    You can apply string-based filter inputs directly to an IQueryable (such as a DbSet) using the ApplyQueryKitFilter extension method. QueryKit translates the string expression into a LINQ expression automatically.

    var filterInput = """FirstName == \"Jane\" && Age > 10""";
    var people = _dbContext.People
      	.ApplyQueryKitFilter(filterInput)
      	.ToList();
  11. Apply sorting using ApplyQueryKitSort

    main

    Sorting can be applied to your query using the ApplyQueryKitSort method. It accepts a comma-separated string of property names and direction indicators (e.g., desc). You can pass a QueryKitConfiguration to handle custom property names during sorting.

    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();