GoatQuery 1.0.0

dotnet add package GoatQuery --version 1.0.0
                    
NuGet\Install-Package GoatQuery -Version 1.0.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="GoatQuery" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GoatQuery" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="GoatQuery" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GoatQuery --version 1.0.0
                    
#r "nuget: GoatQuery, 1.0.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.
#:package GoatQuery@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GoatQuery&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=GoatQuery&version=1.0.0
                    
Install as a Cake Tool

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:

  • eq and contains exclude null rows (null does not equal any value)
  • ne includes 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:

  1. [JsonPropertyName] attribute
  2. QueryOptions.PropertyNamingPolicy (if configured)
  3. 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);

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 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. 
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 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
Loading failed