Wiaoj.Querying.OpenApi
0.2.0-alpha.3
dotnet add package Wiaoj.Querying.OpenApi --version 0.2.0-alpha.3
NuGet\Install-Package Wiaoj.Querying.OpenApi -Version 0.2.0-alpha.3
<PackageReference Include="Wiaoj.Querying.OpenApi" Version="0.2.0-alpha.3" />
<PackageVersion Include="Wiaoj.Querying.OpenApi" Version="0.2.0-alpha.3" />
<PackageReference Include="Wiaoj.Querying.OpenApi" />
paket add Wiaoj.Querying.OpenApi --version 0.2.0-alpha.3
#r "nuget: Wiaoj.Querying.OpenApi, 0.2.0-alpha.3"
#:package Wiaoj.Querying.OpenApi@0.2.0-alpha.3
#addin nuget:?package=Wiaoj.Querying.OpenApi&version=0.2.0-alpha.3&prerelease
#tool nuget:?package=Wiaoj.Querying.OpenApi&version=0.2.0-alpha.3&prerelease
Wiaoj.Querying.OpenApi
Publishes the filter and sort surface a schema-validated endpoint actually accepts.
Installation
dotnet add package Wiaoj.Querying.OpenApi
builder.Services.AddOpenApi(options => options.AddWiaojQuerying());
Endpoints without WithQueryValidation<T>() are left untouched, so it is safe to add once for the entire document.
The problem it solves
QueryRequest binds through BindAsync, and document generation treats a BindAsync parameter as opaque: it emits no parameters for it.
So an endpoint accepting a rich filter language over a fixed set of fields documents nothing at all. The reader of your API sees an endpoint with no inputs; the reality is ?Price[gte]=100&sort=-Name&q=widget, and a query that steps outside the schema comes back 400.
What it adds
For each endpoint marked with WithQueryValidation<T>(), resolved from that entity's registered QuerySchema<T>:
| One query parameter per filterable field | named as callers write it, with its permitted operators spelled out |
sort |
listing the sortable fields |
q |
free-text search |
400 response |
with a note that the body is a ProblemDetails carrying the validation errors |
On POST: a request body |
one entry per media type the registered payload parsers declare; application/json as a typed schema (below). Not required, since an empty body falls back to the query string |
On POST: 413 and 415 |
the statuses the binder returns for a body; 415 documents its Accept header |
Where QUERY is accepted: Accept-Query |
on every response, listing the query media types |
The JSON body
{ "q": "widget", "sort": "-price", "filters": [ { "field": "price", "op": "gte", "value": 100 } ] }
filters is described as a oneOf with one alternative per filterable field. Each alternative's op is limited to the operators that field permits, and its value is typed the way the query parameter is. A generated client therefore can't pair a field with an operator it refuses. op is required for a field that does not permit equality, because omitting it means eq.
The schema's limits carry over:
filtersgetsmaxItems.qgetsmaxLength.- A limit of
0leaves the property out.
QueryFilterStyle does not apply to the body.
By default both the query parameters and the body are described. Set RequestBodyDescription = QueryRequestBodyDescription.BodyOnly to describe only the body.
QUERY endpoints
ASP.NET Core generates OpenAPI 3.0 or 3.1 documents (through Microsoft.OpenApi 2.x). Neither version has a query operation, so the generator leaves the QUERY method out of the document. An endpoint mapped for QUERY alone does not appear, and one mapped for QUERY and POST appears as post only. The document signals QUERY support through the Accept-Query header on the operations it does describe.
Fields the endpoint ignores via IgnoreQueryParameters(...) — or that are ignored globally with AddQuerying(q => q.IgnoreParameters(...)), unless the schema or endpoint opts out of global rules — are left out, because the validator does not accept them. The rule is the same one the validation filter applies.
The field list, the sortable set and the operator list all come from QuerySchema<T>.DescribeFields() — the same rules the validator enforces. What is published and what is enforced have one source, so the document cannot describe a filter the endpoint would reject.
It follows the application's configuration
- Names. Fields are published under the names the schema uses.
- With
UseJsonNamingPolicy()(orUseFieldNamingPolicy) that iscontentTypeorcontent_type, matching the response bodies. The schema accepts those names, so the document never advertises one the server rejects. - Without it, names are the CLR member names (
UsageCount,LastUsedAt). The query string then does not match camelCase bodies. Naming policy is opt-in because it renames parameters and regenerates clients; the old names keep working as aliases.
- With
- Types — from the field's CLR type:
boolisboolean,longisinteger/int64, dates aredate-time, an enum lists the names the engine parses. - Operator tokens — exactly what the parser reads (
isNull,notBetween). - Descriptions — from
.Describe("...")on the schema. - Custom filters declared with
CustomFilter<TValue>are described like any field.
Shaping the output
builder.Services.AddOpenApi(options => options.AddWiaojQuerying(querying => {
querying.FilterStyle = QueryFilterStyle.DeepObject;
querying.ConfigureFilter = (field, parameter) => parameter.Deprecated = field.Name == "legacyCode";
querying.ConfigureOperation = (operation, fields) => { /* anything else */ };
}));
QueryFilterStyle has three values:
| Style | Generated client (TypeScript) | Needs |
|---|---|---|
Prose (default) |
usageCount?: number. The operators are listed only in the description, so the client has no typed way to send usageCount[gte]. |
Nothing |
DeepObject |
usageCount?: { eq?: number; gte?: number; lte?: number } |
A query serializer that writes nested objects as brackets, such as qs. URLSearchParams writes [object Object]. |
OperatorParameters |
usageCount?: number; "usageCount[gte]"?: number; "usageCount[lte]"?: number |
Nothing. Each operator is an ordinary query parameter. |
All three styles have these properties:
- Only the operators a field permits are described, and each is typed from the field.
inis a comma-separated string.isNullandisNotNullaccept onlytrue, because presence is the condition and afalsewould still filter. - In
OperatorParameters, equality is described once, as the bare name. A client that sent bothusageCountandusageCount[eq]would be filtering the same field with the same operator twice, which the validation filter refuses with a 400.
If your generator handles deepObject well, prefer it; otherwise use OperatorParameters.
The hooks receive the full field descriptors, so output can be extended with complete information instead of patched by a transformer that runs afterwards and depends on this one's output staying the same.
Example
A schema like this:
schema.Property(p => p.Name).AllowFilter(QueryOperator.Equal, QueryOperator.Contains).AllowSort();
schema.Property(p => p.Price).AllowFilter(QueryOperator.GreaterThanOrEqual, QueryOperator.LessThanOrEqual);
produces a Name parameter documented as accepting eq, contains, a Price parameter accepting gte, lte, and a sort parameter listing Name only.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Microsoft.AspNetCore.OpenApi (>= 10.0.12)
- Wiaoj.Preconditions (>= 0.2.0-alpha.3)
- Wiaoj.Querying.AspNetCore (>= 0.2.0-alpha.3)
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.2.0-alpha.3 | 51 | 9/24/2026 |
| 0.2.0-alpha.2 | 45 | 9/24/2026 |
| 0.2.0-alpha.1 | 47 | 9/24/2026 |
| 0.1.0-alpha.9 | 49 | 9/21/2026 |
| 0.1.0-alpha.8 | 50 | 9/21/2026 |
| 0.1.0-alpha.7 | 56 | 9/18/2026 |
| 0.1.0-alpha.6 | 47 | 9/16/2026 |
| 0.1.0-alpha.5 | 55 | 9/16/2026 |
| 0.1.0-alpha.4 | 55 | 9/16/2026 |
| 0.1.0-alpha.3 | 51 | 9/15/2026 |
| 0.1.0-alpha.2 | 53 | 9/15/2026 |
| 0.1.0-alpha.1 | 73 | 9/14/2026 |
| 0.0.1-alpha.112-preview | 55 | 9/13/2026 |
| 0.0.1-alpha.111-preview | 51 | 9/13/2026 |
| 0.0.1-alpha.110-preview | 62 | 9/12/2026 |
| 0.0.1-alpha.109-preview | 76 | 9/11/2026 |
| 0.0.1-alpha.108-preview | 75 | 9/8/2026 |
| 0.0.1-alpha.107-preview | 70 | 9/8/2026 |
| 0.0.1-alpha.106-preview | 69 | 9/8/2026 |
| 0.0.1-alpha.105-preview | 70 | 9/8/2026 |