GoatQuery 1.0.0
dotnet add package GoatQuery --version 1.0.0
NuGet\Install-Package GoatQuery -Version 1.0.0
<PackageReference Include="GoatQuery" Version="1.0.0" />
<PackageVersion Include="GoatQuery" Version="1.0.0" />
<PackageReference Include="GoatQuery" />
paket add GoatQuery --version 1.0.0
#r "nuget: GoatQuery, 1.0.0"
#:package GoatQuery@1.0.0
#addin nuget:?package=GoatQuery&version=1.0.0
#tool nuget:?package=GoatQuery&version=1.0.0
GoatQuery
A .NET library for parsing query parameters into LINQ expressions. Enables database-level filtering, sorting, and pagination from HTTP query strings.
This project only supports Entity Framework Linq currently.
Installation
dotnet add package GoatQuery
# Or for ASP.NET Core integration please install this instead.
dotnet add package GoatQuery.AspNetCore
Quick Start
// 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);
Supported Syntax
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
Filtering
Operators
- Comparison:
eq,ne,gt,gte,lt,lte - Logical:
and,or - String:
contains
Data Types
| 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.
Property Path Navigation
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')
Lambda Expressions
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 Range Expansion
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 |
Null Safety
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)
Examples
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')
Ordering
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.
Property Mapping
Properties in query strings are resolved in this order:
[JsonPropertyName]attributeQueryOptions.PropertyNamingPolicy(if configured)- CLR property name
Property names are matched case-insensitively.
Using JsonPropertyName
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
Using PropertyNamingPolicy
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
Configuration
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);
Search
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());
ASP.NET Core Integration
Action Filter
[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.
OpenAPI Integration
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.
Manual Processing
[HttpGet]
public IActionResult GetUsers([FromQuery] Query query)
{
var result = dbContext.Users.Apply(query);
return result.IsFailed ? BadRequest(result.Errors) : Ok(result.Value.Query.ToList());
}
Error Handling
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 = true
Development
Test
dotnet test ./src/GoatQuery/tests
Run the example project
cd example && dotnet run
Targets: GoatQuery targets .NET Standard 2.0/2.1. GoatQuery.AspNetCore targets .NET 8.0/9.0.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. 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 was computed. 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. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- FluentResults (>= 4.0.0)
- System.Text.Json (>= 10.0.8)
-
.NETStandard 2.1
- FluentResults (>= 4.0.0)
- System.Text.Json (>= 10.0.8)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on GoatQuery:
| Package | Downloads |
|---|---|
|
GoatQuery.AspNetCore
ASP.NET Core integration for GoatQuery — action filter for automatic query parameter processing. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 0 | 6/5/2026 |
| 0.6.6 | 5,491 | 5/9/2025 |
| 0.6.5 | 1,114 | 1/9/2025 |
| 0.6.4 | 156 | 1/9/2025 |
| 0.6.3-rc.14 | 339 | 11/14/2025 |
| 0.6.3-rc.13 | 2,659 | 10/2/2025 |
| 0.6.3-rc.12 | 233 | 9/29/2025 |
| 0.6.3-rc.11 | 192 | 9/24/2025 |
| 0.6.3-rc.10 | 168 | 9/9/2025 |
| 0.6.3-rc.9 | 119 | 9/5/2025 |
| 0.6.3-rc.8 | 212 | 8/28/2025 |
| 0.6.3-rc.7 | 998 | 12/13/2024 |
| 0.6.3-rc.6 | 103 | 12/11/2024 |
| 0.6.3-rc.5 | 103 | 12/11/2024 |
| 0.6.3-rc.4 | 96 | 12/4/2024 |
| 0.6.3-rc.3 | 94 | 11/6/2024 |
| 0.6.3-rc.2 | 107 | 10/11/2024 |
| 0.6.3-rc.1 | 100 | 7/26/2024 |
| 0.6.2 | 8,910 | 2/21/2024 |