Wiaoj.Querying.AspNetCore 0.2.0-alpha.3

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

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 via IBindableFromHttpContext<T>.
  • RFC 10008 HTTP QUERY & Body Support: Binds query payloads from request bodies (application/json, text/plain, application/x-www-form-urlencoded) on QUERY and POST methods, with automatic fallback to URL query strings on GET.
  • DI-Driven Endpoint Validation: .WithQueryValidation<TEntity>() endpoint filter automatically resolves QuerySchema<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 an Accept response header), 413 Payload Too Large (via IHttpMaxRequestBodySizeFeature), and 400 Bad Request (on malformed syntax).
  • Accept-Query (RFC 10008): Every response of an endpoint that accepts QUERY (GET included, for discovery) advertises the media types of the registered payload parsers. Endpoints that do not accept QUERY never 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:

  1. HTTP QUERY / POST Requests:
    • Reads Content-Type header.
    • If application/json → parses body via JsonQueryParser.
    • If text/plain or application/x-www-form-urlencoded → parses body via BracketQueryParser.
    • If custom format registered (e.g. YAML) → delegates to matching IQueryPayloadParser.
    • If body is empty → falls back to URL query parameters.
  2. HTTP GET / Other Requests:
    • Reads directly from URL query collection (IQueryCollection).

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:

  1. Resolves QuerySchema<TEntity> as a singleton from DI (or uses an explicitly passed instance).
  2. Locates the bound Query<TEntity> argument in EndpointFilterInvocationContext.Arguments.
  3. Validates the request against QuerySchema<TEntity>.
  4. If validation fails, short-circuits the pipeline and returns Results.ValidationProblem(...) (400 Bad Request).
  5. If valid, passes execution to the endpoint handler.
  6. If the handler throws a QueryValidationException, returns the same 400 ValidationProblem. 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, or ApplyValidatedQuery in 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.): WithQueryValidation produces standard ASP.NET Core Results.ValidationProblem responses that natively route through the platform's IProblemDetailsService. Configure builder.Services.AddProblemDetails(options => options.CustomizeProblemDetails = ctx => ctx.ProblemDetails.Instance = ctx.HttpContext.Request.Path) centrally in Program.cs to 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 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. 
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 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
Loading failed