{"id":18768471,"url":"https://github.com/pdevito3/querykit","last_synced_at":"2026-02-19T02:05:39.051Z","repository":{"id":158973248,"uuid":"634348817","full_name":"pdevito3/QueryKit","owner":"pdevito3","description":"🎛️ QueryKit is a .NET library that makes it easier to query your data by providing a fluent and intuitive syntax for filtering and sorting.","archived":false,"fork":false,"pushed_at":"2025-12-13T14:46:31.000Z","size":368,"stargazers_count":177,"open_issues_count":10,"forks_count":18,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-12-23T04:55:45.004Z","etag":null,"topics":["dotnet","filter","filtering","graphql-alternative","odata","sort","sorting"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pdevito3.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["pdevito3"]}},"created_at":"2023-04-29T20:39:11.000Z","updated_at":"2025-12-13T14:46:35.000Z","dependencies_parsed_at":"2024-08-14T11:47:22.594Z","dependency_job_id":"148c059b-5740-4860-bad2-5017e9b15983","html_url":"https://github.com/pdevito3/QueryKit","commit_stats":null,"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/pdevito3/QueryKit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pdevito3%2FQueryKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pdevito3%2FQueryKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pdevito3%2FQueryKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pdevito3%2FQueryKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pdevito3","download_url":"https://codeload.github.com/pdevito3/QueryKit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pdevito3%2FQueryKit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29600853,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T00:59:38.239Z","status":"online","status_checked_at":"2026-02-19T02:00:07.702Z","response_time":117,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dotnet","filter","filtering","graphql-alternative","odata","sort","sorting"],"created_at":"2024-11-07T19:12:52.907Z","updated_at":"2026-02-19T02:05:39.043Z","avatar_url":"https://github.com/pdevito3.png","language":"C#","readme":"# QueryKit 🎛️\n\nQueryKit 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](https://github.com/Biarity/Sieve), but with some differences.\n\n## Getting Started\n\n```bash\ndotnet add package QueryKit\n```\n\nIf we wanted to apply a filter to a `DbSet` called `People`, we just have to do something like this:\n\n```c#\nvar filterInput = \"\"\"FirstName == \"Jane\" \u0026\u0026 Age \u003e 10\"\"\";\nvar people = _dbContext.People\n  \t.ApplyQueryKitFilter(filterInput)\n  \t.ToList();\n```\n\nQueryKit will automatically translate this into an expression for you. You can even customize your property names:\n\n```c#\nvar filterInput = \"\"\"first == \"Jane\" \u0026\u0026 Age \u003e 10\"\"\";\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cPerson\u003e(x =\u003e x.FirstName).HasQueryName(\"first\");\n});\nvar people = _dbContext.People\n  \t.ApplyQueryKitFilter(filterInput, config)\n  \t.ToList();\n```\n\nSorting works too:\n\n```c#\nvar filterInput = \"\"\"first == \"Jane\" \u0026\u0026 Age \u003e 10\"\"\";\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cPerson\u003e(x =\u003e x.FirstName).HasQueryName(\"first\");\n});\nvar people = _dbContext.People\n  \t.ApplyQueryKitFilter(filterInput, config)\n  \t.ApplyQueryKitSort(\"first, Age desc\", config)\n  \t.ToList();\n```\n\nAnd 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.\n\n## Filtering\n\n### Usage\n\nTo apply filters to your queryable, you just need to pass an input string with your filtering input to `ApplyQueryKitFilter` off of a queryable:\n\n```c#\nvar people = _dbContext.People.ApplyQueryKitFilter(\"Age \u003c 10\").ToList();\n```\n\nYou can also pass a configuration like this. More on configuration options below.\n\n```c#\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cSpecialPerson\u003e(x =\u003e x.FirstName)\n            .HasQueryName(\"first\")\n      \t\t.PreventSort();\n});\nvar people = _dbContext.People\n  \t\t.ApplyQueryKitFilter(@$\"first == \"Jane\" \u0026\u0026 Age \u003c 10\", config)\n  \t\t.ToList();\n```\n\n### Logical Operators\n\nWhen filtering, you can use logical operators `\u0026\u0026` for `and` as well as `||` for `or`. For example:\n\n```c#\nvar input = \"\"\"FirstName == \"Jane\" \u0026\u0026 Age \u003c 10\"\"\";\nvar input = \"\"\"FirstName == \"Jane\" || FirstName == \"John\" \"\"\";\n```\n\n### Order of Operations\n\nYou can use order of operation with parentheses like this:\n\n```c#\nvar input = \"\"\"(FirstName == \"Jane\" \u0026\u0026 Age \u003c 10) || FirstName == \"John\" \"\"\";\n```\n\n### Comparison Operators\n\nThere's a wide variety of comparison operators that use the same base syntax as [Sieve](https://github.com/Biarity/Sieve)'s operators. To do a case-insensitive operation, just append a ` *` at the end of the operator.\n\n| Name                  | Operator | Case Insensitive Operator | Count Operator |\n| --------------------- | -------- | ------------------------- | -------------- |\n| Equals                | ==       | ==*                       | #==            |\n| Not Equals            | !=       | !=*                       | #!=            |\n| Greater Than          | \u003e        | N/A                       | #\u003e             |\n| Less Than             | \u003c        | N/A                       | #\u003c             |\n| Greater Than Or Equal | \u003e=       | N/A                       | #\u003e=            |\n| Less Than Or Equal    | \u003c=       | N/A                       | #\u003c=            |\n| Starts With           | _=       | _=*                       | N/A            |\n| Does Not Start With   | !_=      | !_=*                      | N/A            |\n| Ends With             | _-=      | _-=*                      | N/A            |\n| Does Not End With     | !_-=     | !_-=*                     | N/A            |\n| Contains              | @=       | @=*                       | N/A            |\n| Does Not Contain      | !@=      | !@=*                      | N/A            |\n| Sounds Like           | ~~       | N/A                       | N/A            |\n| Does Not Sound Like   | !~       | N/A                       | N/A            |\n| Has                   | ^$       | ^$*                       | N/A            |\n| Does Not Have         | !^$      | !^$*                      | N/A            |\n| In                    | ^^       | ^^*                       | N/A            |\n| Not In                | !^^      | !^^*                      | N/A            |\n\n\u003e `Sounds Like` and `Does Not Sound Like` requires a soundex configuration on your DbContext. For more info see [the docs below](#soundex)\n\nHere's an example for the `in` operator:\n\n```c#\nvar input = \"\"\"(Age ^^ [20, 30, 40]) \u0026\u0026 (BirthMonth ^^* [\"January\", \"February\", \"March\"]) || (Id ^^ [\"6d623e92-d2cf-4496-a2df-f49fa77328ee\"])\"\"\";\n```\n\n### Property List Grouping\n\nProperty list grouping allows you to apply a single comparison operation across multiple properties, making it easy to search for a value in any of several fields without writing repetitive conditions.\n\n#### Basic Syntax\n\nUse parentheses with comma-separated property names followed by an operator and value:\n\n```c#\n// Search for \"paul\" in either FirstName OR LastName\nvar input = \"\"\"(FirstName, LastName) @=* \"paul\" \"\"\";\n\n// Equivalent to:\n// FirstName @=* \"paul\" || LastName @=* \"paul\"\n```\n\n#### How It Works\n\nProperty list grouping uses different logical operators depending on whether you're using positive or negative comparison operators:\n\n**Positive Operators** (uses **OR** logic - match if found in ANY field):\n- `==`, `@=`, `@=*`, `_=`, `_=*`, `_-=`, `_-=*`, `^$`, `^^`\n- Match if the condition is true for **at least one** property\n\n**Negative Operators** (uses **AND** logic - match if NOT found in ALL fields):\n- `!=`, `!@=`, `!@=*`, `!_=`, `!_=*`, `!_-=`, `!_-=*`, `!^$`, `!^^`\n- Match only if the condition is true for **every** property\n\n#### Examples\n\n**Case-insensitive search across multiple text fields:**\n```c#\nvar input = \"\"\"(FirstName, LastName, Email) @=* \"john\" \"\"\";\n// Finds records where \"john\" appears in FirstName OR LastName OR Email\n```\n\n**Exact match on any of several properties:**\n```c#\nvar input = \"\"\"(Status, Type, Category) == \"Active\" \"\"\";\n// Finds records where Status, Type, OR Category equals \"Active\"\n```\n\n**Numeric comparison across multiple fields:**\n```c#\nvar input = \"\"\"(Age, Rating, Score) \u003e 25\"\"\";\n// Finds records where Age, Rating, OR Score is greater than 25\n```\n\n**Negative operators (NOT contains):**\n```c#\nvar input = \"\"\"(FirstName, LastName) !@=* \"test\" \"\"\";\n// Finds records where \"test\" is NOT in FirstName AND NOT in LastName\n// Both fields must not contain \"test\"\n```\n\n**Not equals:**\n```c#\nvar input = \"\"\"(Status, Type) != \"Inactive\" \"\"\";\n// Finds records where Status != \"Inactive\" AND Type != \"Inactive\"\n// Both fields must not equal \"Inactive\"\n```\n\n#### Advanced Usage\n\n**Combined with other conditions:**\n```c#\nvar input = \"\"\"(FirstName, LastName) @=* \"smith\" \u0026\u0026 Age \u003e 25 \u0026\u0026 Status == \"Active\" \"\"\";\n// Search for \"smith\" in name fields AND apply additional filters\n```\n\n**Multiple property lists in one query:**\n```c#\nvar input = \"\"\"(FirstName, LastName) @=* \"john\" \u0026\u0026 (Email, Phone) @=* \"555\" \"\"\";\n// Search across different property groups\n```\n\n**Complex logical expressions:**\n```c#\nvar input = \"\"\"((FirstName, LastName) @=* \"smith\" || Age == 30) \u0026\u0026 Status == \"Active\" \"\"\";\n// Combine property list grouping with parentheses and other operators\n```\n\n**Nested properties:**\n```c#\nvar input = \"\"\"(Author.Name, Author.Email, Title) @=* \"john\" \"\"\";\n// Search across nested object properties\n```\n\n**With numeric comparisons:**\n```c#\nvar input = \"\"\"(Age, YearsOfExperience, Rating) \u003e= 5\"\"\";\n// Find records where any numeric field meets the criteria\n```\n\n#### Use Cases\n\nProperty list grouping is particularly useful for:\n\n1. **Search functionality** - Search across multiple text fields (name, email, title, etc.)\n2. **Multi-field validation** - Ensure a value doesn't appear in any of several fields\n3. **Status checks** - Check if any status field has a particular value\n4. **Flexible filtering** - Allow users to search without knowing which specific field contains the data\n\n#### Notes\n\n- Works with **all comparison operators** listed in the comparison operators table\n- Can include the **same property multiple times** if needed: `(FirstName, FirstName, LastName)`\n- Supports **any number of properties** in the list\n- Handles **whitespace** around commas gracefully: `( FirstName , LastName )`\n- **Type safety** - All properties in the list should be compatible with the comparison value type\n- **Nullable properties** are handled automatically\n\n### Filtering Notes\n\n* `string` and `guid` properties should be wrapped in double quotes\n\n  * `null` doesn't need quotes: `var input = \"Title == null\";`\n\n  * Double quotes can be escaped by using a similar syntax to raw-string literals introduced in C#11:\n\n    ```c#\n    var input = \"\"\"\"\"Title == \"\"\"lamb is great on a \"gee-ro\" not a \"gy-ro\" sandwich\"\"\" \"\"\"\"\";\n    // OR \n    var input = \"\"\"\"\"\"\"\"\"Title == \"\"\"\"lamb is great on a \"gee-ro\" not a \"gy-ro\" sandwich\"\"\"\" \"\"\"\"\"\"\"\"\";\n    ```\n\n* Dates and times use ISO 8601 format and should be surrounded by double quotes:\n\n  * `DateOnly`: `var filterInput = \"\"\"Birthday == \"2022-07-01\" \"\"\";`\n  * `DateTimeOffset`: \n    * `var filterInput = \"\"\"Birthday == \"2022-07-01T00:00:03Z\" \"\"\";` \n  * `DateTime`: `var filterInput = \"\"\"Birthday == \"2022-07-01\" \"\"\";`\n    * `var filterInput = \"\"\"Birthday == \"2022-07-01T00:00:03\" \"\"\";` \n    * `var filterInput = \"\"\"Birthday == \"2022-07-01T00:00:03+01:00\" \"\"\";` \n\n  * `TimeOnly`: \n    * `var filterInput = \"\"\"Time == \"12:30:00\" \"\"\";`\n    * `var filterInput = \"\"\"Time == \"12:30:00.678722\" \"\"\";`\n\n* `bool` properties need to use `== true`, `== false`, or the same using the `!=` operator. they can not be standalone properies: \n\n  * ❌ `var input = \"\"\"Title == \"chicken \u0026 waffles\" \u0026\u0026 Favorite\"\"\";` \n  * ✅ `var input = \"\"\"Title == \"chicken \u0026 waffles\" \u0026\u0026 Favorite == true\"\"\";` \n\n#### Complex Example\n\n```c#\nvar input = \"\"\"(Title == \"lamb\" \u0026\u0026 ((Age \u003e= 25 \u0026\u0026 Rating \u003c 4.5) || (SpecificDate \u003c= \"2022-07-01T00:00:03Z\" \u0026\u0026 Time == \"00:00:03\")) \u0026\u0026 (Favorite == true || Email.Value _= \"hello@gmail.com\"))\"\"\";\n```\n\n### Property-to-Property Comparisons\n\nQueryKit supports comparing one property directly to another property on the same entity. This allows for dynamic filtering where the comparison value is another field rather than a literal value:\n\n```c#\n// Compare two string properties\nvar input = \"\"\"FirstName == LastName\"\"\";\n\n// Compare numeric properties  \nvar input = \"\"\"Age \u003e Rating\"\"\";\n\n// Use in complex expressions\nvar input = \"\"\"(FirstName != LastName \u0026\u0026 Age \u003e Rating) || (Score1 \u003c= Score2)\"\"\";\n\n// Combine with regular filters\nvar input = \"\"\"FirstName == LastName \u0026\u0026 Age \u003e 21\"\"\";\n```\n\nProperty-to-property comparisons work with all comparison operators and automatically handle type conversions between compatible numeric types (e.g., comparing `int` with `decimal`).\n\n#### Child Property Comparisons\n\nYou can also compare child properties to root properties or other child properties:\n\n```c#\n// Compare child property to root property\nvar input = \"\"\"Author.Name == Title\"\"\";\n\n// Compare nested child properties\nvar input = \"\"\"Email.Value == CollectionEmail.Value\"\"\";\n\n// Mix child and root properties in complex expressions\nvar input = \"\"\"Author.Name != Title \u0026\u0026 Rating \u003e Author.Score\"\"\";\n```\n\nChild property comparisons work with:\n- **Nested Objects**: `Author.Name`, `Email.Value`\n- **All Operators**: `==`, `!=`, `\u003e`, `\u003c`, `\u003e=`, `\u003c=`\n- **Type Conversion**: Automatic conversion between compatible types\n- **Complex Expressions**: Can be combined with logical operators and parentheses\n\n### Arithmetic Expressions\n\nQueryKit supports arithmetic expressions in filters, allowing you to perform calculations directly within your queries. This enables powerful filtering capabilities based on computed values.\n\n#### Basic Arithmetic Operations\n\n```c#\n// Addition: Find records where Age + Rating is greater than 50\nvar input = \"(Age + Rating) \u003e 50\";\n\n// Subtraction: Find records where Age - Rating is positive\nvar input = \"(Age - Rating) \u003e 0\";\n\n// Multiplication: Find records where Price * Quantity exceeds 1000\nvar input = \"(Price * Quantity) \u003e 1000\";\n\n// Division: Find records where Total / Count is less than 100\nvar input = \"(Total / Count) \u003c 100\";\n\n// Modulo: Find records where ID is even\nvar input = \"(Id % 2) == 0\";\n```\n\n#### Operator Precedence\n\nArithmetic expressions follow standard mathematical operator precedence:\n\n```c#\n// Multiplication and division before addition and subtraction\nvar input = \"(Price + Tax * Rate) \u003e 100\";  // Tax * Rate is calculated first\n\n// Use parentheses to override precedence\nvar input = \"((Price + Tax) * Rate) \u003e 100\"; // Addition happens first\n```\n\n#### Mixing Properties and Literals\n\nYou can combine entity properties with literal numeric values:\n\n```c#\n// Property with literal\nvar input = \"(Age - 18) \u003e= 0\";   // Age minus 18\n\n// Multiple properties with literals\nvar input = \"(Price * 1.1 + ShippingCost) \u003c= Budget\";\n```\n\n#### Complex Arithmetic Expressions\n\nArithmetic expressions can be combined with logical operators and used in complex scenarios:\n\n```c#\n// Arithmetic with logical operators\nvar input = \"\"\"(Price - Cost) \u003e 300 \u0026\u0026 Category == \"Electronics\" \"\"\";\n\n// Multiple arithmetic comparisons\nvar input = \"(Score1 + Score2) \u003e 150 \u0026\u0026 (Score1 - Score2) \u003c 20\";\n\n// Nested arithmetic expressions\nvar input = \"((Revenue - Expenses) / Revenue) \u003e 0.1\";\n```\n\n#### Supported Features\n\n- **All Numeric Types**: `int`, `decimal`, `double`, `float`, `long`, `short`, `byte` and their nullable variants\n- **Automatic Type Conversion**: Compatible numeric types are automatically converted for calculations\n- **Parentheses**: Use parentheses to control calculation order and group expressions\n- **Entity Framework Translation**: All arithmetic expressions are translated to efficient SQL queries\n- **Property-to-Property**: Can mix property references with literal values in the same expression\n\n#### Examples\n\n```c#\n// Calculate profit margin and filter\nvar profitableItems = _dbContext.Products\n    .ApplyQueryKitFilter(\"((Price - Cost) / Price) \u003e 0.2\")\n    .ToList();\n\n// Find orders with high shipping ratio\nvar expensiveShipping = _dbContext.Orders\n    .ApplyQueryKitFilter(\"(ShippingCost / TotalAmount) \u003e 0.15\")\n    .ToList();\n\n// Complex business logic in one filter\nvar qualifiedCustomers = _dbContext.Customers\n    .ApplyQueryKitFilter(\"\"\"\n        (TotalPurchases / NumberOfOrders) \u003e 500 \u0026\u0026 \n        ((LastOrderDate - FirstOrderDate) / 365) \u003e= 2\n    \"\"\")\n    .ToList();\n```\n\n#### Filtering Projections\nYou can also filter on queryable projections like so:\n```csharp\nvar input = $\"\"\"info @=* \"{fakeAuthorOne.Name}\" \"\"\";\n\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cRecipeDto\u003e(x =\u003e x.AuthorInfo).HasQueryName(\"info\");\n});\n\nvar queryableRecipe = testingServiceScope.DbContext().Recipes\n    .Include(x =\u003e x.Author)\n    .Select(x =\u003e new RecipeDto\n    {\n        Id = x.Id,\n        Title = x.Title,\n        AuthorName = x.Author.Name,\n        AuthorInfo = x.Author.Name + \" - \" + x.Author.InternalIdentifier\n    });\nvar appliedQueryable = queryableRecipe.ApplyQueryKitFilter(input, config);\nvar recipes = await appliedQueryable.ToListAsync();\n```\n\n#### Filtering Raw SQL Projections\n\nQueryKit can also be used with raw SQL queries via EF Core's `SqlQueryRaw`. This is particularly useful when you need to work with complex SQL queries that include joins, aggregations, or database-specific functions. \n\n\u003e 💡 You can use projections without using raw sql\n\nHere's an example using a school domain:\n\n```csharp\npublic class StudentEnrollmentDto\n{\n    public Guid Id { get; set; }\n    public string StudentFirstName { get; set; }\n    public string StudentLastName { get; set; }\n    public string StudentFullName { get; set; }\n    public int StudentAge { get; set; }\n    public Guid CourseId { get; set; }\n    public string CourseName { get; set; }\n    public DateTime EnrolledOn { get; set; }\n    public DateTime? CourseStartDate { get; set; }\n}\n\nvar sql =\n$\"\"\"\nSELECT\n    e.id as \"Id\",\n    COALESCE(s.first_name, '') as \"StudentFirstName\",\n    COALESCE(s.last_name, '') as \"StudentLastName\",\n    COALESCE(s.first_name, '') || ' ' || COALESCE(s.last_name, '') as \"StudentFullName\",\n    s.age as \"StudentAge\",\n    e.course_id as \"CourseId\",\n    c.name as \"CourseName\",\n    e.enrolled_on as \"EnrolledOn\",\n    c.start_date as \"CourseStartDate\"\nFROM enrollments e\nLEFT JOIN students s ON e.student_id = s.id\nLEFT JOIN courses c ON e.course_id = c.id\nWHERE e.is_deleted = false\n\"\"\";\n\nvar projection = dbContext.Database.SqlQueryRaw\u003cStudentEnrollmentDto\u003e(sql);\n\nvar queryKitConfig = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.StudentFirstName).HasQueryName(\"studentFirstName\");\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.StudentLastName).HasQueryName(\"studentLastName\");\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.StudentFullName).HasQueryName(\"studentName\");\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.StudentAge).HasQueryName(\"studentAge\");\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.CourseId).HasQueryName(\"courseId\");\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.CourseName).HasQueryName(\"courseName\");\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.EnrolledOn).HasQueryName(\"enrolledOn\");\n    config.Property\u003cStudentEnrollmentDto\u003e(x =\u003e x.CourseStartDate).HasQueryName(\"courseStartDate\");\n});\n\nvar queryKitData = new QueryKitData\n{\n    Filters = request.QueryParameters.Filters,\n    SortOrder = request.QueryParameters.SortOrder,\n    Configuration = queryKitConfig\n};\n\nvar appliedCollection = projection.ApplyQueryKit(queryKitData);\nvar results = await appliedCollection.ToListAsync();\n```\n\n**Key Points:**\n- Raw SQL projections work seamlessly with QueryKit's filtering and sorting\n- You can include computed fields (like `StudentFullName`) and nested fields from joins (like `CourseStartDate`)\n- Property mappings allow you to use friendly query names that differ from the DTO property names\n- The resulting queryable can be further filtered and sorted by QueryKit before materializing to a list\n\n#### Filtering Collections\n\nYou can also filter into collections with QueryKit by using most of the normal operators. For example, if I wanted to filter for recipes that only have an ingredient named `salt`, I could do something like this:\n\n```csharp\nvar input = \"\"\"\"Ingredients.Name == \"salt\" \"\"\"\";\n```\n\nBy default, QueryKit will use `Any` under the hood when building this filter, but if you want to use `All`, you just need to prefix the operator with a `%`:\n\n```csharp\nvar input = \"\"\"\"Ingredients.Stock %\u003e= 1\"\"\"\";\n```\n\nNested collections can also be filtered:\n\n```csharp\t\nvar input = $\"\"\"preparations == \"{preparationOne.Text}\" \"\"\";\nvar config = new QueryKitConfiguration(settings =\u003e\n{\n    settings.Property\u003cRecipe\u003e(x =\u003e x.Ingredients\n        .SelectMany(y =\u003e y.Preparations)\n        .Select(y =\u003e y.Text))\n        .HasQueryName(\"preparations\");\n});\n\n// Act\nvar queryableRecipes = testingServiceScope.DbContext().Recipes;\nvar appliedQueryable = queryableRecipes.ApplyQueryKitFilter(input, config);\nvar recipes = await appliedQueryable.ToListAsync();\n```\n\nIf you want to filter a primitve collection like `List\u003cstring\u003e` you can use the `Has` or `DoesNotHave` operator (can be case insensitive with the appended `*`):\n\n```csharp\nvar input = \"\"\"Tags ^$ \"winner\" \"\"\";\n// or\nvar input = \"\"\"Tags !^$ \"winner\" \"\"\";\n```\n\nIf you want to filter on the count of a collection, you can prefix some of the operators with a `#`. For example, if i wanted to get all recipes that have more than 0 ingredients:\n\n```csharp\nvar input = \"\"\"\"Ingredients #\u003e= 0\"\"\"\";\n```\n\n#### Filtering Enums\n\nYou can filter enums with their respective integer value:\n\n```csharp\nvar input = \"BirthMonth == 1\";\n\npublic enum BirthMonthEnum\n{\n    January = 1,\n    February = 2,\n    //...\n}\n```\n\n### Settings\n\n#### Property Settings\n\nFiltering 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.\n\n* `HasQueryName()` to create a custom alias for a property. For exmaple, we can make `FirstName` aliased to `first`.\n* `PreventFilter()` to prevent filtering on a given property\n\n```c#\nvar input = $\"\"\"first == \"Jane\" || Age \u003e 10\"\"\";\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cSpecialPerson\u003e(x =\u003e x.FirstName)\n            .HasQueryName(\"first\");\n    config.Property\u003cSpecialPerson\u003e(x =\u003e x.Age)\n      \t\t.PreventFilter();\n});\n```\n\n#### Derived Properties\n\nYou can also expose custom derived properties for consumption. Just be sure that Linq can handle them in a db query if you're using it that way.\n\n```csharp\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.DerivedProperty\u003cPerson\u003e(p =\u003e p.FirstName + \" \" + p.LastName).HasQueryName(\"fullname\");\n    config.DerivedProperty\u003cPerson\u003e(p =\u003e p.Age \u003e= 18 \u0026\u0026 p.FirstName == \"John\").HasQueryName(\"adult_johns\");\n});\n\nvar input = $\"\"\"(fullname @=* \"John Doe\") \u0026\u0026 age \u003e= 18\"\"\";\n// or\nvar input = $\"\"\"adult_johns == true\"\"\";\n```\n\n#### Custom Operations\n\nFor more complex business logic that can't be expressed as simple derived properties, you can define custom operations. These allow you to encapsulate sophisticated filtering logic that can access related data, perform calculations, or implement domain-specific rules.\n\n```csharp\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    // Define a custom operation that checks if a book has sold more than X units in the last 10 days\n    config.CustomOperation\u003cBook\u003e((x, op, value) =\u003e \n        x.Orders.Where(y =\u003e y.OrderDate \u003e DateTime.UtcNow.AddDays(-10))\n               .Sum(o =\u003e o.Quantity) \u003e (int)value)\n        .HasQueryName(\"SoldUnitsMoreThan10Days\");\n    \n    // Custom operation for VIP customer detection\n    config.CustomOperation\u003cCustomer\u003e((x, op, value) =\u003e \n        (bool)value ? \n            (x.TotalPurchases \u003e 10000 \u0026\u0026 x.AccountAge \u003e 365 \u0026\u0026 x.FirstName.Contains(\"VIP\")) :\n            !(x.TotalPurchases \u003e 10000 \u0026\u0026 x.AccountAge \u003e 365 \u0026\u0026 x.FirstName.Contains(\"VIP\")))\n        .HasQueryName(\"isVipCustomer\");\n    \n    // Custom operation with date parameter handling\n    config.CustomOperation\u003cOrder\u003e((x, op, value) =\u003e \n        x.OrderDate \u003e (DateTime)value)\n        .HasQueryName(\"isAfterDate\");\n});\n\n// Usage examples:\nvar input = \"\"\"SoldUnitsMoreThan10Days \u003e 100\"\"\";        // Books that sold more than 100 units\nvar input = \"\"\"isVipCustomer == true\"\"\";                // VIP customers only\nvar input = \"\"\"isAfterDate == \"2023-06-15T00:00:00Z\" \"\"\"; // Orders after specific date\n```\n\n**Custom Operation Features:**\n\n- **Business Logic Encapsulation**: Complex domain logic can be centralized and reused\n- **Related Data Access**: Can navigate to related entities and collections (e.g., `x.Orders`, `x.Items`)\n- **Operator Access**: The operation receives the comparison operator being used\n- **Type-Safe Parameters**: Automatic conversion of string values to appropriate types (bool, int, decimal, DateTime, etc.)\n- **Entity Framework Compatible**: Generated expressions translate to efficient SQL queries\n\n**Common Use Cases:**\n\n- **Performance Metrics**: Calculate efficiency ratios, averages, or complex aggregations\n- **Business Intelligence**: Revenue calculations, customer scoring, inventory analysis\n- **Time-Based Logic**: Recent activity checks, age calculations, expiration rules\n- **Customer Segmentation**: VIP status, loyalty tiers, purchase behavior patterns\n- **Quality Control**: Average ratings, compliance checks, threshold validations\n\n**Parameter Type Conversion:**\n\nCustom operations automatically handle common data types:\n\n```csharp\n// Boolean parameters\nvar input = \"\"\"isEligible == true\"\"\";      // Converts \"true\" to bool\n\n// Numeric parameters  \nvar input = \"\"\"totalScore \u003e 85.5\"\"\";       // Converts \"85.5\" to decimal/double\nvar input = \"\"\"itemCount \u003e= 10\"\"\";         // Converts \"10\" to int\n\n// Date parameters\nvar input = \"\"\"lastLoginAfter == \"2023-12-01T00:00:00Z\" \"\"\"; // Converts to DateTime\n\n// String parameters (with quotes)\nvar input = \"\"\"categoryMatches == \"electronics\" \"\"\"; // Keeps as string\n```\n\nCustom operations provide a powerful way to extend QueryKit's filtering capabilities while maintaining type safety and Entity Framework compatibility.\n\n#### Custom Operators\n\nYou can also add custom comparison operators to your config if you'd like:\n```csharp\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.EqualsOperator = \"@@$\";\n    config.CaseInsensitiveAppendix = \"$\";\n    config.AndOperator = \"and\";\n});\n```\n\nIf you want to use it globally, you can make a base implementation like this:\n\n```csharp\npublic class CustomQueryKitConfiguration : QueryKitConfiguration\n{\n    public CustomQueryKitConfiguration(Action\u003cQueryKitSettings\u003e? configureSettings = null)\n        : base(settings =\u003e \n        {\n            settings.EqualsOperator = \"eq\";\n            settings.NotEqualsOperator = \"neq\";\n            settings.GreaterThanOperator = \"gt\";\n            settings.GreaterThanOrEqualOperator = \"gte\";\n            settings.LessThanOperator = \"lt\";\n            settings.LessThanOrEqualOperator = \"lte\";\n            settings.ContainsOperator = \"ct\";\n            settings.StartsWithOperator = \"sw\";\n            settings.EndsWithOperator = \"ew\";\n            settings.NotContainsOperator = \"nct\";\n            settings.NotStartsWithOperator = \"nsw\";\n            settings.NotEndsWithOperator = \"new\";\n            settings.AndOperator = \"and\";\n            settings.OrOperator = \"or\";\n            settings.CaseInsensitiveAppendix = \"i\";\n\n            configureSettings?.Invoke(settings);\n        })\n    {\n    }\n}\n\n// ---\n\nvar input = \"\"\"Title eq$ \"Pancakes\" and Rating gt 10\"\"\";\nvar config = new CustomQueryKitConfiguration();\nvar filterExpression = FilterParser.ParseFilter\u003cRecipe\u003e(input, config);\n```\n\n\u003e **Note**\n\u003e Spaces must be used around the comparison operator when using custom values.\n\u003e `Title @@$ \"titilating\"` ✅ \n\u003e `Title@@$\"titilating\"` ❌\n\n#### Allow Unknown Properties\n\nBy 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.\n```csharp\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.AllowUnknownProperties = true;\n});\nvar filterExpression = FilterParser.ParseFilter\u003cRecipe\u003e(input, config);\n```\n\n#### Case-Insensitive Comparison Mode\n\nBy default, QueryKit uses `ToLower()` (which EF Core translates to `LOWER()` in SQL) for case-insensitive string operators like `@=*`, `_=*`, `==*`, etc. If your data is normalized to uppercase, you can switch to `ToUpper()` / `UPPER()` to maintain index efficiency.\n\n```csharp\nvar config = new QueryKitConfiguration(settings =\u003e\n{\n    settings.CaseInsensitiveComparison = CaseInsensitiveMode.Upper;\n});\n```\n\nThis is particularly useful when:\n- Data is stored in uppercase for consistency\n- Database indexes are defined on uppercase column values (e.g., `CREATE INDEX ON people (UPPER(email))`)\n- You need SARGable queries for better performance\n\n**Per-property override** — you can also control the mode per property, overriding the global setting:\n\n```csharp\nvar config = new QueryKitConfiguration(settings =\u003e\n{\n    settings.CaseInsensitiveComparison = CaseInsensitiveMode.Lower; // global default\n    settings.Property\u003cPerson\u003e(x =\u003e x.Email).HasCaseInsensitiveMode(CaseInsensitiveMode.Upper);\n});\n```\n\n#### Max Property Depth\n\nYou can limit the depth of nested property access to prevent deeply nested queries. This is useful for security and performance reasons when exposing QueryKit to external consumers.\n\n```csharp\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.MaxPropertyDepth = 2; // Allow up to 2 levels of nesting (e.g., Author.Address.City)\n});\n\n// These will work:\n// \"Author.Name\" (depth 1)\n// \"Author.Address.City\" (depth 2)\n\n// This will throw QueryKitPropertyDepthExceededException:\n// \"Author.Address.Country.Name\" (depth 3)\n```\n\nYou can also override the global depth limit for specific properties:\n\n```csharp\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.MaxPropertyDepth = 1; // Global limit of 1\n    config.Property\u003cBook\u003e(x =\u003e x.Author).HasMaxDepth(2); // Allow Author to go deeper\n});\n\n// \"Title\" works (depth 0)\n// \"Author.Name\" works (depth 1, within global limit)\n// \"Author.Address.City\" works (depth 2, uses per-property override)\n// \"Publisher.Address.City\" throws (depth 2, exceeds global limit of 1)\n```\n\n**Depth Calculation:**\n- `Title` = depth 0 (root property)\n- `Author.Name` = depth 1 (one level of nesting)\n- `Author.Address.City` = depth 2 (two levels of nesting)\n\nSetting `MaxPropertyDepth = 0` only allows root-level properties. A `null` value (default) allows unlimited depth.\n\n### Nested Objects\n\nSay we have a nested object like this:\n\n```C#\n\npublic class SpecialPerson\n{\n    public Guid Id { get; set; } = Guid.NewGuid();\n    public EmailAddress Email { get; set; }\n}\n\npublic class EmailAddress : ValueObject\n{\n    public EmailAddress(string? value)\n    {\n        Value = value;\n    }\n    \n    public string? Value { get; private set; }\n}\n```\n\nTo actually use the nested properties, you can do something like this:\n\n```c#\nvar input = $\"\"\"Email.Value == \"{value}\" \"\"\";\n\n// or with an alias...\nvar input = $\"\"\"email == \"hello@gmail.com\" \"\"\";\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cSpecialPerson\u003e(x =\u003e x.Email.Value).HasQueryName(\"email\");\n});\n```\n\nNote, with EF core, your QueryKit configuration depends on how you've configured the property:\n\n```c#\npublic sealed class PersonConfiguration : IEntityTypeConfiguration\u003cSpecialPerson\u003e\n{\n    public void Configure(EntityTypeBuilder\u003cSpecialPerson\u003e builder)\n    {\n        builder.HasKey(x =\u003e x.Id);\n\n      \t// Option 1 (as of .NET 8) - ComplexProperty\n      \t// QueryKit: config.Property\u003cSpecialPerson\u003e(x =\u003e x.Email.Value).HasQueryName(\"email\");\n      \tbuilder.ComplexProperty(x =\u003e x.Email,\n            x =\u003e x.Property(y =\u003e y.Value)\n                .HasColumnName(\"email\"));\n\n      \t// Option 2 - HasConversion (see HasConversion support below)\n      \t// QueryKit: config.Property\u003cSpecialPerson\u003e(x =\u003e x.Email).HasQueryName(\"email\").HasConversion\u003cstring\u003e();\n        builder.Property(x =\u003e x.Email)\n            .HasConversion(x =\u003e x.Value, x =\u003e new EmailAddress(x))\n            .HasColumnName(\"email\");\n\n        // Option 3 - OwnsOne\n        // QueryKit: config.Property\u003cSpecialPerson\u003e(x =\u003e x.Email.Value).HasQueryName(\"email\");\n        builder.OwnsOne(x =\u003e x.Email, opts =\u003e\n        {\n            opts.Property(x =\u003e x.Value).HasColumnName(\"email\");\n        }).Navigation(x =\u003e x.Email)\n            .IsRequired();\n    }\n}\n```\n\n**Key Distinction:**\n- **HasConversion**: Use `x =\u003e x.Email` in QueryKit (point to parent property)\n- **ComplexProperty/OwnsOne**: Use `x =\u003e x.Email.Value` in QueryKit (point to nested property)\n\n### HasConversion Support\n\nFor properties configured with EF Core's `HasConversion`, QueryKit provides special support that allows you to filter against the property directly without needing to access nested values. Use the `HasConversion\u003cTTarget\u003e()` configuration method:\n\n```c#\n// EF configuration with HasConversion\nbuilder.Property(x =\u003e x.Email)\n    .HasConversion(x =\u003e x.Value, x =\u003e new EmailAddress(x))\n    .HasColumnName(\"email\");\n\n// QueryKit configuration for HasConversion properties\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cSpecialPerson\u003e(x =\u003e x.Email)  // Point to Email property, NOT Email.Value\n        .HasQueryName(\"email\")\n        .HasConversion\u003cstring\u003e(); // Specify the target type used in HasConversion\n});\n\n// Now you can filter directly against the property:\nvar input = \"\"\"email == \"hello@gmail.com\" \"\"\";\nvar people = _dbContext.People\n    .ApplyQueryKitFilter(input, config)\n    .ToList();\n```\n\nThis allows you to use `Email == \"value\"` syntax instead of `Email.Value == \"value\"` when the property is configured with HasConversion in EF Core. The `HasConversion\u003cTTarget\u003e()` method tells QueryKit what the conversion target type is so it can handle the type conversion properly.\n\n\u003e **Important:** When using `HasConversion` in EF Core, you MUST configure the property in QueryKit using `x =\u003e x.Email`, not `x =\u003e x.Email.Value`. The conversion is on the parent property, so pointing to the nested `.Value` property will cause EF Core translation errors. Use `x =\u003e x.Email.Value` only when using `ComplexProperty` or `OwnsOne` without HasConversion.\n\n## Sorting\n\nSorting is a more simplistic flow. It's just an input with a comma delimited list of properties to sort by. \n\n### Rules\n\n* use `asc` or `desc` to designate if you want it to be ascending or descending. If neither is used, QueryKit will assume `asc`\n* You can use Sieve syntax as well by prefixing a property with `-` to designate it as `desc`\n* Spaces after commas are optional\n\nSo all of these are valid:\n\n```c#\nvar input = \"Title\";\nvar input = \"Title, Age desc\";\nvar input = \"Title desc, Age desc\";\nvar input = \"Title, Age\";\nvar input = \"Title asc, -Age\";\nvar input = \"Title, -Age\";\n```\n\n### Property Settings\n\nSorting 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.\n\n* Just as with filtering, `HasQueryName()` to create a custom alias for a property. For exmaple, we can make `FirstName` aliased to `first`.\n* `PreventSort()` to prevent filtering on a given property\n\n```c#\nvar input = $\"\"\"Age desc, first\"\";\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cSpecialPerson\u003e(x =\u003e x.FirstName)\n          .HasQueryName(\"first\")\n          .PreventSort();\n});\n```\n\n## Aggregate QueryKit Application\n\nIf you want to apply filtering and sorting in one fell swoop, you can do something like this:\n\n```csharp\nvar config = new QueryKitConfiguration(config =\u003e\n{\n    config.Property\u003cPerson\u003e(x =\u003e x.FirstName).HasQueryName(\"first\");\n});\nvar people = _dbContext.People\n  \t.ApplyQueryKit(new QueryKitData() \n        {\n            Filters = \"\"\"first == \"Jane\" \u0026\u0026 Age \u003e 10\"\"\",\n            SortOrder = \"first, Age desc\",\n            Configuration = config\n        })\n  \t.ToList();\n```\n\n## Using QueryKit on Enumerables\nSince QueryKit is really just a parser for expressions, you can use it on any `IEnumerable\u003cT\u003e` as well. Just be sure to use the `ApplyQueryKitFilter` and `ApplyQueryKitSort` methods off of the enumerable.\n\nFor example\n```csharp\nvar recipeOne = new FakeRecipeBuilder().Build();\nvar recipeTwo = new FakeRecipeBuilder().Build();\nvar listOfRecipes = new List\u003cRecipe\u003e { recipeOne, recipeTwo };\n\nvar input = $\"\"\"{nameof(Recipe.Title)} == \"{recipeOne.Title}\" \"\"\";\n\nvar filteredRecipes = listOfRecipes.ApplyQueryKitFilter(input).ToList();\n````\n\n\n## Error Handling\n\nIf you want to capture errors to easily throw a `400`, you can add error handling around these exceptions:\n\n* A `QueryKitException` is the base class for all of the exceptions listed below. This can be caught to catch\nany exception thrown by QueryKit.\n* A `ParsingException` 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).\n* An `UnknownFilterPropertyException` will be thrown if a property is not recognized during filtering\n* A `SortParsingException` will be thrown if a property or operation is not recognized during sorting\n* A `QueryKitDbContextTypeException` will be thrown when trying to use a `DbContext` specific workflow without passing that context (e.g. SoundEx)\n* A `SoundsLikeNotImplementedException` will be thrown when trying to use `soundex` on a `DbContext` that doesn't have it implemented.\n* A `QueryKitParsingException` is a more generic error that will include specific details on a more granular error in the parsing pipeline.\n* A `QueryKitPropertyDepthExceededException` will be thrown when a property path exceeds the configured `MaxPropertyDepth` limit.\n\n## SoundEx\n\nThe `Sounds Like` and `Does Not Sound Like` operators require a soundex configuration on any `DbContext` that contain your `DbSet` being filtered on. Something like the below should work. The `SoundsLike` method does not need to implement anything and is just used as a pointer to the db method.\n\n```csharp\npublic class ExampleDbContext : DbContext\n{\n    public ExampleDbContext(DbContextOptions\u003cTestingDbContext\u003e options)\n        : base(options)\n    {\n    }\n    \n    [DbFunction (Name = \"SOUNDEX\", IsBuiltIn = true)]\n    public static string SoundsLike(string query) =\u003e throw new NotImplementedException();\n\n    public DbSet\u003cPeople\u003e MyPeople { get; set; }\n    \n    protected override void OnModelCreating(ModelBuilder modelBuilder)\n    {\n        base.OnModelCreating(modelBuilder);\n        modelBuilder.HasPostgresExtension(\"fuzzystrmatch\");\n    }\n}\n```\n\n\u003e ⭐️ Note that with Postgres, something like `modelBuilder.HasPostgresExtension(\"fuzzystrmatch\");` will need to be added like the example along with a migration for adding the extension.\n\nYou can even use this on a normal `IQueryable` like this: \n```csharp\nvar waffleRecipes = _dbContext.MyPeople\n  .Where(x =\u003e ExampleDbContext.SoundsLike(x.LastName) == ExampleDbContext.SoundsLike(\"devito\"))\n  .ToList();\n```\n\n### Usage\n\nOnce your `DbContext` is configured to allow soundex, you'll need to provide that `DbContext` type in your QueryKit config. This is because, as of now, there is no reliable way to get the `DbContext` from an `IQueryable`.\n\n```csharp\nvar input = $\"\"\"LastName ~~ \"devito\" \"\"\";\n\n// Act\nvar queryablePeople = testingServiceScope.DbContext().People;\nvar appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, new QueryKitConfiguration(o =\u003e\n{\n    o.DbContextType = typeof(TestingDbContext);\n}));\nvar people = await appliedQueryable.ToListAsync();\n```\n\n## Community Projects\n- [Fluent QueryKit](https://github.com/CLFPosthumus/fluent-querykit) for easy usage in javascript or typescript projects.\n- [QueryKit Builder](https://github.com/notlimey/querykit-builder) a TypeScript query builder designed to work with","funding_links":["https://github.com/sponsors/pdevito3"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpdevito3%2Fquerykit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpdevito3%2Fquerykit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpdevito3%2Fquerykit/lists"}