Wiaoj.Querying.AspNetCore
0.2.0-alpha.3
dotnet add package Wiaoj.Querying.AspNetCore --version 0.2.0-alpha.3
NuGet\Install-Package Wiaoj.Querying.AspNetCore -Version 0.2.0-alpha.3
<PackageReference Include="Wiaoj.Querying.AspNetCore" Version="0.2.0-alpha.3" />
<PackageVersion Include="Wiaoj.Querying.AspNetCore" Version="0.2.0-alpha.3" />
<PackageReference Include="Wiaoj.Querying.AspNetCore" />
paket add Wiaoj.Querying.AspNetCore --version 0.2.0-alpha.3
#r "nuget: Wiaoj.Querying.AspNetCore, 0.2.0-alpha.3"
#:package Wiaoj.Querying.AspNetCore@0.2.0-alpha.3
#addin nuget:?package=Wiaoj.Querying.AspNetCore&version=0.2.0-alpha.3&prerelease
#tool nuget:?package=Wiaoj.Querying.AspNetCore&version=0.2.0-alpha.3&prerelease
Wiaoj.Querying.AspNetCore
ASP.NET Core integration for Wiaoj.Querying. Provides Minimal API parameter binding (Query<TEntity>), RFC 10008 HTTP QUERY request body handling, DI registration extensions, and automatic RFC 7807 validation endpoint filters.
Features
- Minimal API Parameter Binding: Strongly typed
Query<TEntity>parameter binding viaIBindableFromHttpContext<T>. - RFC 10008 HTTP
QUERY& Body Support: Binds query payloads from request bodies (application/json,text/plain,application/x-www-form-urlencoded) onQUERYandPOSTmethods, with automatic fallback to URL query strings onGET. - DI-Driven Endpoint Validation:
.WithQueryValidation<TEntity>()endpoint filter automatically resolvesQuerySchema<TEntity>from the DI container and validates incoming requests before handler execution. - RFC 7807 Validation Responses: Automatically returns standard
400 Bad Request(ValidationProblemDetails) when requests violate schema rules, limits, or types. - Protocol Status Codes: Enforces
415 Unsupported Media Type(with anAcceptresponse header),413 Payload Too Large(viaIHttpMaxRequestBodySizeFeature), and400 Bad Request(on malformed syntax). Accept-Query(RFC 10008): Every response of an endpoint that acceptsQUERY(GET included, for discovery) advertises the media types of the registered payload parsers. Endpoints that do not acceptQUERYnever send it.- Route Group Support: Applies schema validation across individual endpoints (
RouteHandlerBuilder) and route groups (RouteGroupBuilder). - Native AOT Compatible: Reflection-free parameter binding and stream parsing.
- Zero Boilerplate: Implicit conversions allow
Query<TEntity>to be passed directly to.ApplyQuery(...).
Installation
dotnet add package Wiaoj.Querying.AspNetCore
Quick Start (Minimal APIs)
1. Define Entity & Query Schema
using Wiaoj.Querying;
public sealed record Product(int Id, string Name, decimal Price, string Category, bool IsDeleted);
public sealed class ProductQuerySchema : QuerySchema<Product>
{
public ProductQuerySchema()
{
AllowFilter(x => x.Category);
Property(x => x.Price)
.AllowFilter(QueryOperator.Equal, QueryOperator.GreaterThanOrEqual, QueryOperator.Between)
.AllowSort();
Property(x => x.Name)
.HasName("name")
.AllowFilter(QueryOperator.Contains, QueryOperator.StartsWith)
.AllowSort();
SearchIn(x => x.Name);
RequireFilter(x => !x.IsDeleted);
ConfigureLimits(maxFilters: 10, maxInValues: 20, maxSortFields: 3);
}
}
2. Configure Dependency Injection (Program.cs)
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Wiaoj.Querying;
using Wiaoj.Querying.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opt => opt.UseNpgsql(...));
// Register querying engine, global ignored parameters (e.g. PaginationParameters.All or custom strings), and schemas
builder.Services.AddQuerying()
.IgnoreParameters(PaginationParameters.All)
.AddSchema<Product, ProductQuerySchema>();
// Native ASP.NET Core RFC 7807 Problem Details customization
builder.Services.AddProblemDetails(options => {
options.CustomizeProblemDetails = ctx => {
ctx.ProblemDetails.Instance = ctx.HttpContext.Request.Path;
};
});
var app = builder.Build();
3. Expose Minimal API Endpoint
Declare Query<Product> in your route handler and attach .WithQueryValidation<Product>():
app.MapMethods("/api/products", ["GET", "QUERY"], async (
Query<Product> query,
QuerySchema<Product> schema, //or ProductQuerySchema schema,
AppDbContext db,
CancellationToken ct) =>
{
// `query` implicitly converts to `QueryRequest`
List<Product> products = await db.Products
.AsNoTracking()
.ApplyQuery(query, schema)
.ToListAsync(ct);
return Results.Ok(products);
})
.WithQueryValidation<Product>(); // Resolves schema from DI and validates
app.Run();
4. Field names that follow your JSON settings
builder.Services.AddQuerying(querying => querying
.UseJsonNamingPolicy()
.AddSchema<Product, ProductQuerySchema>());
Takes JsonOptions.SerializerOptions.PropertyNamingPolicy — camelCase by default in a minimal API — and applies it to every schema's field names not set with HasName. A response that says contentType is then filtered with ?contentType=..., and a generated OpenAPI document says so. Rendered names are accepted as aliases; the original names keep working. It is opt-in because it renames parameters in a published document, which regenerates clients. An explicit UseFieldNamingPolicy(...) takes precedence.
5. Filters that are not columns
Declare them on the schema with CustomFilter<TValue> instead of binding a separate [AsParameters] record and hiding its names with IgnoreParameters. The validation filter then checks them, the document describes them, and the handler reads them typed:
// schema
CustomFilter<bool>("hasScreenshot").AllowFilter(QueryOperator.Equal);
// handler
app.MapGet("/api/keys", (Query<TranslationKey> query, QuerySchema<TranslationKey> schema, AppDbContext db) => {
IQueryable<TranslationKey> keys = db.Keys.ApplyQuery(query, schema);
if(schema.TryGetFilterValue(query.Value, "hasScreenshot", out bool hasScreenshot)) {
keys = keys.Where(k => db.KeyScreenshots.Any(s => s.KeyId == k.Id) == hasScreenshot);
}
return keys.ToListAsync();
}).WithQueryValidation<TranslationKey>();
?hasScreenshot=sometimes is a 400, like any other invalid filter.
6. A different schema per endpoint
Two endpoints over one entity can expose different query surfaces. Select the schema class for each endpoint:
app.MapGet("/admin/assets", (Query<Asset> query, AdminAssetSchema schema, AppDbContext db) => ...)
.WithQueryValidation<Asset, AdminAssetSchema>();
app.MapGet("/assets", (Query<Asset> query, PublicAssetSchema schema, AppDbContext db) => ...)
.WithQueryValidation<Asset, PublicAssetSchema>();
// or for a whole group
app.MapGroup("/public").WithQueryValidation<Asset, PublicAssetSchema>();
The endpoint metadata records the selected schema, and everything that reads the endpoint's schema uses it:
- the validation filter;
Query<Asset>binding, including the schema's parameter rules and naming aliases;- the OpenAPI document.
The three therefore cannot apply different contracts. Inject the schema class in the handler as well. Once Asset has more than one schema, QuerySchema<Asset> and WithQueryValidation<Asset>() throw instead of picking one.
How It Works
1. Parameter Binding (Query<TEntity> & QueryRequestBinder)
The Query<TEntity> record implements IBindableFromHttpContext<Query<TEntity>>. During request binding, QueryRequestBinder inspects the request:
- HTTP
QUERY/POSTRequests:- Reads
Content-Typeheader. - If
application/json→ parses body viaJsonQueryParser. - If
text/plainorapplication/x-www-form-urlencoded→ parses body viaBracketQueryParser. - If custom format registered (e.g. YAML) → delegates to matching
IQueryPayloadParser. - If body is empty → falls back to URL query parameters.
- Reads
- HTTP
GET/ Other Requests:- Reads directly from URL query collection (
IQueryCollection).
- Reads directly from URL query collection (
Note:
Query<TEntity>performs parsing only. Schema validation occurs during the endpoint filter pipeline to produce standard RFC 7807 responses.
2. Validation Endpoint Filter (WithQueryValidation)
The .WithQueryValidation<TEntity>() extension adds an endpoint filter factory that:
- Resolves
QuerySchema<TEntity>as a singleton from DI (or uses an explicitly passed instance). - Locates the bound
Query<TEntity>argument inEndpointFilterInvocationContext.Arguments. - Validates the request against
QuerySchema<TEntity>. - If validation fails, short-circuits the pipeline and returns
Results.ValidationProblem(...)(400 Bad Request). - If valid, passes execution to the endpoint handler.
- If the handler throws a
QueryValidationException, returns the same400ValidationProblem. Some errors can only be found once the query is applied: a cursor issued for a different sort, a sort on a field the endpoint cannot page by, orApplyValidatedQueryin the handler. These are caller errors, so they get a 400 like any invalid query string, not a 500.
HTTP Status Codes & Error Handling
QueryRequestBinder and QueryValidationEndpointFilter return standardized status codes based on request state:
| Status Code | Condition | Behavior / Headers |
|---|---|---|
200 OK |
Successful binding & validation | Request proceeds to handler. |
400 Bad Request |
Malformed JSON or bracket syntax | Returns BadHttpRequestException(400) before reaching handler. |
400 Validation Problem |
Schema rule or limit violation | Returns RFC 7807 ValidationProblemDetails dictionary. |
413 Payload Too Large |
Body exceeds IHttpMaxRequestBodySizeFeature |
Aborts body reading to protect server memory. |
415 Unsupported Media Type |
Unrecognized body Content-Type on QUERY/POST |
Sets Accept to the registered parsers' media types (RFC 10008 Appendix A.3); Accept-Query too when the endpoint accepts QUERY. |
Validation Response Format (RFC 7807)
When query parameters fail schema validation, the endpoint returns an RFC 7807 application/problem+json payload:
Example Request
GET /api/products?price[between]=invalid..value&unregisteredField=test&sort=-secretColumn
Response (400 Bad Request)
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more query validation errors occurred.",
"status": 400,
"errors": {
"price": [
"Range boundary values in 'invalid..value' are not valid for property 'price' of type 'Decimal'."
],
"unregisteredField": [
"Filtering by field 'unregisteredField' is not allowed."
],
"secretColumn": [
"Sorting by field 'secretColumn' is not allowed."
]
}
}
Enriching Problem Details (
instance,traceId, etc.):WithQueryValidationproduces standard ASP.NET CoreResults.ValidationProblemresponses that natively route through the platform'sIProblemDetailsService. Configurebuilder.Services.AddProblemDetails(options => options.CustomizeProblemDetails = ctx => ctx.ProblemDetails.Instance = ctx.HttpContext.Request.Path)centrally inProgram.csto enrich validation responses without ad-hoc library settings.
Route Groups (MapGroup)
WithQueryValidation<TEntity>() can be applied to route groups to enforce validation across grouped endpoints:
var productApi = app.MapGroup("/api/products");
productApi.MapMethods("/", ["GET", "QUERY"], async (Query<Product> query, QuerySchema<Product> schema, AppDbContext db) =>
Results.Ok(await db.Products.ApplyQuery(query, schema).ToListAsync()))
.WithQueryValidation<Product>();
Type Conversions & Testing
Query<TEntity> provides implicit conversion operators for use in unit and integration tests:
// Implicit unwrap to QueryRequest
Query<Product> query = ...;
QueryRequest request = query;
// Implicit wrap from QueryRequest (construct in tests without HttpContext)
QueryRequest testRequest = QueryRequest.Parse("price[gte]=100&sort=-createdAt");
Query<Product> wrapped = testRequest;
License
This project is licensed under the MIT License.
| 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
- Wiaoj.Querying (>= 0.2.0-alpha.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Wiaoj.Querying.AspNetCore:
| Package | Downloads |
|---|---|
|
Wiaoj.Querying.OpenApi
Package Description |
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 | 50 | 9/24/2026 |
| 0.2.0-alpha.1 | 54 | 9/24/2026 |
| 0.1.0-alpha.9 | 149 | 9/21/2026 |
| 0.1.0-alpha.8 | 51 | 9/21/2026 |
| 0.1.0-alpha.7 | 61 | 9/18/2026 |
| 0.1.0-alpha.6 | 56 | 9/16/2026 |
| 0.1.0-alpha.5 | 58 | 9/16/2026 |
| 0.1.0-alpha.4 | 60 | 9/16/2026 |
| 0.1.0-alpha.3 | 55 | 9/15/2026 |
| 0.1.0-alpha.2 | 94 | 9/15/2026 |
| 0.1.0-alpha.1 | 76 | 9/14/2026 |
| 0.0.1-alpha.112-preview | 62 | 9/13/2026 |
| 0.0.1-alpha.111-preview | 55 | 9/13/2026 |
| 0.0.1-alpha.110-preview | 56 | 9/12/2026 |
| 0.0.1-alpha.109-preview | 80 | 9/11/2026 |
| 0.0.1-alpha.108-preview | 81 | 9/8/2026 |
| 0.0.1-alpha.107-preview | 71 | 9/8/2026 |
| 0.0.1-alpha.106-preview | 66 | 9/8/2026 |
| 0.0.1-alpha.105-preview | 63 | 9/8/2026 |