A .NET library for parsing query parameters into LINQ expressions. Enables database-level filtering, sorting, and pagination from HTTP query strings.
Note
This project only supports Entity Framework Linq currently.
dotnet add package GoatQuery
# Or for ASP.NET Core integration please install this instead.
dotnet add package GoatQuery.AspNetCore// Basic filtering
var users = dbContext.Users
.Apply(new Query { Filter = "age gt 18 and isActive eq true" })
.Value.Query;
// Lambda expressions for collection filtering
var usersWithLondonAddress = dbContext.Users
.Apply(new Query { Filter = "addresses/any(x: x/city eq 'London')" })
.Value.Query;
// Filter by primitive arrays (tags, categories, etc.)
var vipUsers = dbContext.Users
.Apply(new Query { Filter = "tags/any(x: x eq 'vip')" })
.Value.Query;
// Complex nested filtering
var activeUsersWithHighValueOrders = dbContext.Users
.Apply(new Query {
Filter = "isActive eq true and orders/any(o: o/items/any(i: i/price gt 1000))"
})
.Value.Query;
// ASP.NET Core integration
[HttpGet]
[EnableQuery<UserDto>(maxTop: 100)]
public IActionResult GetUsers() => Ok(dbContext.Users);GET /api/users?filter=age gt 18 and isActive eq true
GET /api/users?filter=addresses/any(x: x/city eq 'London')
GET /api/users?orderby=lastName asc, firstName desc
GET /api/users?orderby=company/name asc
GET /api/users?filter=tags/any(x: x eq 'premium')
GET /api/users?top=10&skip=20&count=true
GET /api/users?search=john
- Comparison:
eq,ne,gt,gte,lt,lte - Logical:
and,or - String:
contains
| Type | Example |
|---|---|
| String | 'value', 'it\'s escaped', 'back\\slash' |
| Integer | 42, 99999999999, -7 |
| Double | 3.14, 1.0, -2.5 |
| Boolean | true, false |
| DateTime | 2023-12-25T10:30:00Z |
| DateTimeOffset | 2023-12-25T10:30:00+05:00 |
| Date | 2023-12-25 (expanded to full-day range) |
| GUID | 123e4567-e89b-12d3-a456-426614174000 |
| Enum | 'Active' (string) or 1 (integer) |
| Null | null |
String literals support backslash escaping: \' for a literal single quote, \\ for a literal backslash.
Access nested properties using forward slash (/) syntax:
filter=company/name eq 'TechCorp'
filter=profile/address/city eq 'London'
filter=user/addresses/any(x: x/country/name eq 'UK')
Filter collections using any() and all():
// any() - true if at least one element matches
addresses/any(x: x/city eq 'London')
// all() - true if all elements match (requires non-empty collection)
addresses/all(x: x/isVerified eq true)
// Nested lambda expressions
orders/any(o: o/items/any(i: i/price gt 100))
// Primitive array filtering
tags/any(x: x eq 'premium')
scores/any(x: x gt 80)
// Root property access inside lambda body
orders/any(o: o/total gt 100 and status eq 'Active')
Date-only literals (e.g., 2023-12-25) are automatically expanded to full-day range comparisons against DateTime/DateTimeOffset columns:
| Operator | Expansion |
|---|---|
eq |
>= 2023-12-25T00:00:00 AND < 2023-12-26T00:00:00 |
ne |
< 2023-12-25T00:00:00 OR >= 2023-12-26T00:00:00 |
lt |
< 2023-12-25T00:00:00 |
lte |
< 2023-12-26T00:00:00 |
gt |
>= 2023-12-26T00:00:00 |
gte |
>= 2023-12-25T00:00:00 |
String comparisons (eq, ne, contains) are null-safe. When a string property is null in the database:
eqandcontainsexclude null rows (null does not equal any value)neincludes null rows (null is "not equal" to any value)
age gt 18
firstName eq 'John' and isActive ne false
name contains 'smith'
status eq 'Active'
createdAt gte 2023-01-01T00:00:00Z
createdAt eq 2023-06-15
balance gt -100
addresses/any(x: x/city eq 'London' and x/isActive eq true)
age gt 25 and tags/any(x: x eq 'premium')
orders/any(o: o/total gt 50 and status eq 'Active')
Sort by one or more properties, including nested properties:
GET /api/users?orderby=lastName asc
GET /api/users?orderby=lastName asc, firstName desc
GET /api/users?orderby=company/name asc
GET /api/users?orderby=company/name asc, age desc
Default direction is ascending when omitted.
Properties in query strings are resolved in this order:
[JsonPropertyName]attributeQueryOptions.PropertyNamingPolicy(if configured)- CLR property name
Property names are matched case-insensitively.
public class UserDto
{
[JsonPropertyName("first_name")]
public string FirstName { get; set; }
public int Age { get; set; }
}filter=first_name eq 'John' and age gt 18
Apply a global naming policy instead of decorating every property:
var options = new QueryOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
var result = dbContext.Users.Apply(query, options: options);filter=first_name eq 'John' and date_of_birth gt 1990-01-01
Configure query behaviour through QueryOptions:
var options = new QueryOptions
{
MaxTop = 100, // Maximum allowed top value
MaxPropertyMappingDepth = 5, // Max depth for nested property resolution (default: 5)
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // Global property naming policy
};
var result = dbContext.Users.Apply(query, options: options);Implement custom search logic:
public class UserSearchBinder : ISearchBinder<User>
{
public Expression<Func<User, bool>> Bind(string searchTerm) =>
user => user.FirstName.Contains(searchTerm) ||
user.LastName.Contains(searchTerm);
}
var result = users.Apply(query, new UserSearchBinder());[HttpGet]
[EnableQuery<UserDto>(maxTop: 100)]
public IActionResult GetUsers() => Ok(dbContext.Users);
// With custom depth
[EnableQuery<UserDto>(maxTop: 100, maxPropertyMappingDepth: 3)]EnableQuery automatically resolves JsonNamingPolicy from your configured JsonOptions in DI. If you've configured a naming policy globally:
builder.Services.Configure<JsonOptions>(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});The action filter will use that policy for property resolution without additional configuration.
On .NET 9+, register the operation transformer to automatically add GoatQuery query parameters to your OpenAPI document for any endpoint decorated with [EnableQuery<T>]:
builder.Services.AddOpenApi(options =>
{
options.AddOperationTransformer<EnableQueryOperationTransformer>();
});This adds filter, orderby, top, skip, count, and search parameters to the generated OpenAPI spec.
[HttpGet]
public IActionResult GetUsers([FromQuery] Query query)
{
var result = dbContext.Users.Apply(query);
return result.IsFailed ? BadRequest(result.Errors) : Ok(result.Value.Query.ToList());
}Uses FluentResults pattern:
var result = users.Apply(query);
if (result.IsFailed)
return BadRequest(result.Errors.Select(e => e.Message));
var data = result.Value.Query.ToList();
var count = result.Value.Count; // If Count = truedotnet test ./src/GoatQuery/testscd example && dotnet runTargets: GoatQuery targets .NET Standard 2.0/2.1. GoatQuery.AspNetCore targets .NET 8.0/9.0.