QueryContracts 0.1.0

dotnet add package QueryContracts --version 0.1.0
                    
NuGet\Install-Package QueryContracts -Version 0.1.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="QueryContracts" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="QueryContracts" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="QueryContracts" />
                    
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 QueryContracts --version 0.1.0
                    
#r "nuget: QueryContracts, 0.1.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 QueryContracts@0.1.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=QueryContracts&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=QueryContracts&version=0.1.0
                    
Install as a Cake Tool

QueryContracts

NuGet

Define public query contracts for .NET APIs.

QueryContracts maps an application input object to IQueryable<T> using an explicit, type-safe contract.

The problem

Public API endpoints that accept filtering, sorting, and pagination need to answer:

  • Which filters are accepted?
  • Which sorts are public?
  • What is the maximum page size?
  • Where does query validation live?
  • How do we avoid exposing internal entity properties by accident?

Without an explicit contract, these decisions are scattered across controllers, attributes, dynamic LINQ strings, or ad-hoc validation code.

The solution

QueryContracts lets developers define public query behavior explicitly:

public static readonly QueryContract<User, UserQuery> Contract =
    QueryContract.For<User, UserQuery>()
        .Filter(q => q.Name, u => u.Name).Contains()
        .Filter(q => q.Active, u => u.IsActive).Equals()
        .Sort(q => q.Sort)
            .Allow("name", u => u.Name)
            .Allow("created", u => u.CreatedAt)
            .Default("created", descending: true)
        .Page(q => q.Page, q => q.PageSize, maxSize: 100)
        .Build();

Installation

dotnet add package QueryContracts

Quickstart

Define your entity and input:

public sealed class User
{
    public Guid Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public bool IsActive { get; init; }
    public DateTime CreatedAt { get; init; }
}

public sealed record UserQuery(
    string? Name,
    bool? Active,
    string? Sort,
    int? Page,
    int? PageSize);

Define the contract:

public static readonly QueryContract<User, UserQuery> Contract =
    QueryContract.For<User, UserQuery>()
        .Filter(q => q.Name, u => u.Name).Contains()
        .Filter(q => q.Active, u => u.IsActive).Equals()
        .Sort(q => q.Sort)
            .Allow("name", u => u.Name)
            .Allow("created", u => u.CreatedAt)
            .Default("created", descending: true)
        .Page(q => q.Page, q => q.PageSize, maxSize: 100)
        .Build();

Apply it:

var result = users.Apply(Contract, query);

if (!result.IsValid)
{
    return Results.BadRequest(result.Errors);
}

var finalQuery = result.Query;

Why two expressions?

.Filter(q => q.Active, u => u.IsActive).Equals()

The API requires two expressions per filter:

  • The first expression reads from the public/application input (TInput).
  • The second expression maps to the entity property (TEntity).

The library does not guess mappings by convention. Explicit mapping prevents accidental exposure of entity properties that should not be queryable.

Sorting

Sort aliases are explicit strings declared in the contract:

Sort string Behavior
"name" Ascending by name
"-name" Descending by name
"created" Ascending by created
"-created" Descending by created

When the sort input is null, empty, or whitespace, the default sort is applied (if configured).

Unknown aliases return a structured error and do not fall back to the default.

Pagination

  • Page is 1-based.
  • Default page is 1 when null.
  • Default page size is 20 when null.
  • Maximum page size is configured by the contract.
  • Invalid page or page size returns a structured error.

Error handling

var result = users.Apply(Contract, query);

if (!result.IsValid)
{
    return Results.BadRequest(result.Errors);
}

Errors are structured with:

  • Code — a QueryContractErrorCode enum value.
  • Message — a human-readable description.
  • MemberName — the input member that caused the error.
  • AttemptedValue — the value that was attempted.

Design principles

  • Does not execute queries.
  • Does not depend on ASP.NET Core.
  • Does not depend on EF Core.
  • Does not expose entity properties automatically.
  • Does not parse a query language.
  • Keeps public query behavior explicit and testable.

What QueryContracts is not

  • Not OData
  • Not GraphQL
  • Not Dynamic LINQ
  • Not a query language
  • Not a repository abstraction
  • Not an EF Core extension
  • Not an ASP.NET Core extension

Sample API

A Minimal API sample is available in samples/QueryContracts.SampleApi.

It demonstrates the library with an in-memory product list and a GET /products endpoint.

Roadmap

v0.1.0 (released):

  • Filters (Equals, Contains, StartsWith, GreaterThanOrEqual, LessThanOrEqual)
  • Sorting with aliases and defaults
  • Pagination with maximum page size validation
  • Structured validation errors
  • Sample API

Future:

  • More examples and integration guides
  • Optional ASP.NET Core helpers
  • More filter operators

Contributing

Open an issue or pull request at github.com/LucasFernandes0101/querycontracts.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 367 6/28/2026
0.1.0-preview.5 63 6/28/2026
0.1.0-preview.4 54 6/28/2026
0.1.0-preview.3 66 6/28/2026
0.1.0-preview.2 57 6/28/2026
0.1.0-preview.1 63 6/28/2026

Initial stable release. Adds filters, sorting, pagination, structured errors and multi-target support for net8.0 and net10.0.