From 1f8ad5ad9f85c5bdbf665e2eaf5c02482c73137a Mon Sep 17 00:00:00 2001 From: Peter John Casasola Date: Sun, 12 Jul 2026 20:07:04 +0800 Subject: [PATCH 1/6] docs: added open api readme --- src/FlexQuery.NET.OpenApi/README.md | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/FlexQuery.NET.OpenApi/README.md diff --git a/src/FlexQuery.NET.OpenApi/README.md b/src/FlexQuery.NET.OpenApi/README.md new file mode 100644 index 0000000..666f99b --- /dev/null +++ b/src/FlexQuery.NET.OpenApi/README.md @@ -0,0 +1,65 @@ +# FlexQuery.NET.OpenApi + +[![NuGet Version](https://img.shields.io/nuget/v/FlexQuery.NET.OpenApi.svg)](https://www.nuget.org/packages/FlexQuery.NET.OpenApi) + +OpenAPI/Swagger documentation and examples for FlexQuery.NET endpoints. + +## When to Use This Package + +Install this package when you expose FlexQuery-enabled endpoints and want automatic OpenAPI schema descriptions, parameter documentation, and canonical request/response examples in your Swagger UI or OpenAPI document. + +## Installation + +```bash +dotnet add package FlexQuery.NET.OpenApi +``` + +## Registration + +```csharp +builder.Services.AddFlexQueryOpenApi(); +``` + +This registers schema and operation transformers that enrich your OpenAPI document with: +- Descriptions for all FlexQuery model types (`FlexQueryRequest`, `FlexQueryParameters`, `QueryResult`, etc.) +- Descriptions for query parameters (`filter`, `select`, `sort`, `page`, `pageSize`, `includeCount`) +- Canonical, production-quality examples for `FlexQueryRequest`, `FlexQueryParameters`, and `QueryResult` + +## Quick Start + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddFlexQueryOpenApi(); + +builder.Services.AddOpenApi(options => +{ + options.AddFlexQuery(); +}); + +var app = builder.Build(); + +app.MapOpenApi(); +app.Run(); +``` + +## Features + +- **Schema Descriptions** — Human-readable descriptions for all FlexQuery model types (`FlexQueryRequest`, `FlexQueryParameters`, `QueryResult`, `FilterGroup`, `FilterCondition`, `SortNode`, `PagingOptions`, `AggregateModel`, `HavingCondition`, `IncludeNode`, `ProjectionMode`, `LogicOperator`, `AggregateFunction`) +- **Parameter Documentation** — Descriptions for query parameters (`filter`, `select`, `sort`, `page`, `pageSize`, `includeCount`) +- **Canonical Examples** — Production-quality, strongly typed examples for `FlexQueryRequest`, `FlexQueryParameters`, and `QueryResult` that immediately demonstrate FlexQuery capabilities +- **Zero Configuration** — Single registration call, no options, no builders +- **Minimal Public API** — Two extension methods, everything else internal + +## Related Packages + +- [FlexQuery.NET](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET/README.md) — Core query engine +- [FlexQuery.NET.AspNetCore](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.AspNetCore/README.md) — ASP.NET Core integration +- [FlexQuery.NET.EntityFrameworkCore](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.EntityFrameworkCore/README.md) — EF Core execution +- [FlexQuery.NET.Dapper](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Dapper/README.md) — Dapper execution + +## Documentation + +- [OpenAPI Integration](https://flexquery.vercel.app/guide/openapi-integration) +- [Swagger Integration](https://flexquery.vercel.app/guide/swagger-integration) From 7df035d100531bc2642142722627b58ce3a5a21e Mon Sep 17 00:00:00 2001 From: Peter John Casasola Date: Sun, 12 Jul 2026 20:27:08 +0800 Subject: [PATCH 2/6] docs: align v4 API references and fix broken doc links --- README.md | 10 +- docs/.vitepress/config.ts | 6 +- docs/adapters/ag-grid.md | 3 +- docs/adapters/miniodata.md | 31 +- docs/architecture/provider-model.md | 6 +- docs/examples/basic.md | 2 +- docs/examples/filtering.md | 16 +- docs/examples/formats.md | 39 - docs/guide/aspnet-integration.md | 35 +- docs/guide/basic-usage.md | 26 + docs/guide/comparison-libraries.md | 4 +- docs/guide/comparison.md | 2 +- docs/guide/core-concepts.md | 23 +- docs/guide/dotnet-comparison.md | 2 +- docs/guide/extension-methods.md | 57 +- docs/guide/filtering.md | 63 +- docs/guide/fluent-builder.md | 145 ++ docs/guide/getting-started.md | 59 +- docs/guide/grouping.md | 136 +- docs/guide/how-it-works.md | 9 +- docs/guide/include-filtering.md | 20 +- docs/guide/introduction.md | 2 +- docs/guide/linq-integration.md | 29 +- docs/guide/paging.md | 38 +- docs/guide/performance-tuning.md | 56 +- docs/guide/performance/api-benchmarks.md | 5 +- docs/guide/performance/benchmark-overview.md | 4 +- docs/guide/performance/benchmarks.md | 8 +- docs/guide/performance/database-execution.md | 8 +- .../performance/expression-generation.md | 2 +- docs/guide/performance/index.md | 8 +- .../guide/performance/interpretation-guide.md | 6 +- docs/guide/performance/memory-usage.md | 13 +- docs/guide/performance/parsing-performance.md | 8 +- docs/guide/projection.md | 4 +- docs/guide/query-composition.md | 10 +- docs/guide/query-formats.md | 77 +- docs/guide/query-models.md | 31 +- docs/guide/query-syntax.md | 244 ++- docs/guide/security-governance.md | 3 +- docs/guide/security.md | 95 +- docs/guide/sorting.md | 4 +- docs/guide/swagger-integration.md | 4 +- docs/guide/validation.md | 8 +- docs/guide/why-flexquery.md | 2 +- docs/index.md | 15 +- docs/migration/v2-to-v3.md | 64 +- docs/migration/v3-to-v4.md | 816 +++++++++ docs/package-lock.json | 1497 +++++++++++++---- docs/providers/dapper.md | 11 +- docs/providers/dapper/conventions.md | 58 +- docs/providers/dapper/dialects.md | 76 +- docs/providers/dapper/getting-started.md | 49 +- docs/providers/dapper/relationship-queries.md | 8 +- docs/providers/dapper/sql-generation.md | 9 +- docs/providers/ef-core.md | 47 +- docs/shared/operators.md | 18 +- docs/shared/query-language.md | 183 +- src/FlexQuery.NET.Adapters.AgGrid/README.md | 2 +- src/FlexQuery.NET.Adapters.Kendo/README.md | 2 +- src/FlexQuery.NET.AspNetCore/README.md | 12 +- src/FlexQuery.NET.Dapper/README.md | 19 +- src/FlexQuery.NET.Diagnostics/README.md | 4 +- .../README.md | 11 +- src/FlexQuery.NET.Parsers.Fql/README.md | 261 ++- src/FlexQuery.NET.Parsers.MiniOData/README.md | 56 +- src/FlexQuery.NET/README.md | 184 +- 67 files changed, 3457 insertions(+), 1308 deletions(-) create mode 100644 docs/guide/fluent-builder.md create mode 100644 docs/migration/v3-to-v4.md diff --git a/README.md b/README.md index 161cefa..c842ddf 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Works with Entity Framework Core, Dapper, and other FlexQuery.NET providers. - **No OData Dependency** — Get powerful querying without OData's complexity and tight coupling - **100% Server-Side** — All operations translate to SQL via expression trees — zero client evaluation - **Security First** — Declare allowed/blocked fields per-endpoint with strict validation -- **Multi-Format** — Auto-detects DSL, JSON, JQL, and OData query syntax on the same endpoint +- **Multi-Format** — Auto-detects DSL, JSON, FQL, and OData query syntax on the same endpoint - **Multiple Data Providers** — Works with EF Core, Dapper, or any IQueryable source ## Package Ecosystem @@ -41,8 +41,9 @@ graph TD Dapper --> Kendo AspNet --> AgGrid AspNet --> Kendo - Core --> Jql["FlexQuery.NET.Parsers.Jql"] + Core --> Fql["FlexQuery.NET.Parsers.Fql"] Core --> OData["FlexQuery.NET.Parsers.MiniOData"] + AspNet --> OpenApi["FlexQuery.NET.OpenApi"] ``` | Package | Purpose | @@ -54,14 +55,15 @@ graph TD | [FlexQuery.NET.Diagnostics](https://www.nuget.org/packages/FlexQuery.NET.Diagnostics) | Execution diagnostics, timing, and observability | | [FlexQuery.NET.Adapters.AgGrid](https://www.nuget.org/packages/FlexQuery.NET.Adapters.AgGrid) | AG Grid Server-Side Row Model (SSRM) request/response adapter | | [FlexQuery.NET.Adapters.Kendo](https://www.nuget.org/packages/FlexQuery.NET.Adapters.Kendo) | Kendo UI DataSource request adapter | -| [FlexQuery.NET.Parsers.Jql](https://www.nuget.org/packages/FlexQuery.NET.Parsers.Jql) | JQL (Jira Query Language) syntax parser | +| [FlexQuery.NET.Parsers.Fql](https://www.nuget.org/packages/FlexQuery.NET.Parsers.Fql) | FQL (FlexQuery Language) syntax parser | | [FlexQuery.NET.Parsers.MiniOData](https://www.nuget.org/packages/FlexQuery.NET.Parsers.MiniOData) | Lightweight OData-compatible syntax parser | +| [FlexQuery.NET.OpenApi](https://www.nuget.org/packages/FlexQuery.NET.OpenApi) | OpenAPI/Swagger documentation and examples for FlexQuery endpoints | ## Supported Query Formats ```http GET /api/users?filter=age:gte:18&sort=name:asc&page=1&pageSize=20 # DSL (default) -GET /api/users?filter=Age >= 18 AND Status = 'Active' # JQL +GET /api/users?filter=Age >= 18 AND Status = 'Active' # FQL GET /api/users?$filter=Age ge 18 and Status eq 'Active' # OData ``` diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index e0c1d08..a3e2bcb 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -35,6 +35,7 @@ export default defineConfig({ nav: [ { text: 'Guide', link: '/guide/getting-started' }, { text: 'Examples', link: '/examples/basic' }, + { text: 'Migration v3→v4', link: '/migration/v3-to-v4' }, { text: 'Migration v2→v3', link: '/migration/v2-to-v3' }, { text: 'Links', @@ -149,7 +150,7 @@ export default defineConfig({ { text: 'Architecture', items: [ - { text: 'Provider Model', link: '/guide/architecture/provider-model' } + { text: 'Provider Model', link: '/architecture/provider-model' } ] }, { @@ -158,7 +159,8 @@ export default defineConfig({ { text: 'Query Language', link: '/shared/query-language' }, { text: 'Operators', link: '/shared/operators' }, { text: 'Migration v1 → v2', link: '/migration' }, - { text: 'Migration v2 → v3', link: '/migration/v2-to-v3' } + { text: 'Migration v2 → v3', link: '/migration/v2-to-v3' }, + { text: 'Migration v3 → v4', link: '/migration/v3-to-v4' } ] } ], diff --git a/docs/adapters/ag-grid.md b/docs/adapters/ag-grid.md index 89195da..20fc56a 100644 --- a/docs/adapters/ag-grid.md +++ b/docs/adapters/ag-grid.md @@ -263,10 +263,9 @@ public class SalesController : ControllerBase // 1. Parse the AG Grid payload var options = request.ToQueryOptions(); - // 2. Execute with Dapper + // 2. Execute with Dapper (dialect is auto-detected from the DbConnection) var result = await ((DbConnection)_db).FlexQueryAsync(options, opts => { - opts.Dialect = new SqlServerDialect(); opts.AllowedFields = new HashSet { "Id", "Product", "Category", "Region", "Amount", "SaleDate" diff --git a/docs/adapters/miniodata.md b/docs/adapters/miniodata.md index 99a4c6f..436b25b 100644 --- a/docs/adapters/miniodata.md +++ b/docs/adapters/miniodata.md @@ -16,11 +16,11 @@ Many enterprise applications have existing clients that send OData-style queries - You are migrating from Microsoft.AspNetCore.OData or similar OData libraries - You have existing clients that send `$filter`, `$orderby`, `$top`, `$skip` parameters -- You want to accept OData syntax on the same endpoint that handles JSON and JQL queries +- You want to accept OData syntax on the same endpoint that handles JSON and FQL queries ### When NOT to Use It -- You are building a new API from scratch — use FlexQuery's native JSON or JQL syntax instead +- You are building a new API from scratch — use FlexQuery's native JSON or FQL syntax instead - You need full OData compliance (metadata, batch, delta, etc.) — use the official OData library - Your clients can be updated to use FlexQuery's native formats @@ -35,12 +35,12 @@ dotnet add package FlexQuery.NET.Parsers.MiniOData ```csharp using FlexQuery.NET.Parsers.MiniOData; -builder.Services.AddFlexQueryMiniOData(); +MiniOData.Register(); ``` -This registers the `MiniODataQueryParser` with the `QueryOptionsParser` and enables the `MiniODataFeature` flag in the DI container. +This registers the `MiniODataQueryParser` with the `QueryOptionsParser` and enables the MiniOData syntax. Must be called **once** at startup. -**Important:** If the MiniOData assembly is loaded, the parser is also auto-registered via reflection in the `QueryOptionsParser` static constructor. Explicit registration via `AddFlexQueryMiniOData()` is recommended for clarity and to enable the feature flag. +**Important:** If the MiniOData assembly is loaded, the parser is also auto-registered via reflection in the `QueryOptionsParser` static constructor. Explicit registration via `MiniOData.Register()` is recommended for clarity. ## Supported Operators @@ -103,17 +103,21 @@ You do **not** need to explicitly tell FlexQuery to use the MiniOData parser. Th 1. **`$` prefix on parameter keys** — If any raw query parameter starts with `$` (e.g., `$filter`, `$orderby`), the MiniOData parser claims the request 2. **OData operator keywords** — If the filter string contains `eq`, `ne`, or `contains(`, the parser recognizes it as OData syntax -This means your API can seamlessly accept JSON DSL, JQL, and OData syntax on the **same endpoint** without structural changes. +This means your API can seamlessly accept JSON DSL, FQL, and OData syntax on the **same endpoint** without structural changes. ## Basic Example +In v4, MiniOData provides a dedicated `MiniODataRequest` canonical DTO that explicitly accepts OData parameters. This separates transport from parsing. + ```csharp [HttpGet("products")] public async Task GetProducts( - [FromQuery] FlexQueryParameters parameters) + [FromQuery] MiniODataRequest request) { - // Works with OData, JQL, or JSON — auto-detected! - var result = await _context.Products.FlexQueryAsync(parameters); + // Convert the OData request into standard FlexQuery options + var options = request.ToQueryOptions(); + + var result = await _context.Products.FlexQueryAsync(options); return Ok(result); } ``` @@ -136,9 +140,10 @@ public IQueryable Get() => _context.Products; ```csharp [HttpGet] public async Task Get( - [FromQuery] FlexQueryParameters parameters) + [FromQuery] MiniODataRequest request) { - var result = await _context.Products.FlexQueryAsync(parameters, opts => + var options = request.ToQueryOptions(); + var result = await _context.Products.FlexQueryAsync(options, opts => { opts.AllowedFields = new HashSet { "Id", "Name", "Price", "Category" }; opts.StrictFieldValidation = true; @@ -153,7 +158,7 @@ public async Task Get( - Field-level security (`AllowedFields`, `BlockedFields`) - Consistent validation across all syntax types - No dependency on the full OData model builder -- Same endpoint handles OData + JSON + JQL clients simultaneously +- Same endpoint handles OData + JSON + FQL clients simultaneously **What you lose:** - `$metadata` endpoint (replace with Swagger) @@ -174,7 +179,7 @@ public async Task Get( ## Best Practices -1. **Use MiniOData as a migration bridge** — For new APIs, prefer FlexQuery's native JSON or JQL syntax +1. **Use MiniOData as a migration bridge** — For new APIs, prefer FlexQuery's native JSON or FQL syntax 2. **Document supported features** — Clearly communicate to clients which OData features are and aren't supported 3. **Set `AllowedFields`** — OData clients often expect full access to the entity model. Restrict field access explicitly 4. **Test with real OData clients** — Libraries like `Simple.OData.Client` can verify compatibility diff --git a/docs/architecture/provider-model.md b/docs/architecture/provider-model.md index 8d6da09..85729af 100644 --- a/docs/architecture/provider-model.md +++ b/docs/architecture/provider-model.md @@ -13,7 +13,7 @@ FlexQueryParameters (raw input from any source) │ ▼ Parser Layer (shared) - ├── JqlQueryParser + ├── FQLQueryParser ├── JsonQueryParser ├── DslQueryParser ├── MiniODataQueryParser (optional) @@ -43,7 +43,7 @@ FlexQueryParameters (raw input from any source) │ ├── ApplySort() → OrderBy/ThenBy expressions │ ├── ApplyPaging() → Skip/Take │ ├── ApplySelect() → Dynamic projection - │ └── ApplyFilteredIncludes() → Include/ThenInclude + │ └── ApplyExpand() → Include/ThenInclude │ │ │ ▼ │ EF Core → SQL (via database provider) @@ -117,7 +117,7 @@ The validation pipeline checks: | **Filtered includes** | ✅ Via `Include().Where()` | ✅ Via JOIN + WHERE | | **Aggregation** | ✅ Via LINQ GroupBy | ✅ Via SQL GROUP BY | | **SQL preview** | ✅ `ToSqlPreview()` | ✅ Access `SqlCommand.Sql` directly | -| **Dialect awareness** | ❌ Handled by EF provider | ✅ Explicit `ISqlDialect` | +| **Dialect awareness** | ❌ Handled by EF provider | ✅ Auto-detected from `DbConnection` | | **Connection management** | ✅ Via DbContext | ❌ You manage `DbConnection` | | **Migrations** | ✅ EF Migrations | ❌ Not applicable | diff --git a/docs/examples/basic.md b/docs/examples/basic.md index d77e4ab..dbfed20 100644 --- a/docs/examples/basic.md +++ b/docs/examples/basic.md @@ -142,7 +142,7 @@ ORDER BY Status ASC, Name ASC, CreatedAt DESC --- -## Example 5: JQL Filter +## Example 5: FQL Filter **Request:** ``` diff --git a/docs/examples/filtering.md b/docs/examples/filtering.md index 51b1b7c..e843405 100644 --- a/docs/examples/filtering.md +++ b/docs/examples/filtering.md @@ -29,7 +29,7 @@ Customers who have *at least one* order over £500. ### Request ```http -GET /api/customers?query=orders.any(total > 500) +GET /api/customers?filter=orders.any(total > 500) ``` ### LINQ Translation @@ -47,7 +47,7 @@ Customers with a single order that is BOTH cancelled AND over £500. ### Request ```http -GET /api/customers?query=orders.any(status = "Cancelled" AND total > 500) +GET /api/customers?filter=orders.any(status = "Cancelled" AND total > 500) ``` ### LINQ Translation @@ -63,7 +63,7 @@ Customers with a cancelled order that contains a specific product. ### Request ```http -GET /api/customers?query=orders.any(status = "Cancelled" AND orderItems.any(productName CONTAINS "Laptop")) +GET /api/customers?filter=orders.any(status = "Cancelled" AND orderItems.any(productName CONTAINS "Laptop")) ``` --- @@ -76,7 +76,7 @@ Customers where ALL orders are completed. ### Request ```http -GET /api/customers?query=orders.all(status = "Shipped" OR status = "Delivered") +GET /api/customers?filter=orders.all(status = "Shipped" OR status = "Delivered") ``` ### LINQ Translation @@ -92,8 +92,8 @@ Customers without an assigned profile, or with a specific nested null check. ### Request ```http -GET /api/customers?query=profile isnull -GET /api/customers?query=profile.bio notnull +GET /api/customers?filter=profile isnull +GET /api/customers?filter=profile.bio notnull ``` --- @@ -104,7 +104,7 @@ Customers with more than 3 orders. ### Request ```http -GET /api/customers?query=orders count > 3 +GET /api/customers?filter=orders count > 3 ``` --- @@ -115,5 +115,5 @@ Orders placed in Q1 2026. ### Request ```http -GET /api/orders?query=orderDate between "2026-01-01","2026-03-31" +GET /api/orders?filter=orderDate between "2026-01-01","2026-03-31" ``` diff --git a/docs/examples/formats.md b/docs/examples/formats.md index 746b88a..2025759 100644 --- a/docs/examples/formats.md +++ b/docs/examples/formats.md @@ -15,43 +15,4 @@ GET /api/customers --- -## JSON Format Filter (Complex OR/AND Nesting) -Active customers in London OR any pending customers. This is best sent as a URL-Encoded JSON string. - -### Request -```http -GET /api/customers - ?filter={"logic":"or","filters":[{"logic":"and","filters":[{"field":"city","operator":"eq","value":"London"},{"field":"status","operator":"eq","value":"Active"}]},{"field":"status","operator":"eq","value":"Pending"}]} -``` - -### Decoded JSON Payload -```json -{ - "logic": "or", - "filters": [ - { - "logic": "and", - "filters": [ - { "field": "city", "operator": "eq", "value": "London" }, - { "field": "status", "operator": "eq", "value": "Active" } - ] - }, - { "field": "status", "operator": "eq", "value": "Pending" } - ] -} -``` - ---- - -## Indexed Format (Form Submission) - -Filter by multiple fields using standard HTML array-style parameters (often default in jQuery or basic form submissions). - -### Request -```http -GET /api/customers - ?filter[0].field=city&filter[0].operator=eq&filter[0].value=London - &filter[1].field=status&filter[1].operator=eq&filter[1].value=Active - &logic=and -``` diff --git a/docs/guide/aspnet-integration.md b/docs/guide/aspnet-integration.md index 433c69a..dec116c 100644 --- a/docs/guide/aspnet-integration.md +++ b/docs/guide/aspnet-integration.md @@ -24,24 +24,22 @@ dotnet add package FlexQuery.NET.AspNetCore --- -## Service Registration - -Unlike v3, FlexQuery.NET v4 relies on a robust dependency injection container. To enable the declarative security attributes, register the core engine, your execution provider, and the ASP.NET Core security filters in `Program.cs`: +Unlike v3, FlexQuery.NET v4 uses a static configuration model. To enable the declarative security attributes, configure the global defaults and register the ASP.NET Core security filters in `Program.cs`: ```csharp -using FlexQuery.NET.DependencyInjection; -using FlexQuery.NET.EntityFrameworkCore.DependencyInjection; +using FlexQuery.NET; using FlexQuery.NET.AspNetCore.DependencyInjection; var builder = WebApplication.CreateBuilder(args); -// 1. Register Core Engine -builder.Services.AddFlexQuery(); - -// 2. Register Execution Provider (e.g. EF Core) -builder.Services.AddFlexQueryEntityFrameworkCore(); +// 1. Configure global FlexQuery defaults (optional) +FlexQueryCore.Configure(options => +{ + options.MaxPageSize = 1000; + options.MaxFieldDepth = 5; +}); -// 3. Register MVC with FlexQuery Security +// 2. Register MVC with FlexQuery Security builder.Services.AddControllers() .AddFlexQuerySecurity(); @@ -49,7 +47,7 @@ var app = builder.Build(); ``` > [!NOTE] -> `AddFlexQuerySecurity()` registers the `FieldAccessFilter` into the ASP.NET Core MVC pipeline. It requires the core `AddFlexQuery()` engine to be registered first. +> `AddFlexQuerySecurity()` registers the `FieldAccessFilter` into the ASP.NET Core MVC pipeline. No core engine DI registration is needed — FlexQuery.NET uses a static configuration model. --- @@ -67,9 +65,16 @@ The `[FieldAccess]` attribute allows you to define field security rules directly )] public async Task GetUsers([FromQuery] FlexQueryParameters parameters) { - // The FlexQueryAsync overload natively extracts the security rules - // from the HttpContext metadata populated by the [FieldAccess] filter. - var result = await _context.Users.FlexQueryAsync(parameters, HttpContext); + // Retrieve the security rules populated by the [FieldAccess] filter + var execOptions = HttpContext.GetFlexQueryExecutionOptions(); + + var result = await _context.Users.FlexQueryAsync(parameters, exec => + { + exec.AllowedFields = execOptions.AllowedFields; + exec.FilterableFields = execOptions.FilterableFields; + exec.SortableFields = execOptions.SortableFields; + exec.MaxFieldDepth = execOptions.MaxFieldDepth; + }); return Ok(result); } diff --git a/docs/guide/basic-usage.md b/docs/guide/basic-usage.md index 3a7397a..05bb809 100644 --- a/docs/guide/basic-usage.md +++ b/docs/guide/basic-usage.md @@ -111,6 +111,32 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame } ``` +### HTTP POST Requests +If your query is too large for a URL query string, you can use the `FlexQueryRequest` model to accept the query via a JSON POST body. The properties are exactly the same as `FlexQueryParameters`. + +```csharp +[HttpPost("query")] +public async Task QueryUsers([FromBody] FlexQueryRequest request) +{ + var result = await _context.Users.FlexQueryAsync(request, options => + { + options.AllowedFields = ["Id", "Name", "Price", "Status"]; + }); + + return Ok(result); +} +``` + +**JSON Payload:** +```json +{ + "filter": "Status:eq:Active", + "sort": "Name:asc", + "page": 1, + "pageSize": 20 +} +``` + This single call handles: 1. Parsing the query string. 2. Validating the requested fields against your `AllowedFields` policy. diff --git a/docs/guide/comparison-libraries.md b/docs/guide/comparison-libraries.md index 1941644..a46ecf9 100644 --- a/docs/guide/comparison-libraries.md +++ b/docs/guide/comparison-libraries.md @@ -22,7 +22,7 @@ When evaluating .NET packages for dynamic querying, developers frequently encoun | Feature | FlexQuery.NET | Gridify | Sieve | | :--- | :--- | :--- | :--- | -| Filtering | Yes (DSL, JQL, JSON) | Yes | Yes | +| Filtering | Yes (DSL, FQL, JSON) | Yes | Yes | | Sorting | Yes | Yes | Yes | | Paging | Yes | Yes | Yes | | Projection (Select) | ✅ Yes (Dynamic) | ❌ No | ❌ No | @@ -72,7 +72,7 @@ Sieve is an attribute-driven filtering, sorting, and paging library. FlexQuery.NET is a **unified query pipeline** built on top of `IQueryable` (and ADO.NET via Dapper). It supports: -- Filtering (DSL, JQL, JSON) +- Filtering (DSL, FQL, JSON) - Sorting - Projection (dynamic `SELECT` at runtime) - Includes / joins with nested filtering diff --git a/docs/guide/comparison.md b/docs/guide/comparison.md index ca262d4..defbc53 100644 --- a/docs/guide/comparison.md +++ b/docs/guide/comparison.md @@ -211,7 +211,7 @@ OData emphasizes standardized metadata-driven REST interoperability, but brings GET /api/users?filter=orders:any:status:eq:shipped ``` -### FlexQuery.NET (JQL) +### FlexQuery.NET (FQL) ```http GET /api/users?query=Orders.any(Status = "shipped") diff --git a/docs/guide/core-concepts.md b/docs/guide/core-concepts.md index c52631b..eba402c 100644 --- a/docs/guide/core-concepts.md +++ b/docs/guide/core-concepts.md @@ -77,19 +77,19 @@ public async Task Get([FromQuery] FlexQueryParameters parameters) | Property | Type | Purpose | | :--- | :--- | :--- | -| `Filter` | `string?` | DSL filter expression (`Name:eq:Alice`) | -| `Query` | `string?` | Alternative JQL/OData-style string parser | -| `Sort` | `string?` | Sort expression (`Name:asc,Age:desc`) | +| `Filter` | `string?` | DSL filter expression (`Status:eq:Active`) | +| `Sort` | `string?` | Sort expression (`LastName:asc,CreatedAt:desc`) | | `Select` | `string?` | Comma-separated fields to project | | `Include` | `string?` | Navigation properties to include / expand | | `GroupBy` | `string?` | Fields to group by | | `Having` | `string?` | Aggregate condition on groups | +| `Aggregate` | `string?` | Aggregate expressions (`SUM(TotalDue) AS Revenue`) | | `Page` | `int?` | Page number (1-indexed) | | `PageSize` | `int?` | Items per page | | `IncludeCount` | `bool?` | Whether to return total count | | `Distinct` | `bool?` | Apply DISTINCT | | `Mode` | `string?` | Projection mode: `Nested`, `Flat`, `FlatMixed` | -| `UseKeysetPagination` | `bool?` | Force keyset pagination engine | +| `UseKeysetPagination` | `bool` | Force keyset pagination engine | | `Cursor` | `string?` | Keyset pagination token from a previous request | --- @@ -144,22 +144,19 @@ var result = await _db.Users.FlexQueryAsync(parameters, exec => ## Parsing Formats -FlexQuery auto-detects the input format based on the property used in `FlexQueryParameters`: +FlexQuery supports multiple query syntaxes. The active syntax is configured globally via `FlexQueryCore.Configure()` and parser registration. The default is **DSL** (`QuerySyntax.NativeDsl`). -### DSL Format (Standard) +### DSL Format (Default) ```http -GET /api/users?filter=status:eq:active%26name:contains:alice&sort=name:asc +GET /api/users?filter=Status:eq:Active%26LastName:contains:Smi&sort=LastName:asc ``` -### JQL Format +### FQL Format (SQL-like) +Requires `Fql.Register()` at startup and `QuerySyntax.Fql`: ```http -GET /api/users?query=status = "active" AND age >= 18&sort=name:asc +GET /api/users?filter=Status = "Active" AND LastName CONTAINS "Smi"&sort=LastName:asc ``` -### JSON Format -```http -GET /api/users?filter={"logic":"and","filters":[{"field":"status","operator":"eq","value":"active"}]} -``` --- diff --git a/docs/guide/dotnet-comparison.md b/docs/guide/dotnet-comparison.md index d065140..24fc9ce 100644 --- a/docs/guide/dotnet-comparison.md +++ b/docs/guide/dotnet-comparison.md @@ -28,7 +28,7 @@ When building dynamic APIs in .NET, teams often evaluate a spectrum of tools ran | | FlexQuery.NET | Gridify | Sieve | System.Linq.Dynamic.Core | | :--- | :--- | :--- | :--- | :--- | | **Primary focus** | Unified query pipeline | Lightweight filtering | Attribute-based filtering | Dynamic LINQ expressions | -| **Input style** | DSL, JQL, JSON, Indexed | Custom DSL | Query model | LINQ expression strings | +| **Input style** | DSL, FQL | Custom DSL | Query model | LINQ expression strings | | **Projection (`select`)** | ✅ | ❌ | ❌ | ✅ | | **Grouping / Aggregates**| ✅ | ❌ | ❌ | ✅ | | **Filtered includes** | ✅ | ❌ | ❌ | ❌ | diff --git a/docs/guide/extension-methods.md b/docs/guide/extension-methods.md index 3a57de5..7a800db 100644 --- a/docs/guide/extension-methods.md +++ b/docs/guide/extension-methods.md @@ -10,7 +10,9 @@ Extension methods preserve the idiomatic .NET style. Your controller logic stays --- -## `FlexQueryAsync` ⭐ Recommended +## Core Pipeline (`IQueryable`) + +### `FlexQueryAsync` ⭐ Recommended The all-in-one unified pipeline method. Handles parsing, validation, filtering, sorting, paging, includes, and projection in a single secure call. @@ -28,6 +30,19 @@ public async Task Get([FromQuery] FlexQueryParameters parameters) } ``` +``` + +--- + +## Request Parsing + +### `.ToQueryOptions()` +Converts an incoming `FlexQueryParameters`, `FlexQueryRequest`, or `MiniODataRequest` into the canonical `QueryOptions` AST. This is typically the first step if you are building a custom pipeline. + +```csharp +var options = parameters.ToQueryOptions(); +``` + --- ## Atomic Pipeline Methods @@ -100,6 +115,46 @@ var data = await pagedQuery.ApplySelect(options).ToListAsync(); return Ok(options.BuildQueryResult(data, total)); ``` +--- + +## QueryResult Mapping + +If you need to project or map the results inside a `QueryResult` *after* execution (for example, mapping entities to DTOs), FlexQuery provides helper extensions to safely cast the data while preserving pagination metadata. + +### `.ToProjectedQueryResult()` +Casts the underlying `Data` collection from one type to another. + +```csharp +var entityResult = await _context.Users.FlexQueryAsync(options); +var dtoResult = entityResult.ToProjectedQueryResult(); +``` + +### `.ToObjectResult()` & `.ToDynamicResult()` +Casts the inner generic types to `object` or `dynamic`, which is useful for untyped serialization or dynamic projection results. + +```csharp +var objResult = entityResult.ToObjectResult(); +``` + +--- + +## Dapper Extensions (`DbConnection`) + +If you are using the `FlexQuery.NET.Dapper` provider, extension methods are provided directly on `System.Data.Common.DbConnection`. + +### `.FlexQueryAsync()` +Executes the full FlexQuery pipeline against an open database connection using Dapper. + +```csharp +await using var connection = new SqlConnection(_connectionString); +await connection.OpenAsync(); + +var result = await connection.FlexQueryAsync(parameters, opts => +{ + opts.AllowedFields = ["Id", "Name", "Email"]; +}); +``` + ## Related Topics - [Execution Pipeline](/guide/execution-pipeline) diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index 56a291e..674f02f 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -13,7 +13,7 @@ Filtering applies a `WHERE` clause to your `IQueryable`. It supports: - Null checks - Collection predicates (`any`, `all`) - Nested AND/OR logic groups -- Multiple input formats (DSL, JQL, JSON, Indexed) +- Multiple input formats (DSL, FQL, MiniOData) --- @@ -32,7 +32,7 @@ Use filtering when clients need to **search or narrow down results** at runtime ## Filter Formats -FlexQuery.NET auto-detects the format from the query string. +FlexQuery.NET supports multiple filter syntaxes. The active syntax is configured globally via `FlexQueryCore.Configure()` and parser registration (`Fql.Register()`, `MiniOData.Register()`). The default is **DSL** (`QuerySyntax.NativeDsl`). ### DSL Format @@ -50,53 +50,20 @@ GET /api/users?filter=name:contains:alice GET /api/users?filter=status:eq:active%26age:gte:18 ``` -### JQL Format (SQL-like) +### FQL Format (SQL-like) -Natural language query syntax using the `query` parameter. +Natural language query syntax. FQL uses the same `filter` parameter as DSL but requires SQL-like expressions. To enable FQL, register the parser at startup: +```csharp +FlexQueryCore.Configure(options => options.QuerySyntax = QuerySyntax.Fql); +Fql.Register(); ``` -GET /api/users?query=status = "active" AND age >= 18 -GET /api/users?query=(name = "alice" OR name = "bob") AND status = "active" -``` - -### JSON Format - -Structured filter tree for complex nested logic. ``` -GET /api/users?filter={"logic":"and","filters":[ - {"field":"status","operator":"eq","value":"active"}, - {"field":"age","operator":"gte","value":"18"} -]} +GET /api/users?filter=status = "active" AND age >= 18 +GET /api/users?filter=(name = "alice" OR name = "bob") AND status = "active" ``` -**Nested OR group:** - -```json -{ - "logic": "and", - "filters": [ - { "field": "status", "operator": "eq", "value": "active" }, - { - "logic": "or", - "filters": [ - { "field": "name", "operator": "contains", "value": "alice" }, - { "field": "name", "operator": "contains", "value": "bob" } - ] - } - ] -} -``` - -### Indexed Format - -Good for form-based UIs. - -``` -GET /api/users?filter[0].field=status&filter[0].operator=eq&filter[0].value=active - &filter[1].field=age&filter[1].operator=gte&filter[1].value=18 - &logic=and -``` --- @@ -158,11 +125,11 @@ SELECT * FROM Users u WHERE EXISTS ( ) ``` -### Nested ANY Filter +### Nested ANY Filter (FQL) -Request: +Request (FQL syntax): ``` -GET /api/users?query=Orders.any(Status = "shipped" AND Amount > 100) +GET /api/users?filter=Orders.any(Status = "shipped" AND Amount > 100) ``` ### Range Filter @@ -226,12 +193,12 @@ GET /api/users?filter=status:eq:active&page=1&pageSize=3 ```csharp // WRONG — client can filter on any field, including sensitive ones -var result = await _context.Users.FlexQueryAsync(parameters); +var result = await _context.Users.FlexQueryAsync(parameters); ``` ```csharp // CORRECT -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "name", "email", "status" }; }); @@ -270,4 +237,4 @@ FlexQuery.NET uses expression trees — never string concatenation. - Collection predicates (`any`, `all`) generate `EXISTS` subqueries in SQL — efficient and server-side. - `contains` maps to SQL `LIKE '%value%'` which **cannot use an index**. Prefer `startswith` for large tables. - Deeply nested OR groups with `contains` can be slow — consider full-text search for those cases. -- `CaseInsensitive = true` (default) uses database collation — no performance penalty on most providers. +- String comparisons respect the database collation — case sensitivity is handled by the database provider. diff --git a/docs/guide/fluent-builder.md b/docs/guide/fluent-builder.md new file mode 100644 index 0000000..f9cdedc --- /dev/null +++ b/docs/guide/fluent-builder.md @@ -0,0 +1,145 @@ +# Fluent Query Builder + +## Overview + +FlexQuery.NET v4 introduces a fully typed, fluent API for constructing queries programmatically. The Fluent Query Builder allows you to build `QueryOptions` objects directly in C# without concatenating strings or parsing JSON. + +## Why this feature exists + +While FlexQuery's string-based DSL and JSON formats are excellent for HTTP APIs and frontend-to-backend communication, they are cumbersome to use when constructing queries dynamically *within* your backend code (e.g., service-to-service communication, background workers, or dynamic repository methods). The Fluent Builder provides a type-safe, discoverable alternative. + +## Basic Usage + +Note: v4 replaces string-based aggregate function names with the strongly-typed `AggregateFunction` enum. Use the dedicated builder methods (`.Sum()`, `.Count()`, `.Avg()`, `.Min()`, `.Max()`) instead of `.Custom()`. + +The entry point for the fluent API is the static `Query.Create()` method. + +```csharp +using FlexQuery.NET.Builders.Fluent; + +var options = Query.Create() + .Filter(f => f + .Equal("Status", "Active") + .GreaterThan("Age", 18)) + .Sort(s => s.Ascending("CreatedAt")) + .Page(1, pageSize: 50) + .Select("Id", "Name", "Email") + .Build(); + +// options is now a fully formed QueryOptions object +var result = await _context.Users.FlexQueryAsync(options); +``` + +## Filtering + +The `Filter` method accepts an action that configures a `FilterGroupBuilder`. You can build complex, nested logical groups. + +### Simple Conditions + +```csharp +var options = Query.Create() + .Filter(f => f.Equal("Category", "Electronics")) + .Build(); +``` + +### Logical Operators + +Conditions are implicitly ANDed. Use `.And(...)` and `.Or(...)` for nested groups. + +```csharp +var options = Query.Create() + .Filter(f => f + .Equal("Status", "Active") + .GreaterThan("Price", 100)) + .Build(); +``` + +### Nested Groups + +For grouped logic (like parentheses in SQL), use `.And(...)` or `.Or(...)` with a lambda: + +```csharp +// Generates: Status eq 'Active' AND (Role eq 'Admin' OR Role eq 'Manager') +var options = Query.Create() + .Filter(f => f + .Equal("Status", "Active") + .And(g => g + .Equal("Role", "Admin") + .Or(o => o.Equal("Role", "Manager")))) + .Build(); +``` + +### All Filter Operators + +```csharp +var options = Query.Create() + .Filter(f => f + .Equal("A", "x") + .NotEqual("B", "y") + .GreaterThan("C", 1) + .GreaterThanOrEqual("D", 2) + .LessThan("E", 3) + .LessThanOrEqual("F", 4) + .Contains("G", "sub") + .StartsWith("H", "pre") + .EndsWith("I", "suf") + .In("J", "a", "b", "c") + .NotIn("K", "d", "e") + .IsNull("L") + .IsNotNull("M") + .Between("N", 10, 20)) + .Build(); +``` + +## Sorting + +You can chain `.Sort()` calls using a `SortBuilder` lambda to apply secondary and tertiary sorts. + +```csharp +var options = Query.Create() + .Sort(s => s.Ascending("Category.Name")) + .Sort(s => s.Descending("Price")) + .Build(); +``` + +## Paging + +Configure offset paging using the `.Page()` method. + +```csharp +var options = Query.Create() + .Page(pageNumber: 2, pageSize: 20) + .Build(); +``` + +## Includes (Expand) + +You can specify navigation properties to eager load, along with optional nested filters for filtered includes. + +```csharp +var options = Query.Create() + .Expand(e => e.Path("Orders", f => f.GreaterThan("Total", 100))) + .Build(); +``` + +## Aggregations + +Configure server-side aggregations safely: + +```csharp +var options = Query.Create() + .Aggregate(a => a + .Sum("Amount", "TotalAmount") + .Count("Id", "OrderCount") + .Count("TotalCount") + .Avg("Price", "AvgPrice") + .Min("Date", "Earliest") + .Max("Date", "Latest")) + .Build(); +``` + +## When to use + +* **Service Layer:** When one backend service needs to query another using FlexQuery conventions. +* **Background Jobs:** When a worker process needs to generate a dynamic report. +* **Unit Testing:** When writing integration tests for your FlexQuery endpoints, it's safer to use the builder than to hardcode DSL strings in your tests. \ No newline at end of file diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index fdd4f56..d85a76f 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -36,16 +36,14 @@ dotnet add package FlexQuery.NET.AspNetCore ### Step 1: Configure Services -In `Program.cs`, you must register the core engine and your execution provider. +In `Program.cs`, configure the global FlexQuery defaults and register the ASP.NET Core integration. ```csharp -using FlexQuery.NET.DependencyInjection; -using FlexQuery.NET.EntityFrameworkCore.DependencyInjection; -using FlexQuery.NET.AspNetCore.DependencyInjection; +using FlexQuery.NET; +using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); @@ -53,8 +51,8 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("Default"))); -// 1. Register FlexQuery Core globally -builder.Services.AddFlexQuery(options => +// 1. Configure global FlexQuery defaults (optional — called once at startup) +FlexQueryCore.Configure(options => { options.MaxPageSize = 1000; options.DefaultPageSize = 50; @@ -64,10 +62,7 @@ builder.Services.AddFlexQuery(options => options.MaxFieldDepth = 5; }); -// 2. Register the EF Core Provider -builder.Services.AddFlexQueryEntityFrameworkCore(); - -// 3. Register MVC and optional declarative Security ([FieldAccess]) +// 2. Register MVC and optional declarative Security ([FieldAccess]) builder.Services .AddControllers() .AddFlexQuerySecurity(); @@ -83,7 +78,7 @@ app.Run(); This optional integration automatically registers the `FieldAccessFilter` into the ASP.NET Core MVC pipeline. This enables you to use declarative security attributes directly on your controllers: ```csharp -[FieldAccess(AllowedFields = new[] { "Id", "Name", "Email" })] +[FieldAccess(Allowed = new[] { "Id", "Name", "Email" })] [HttpGet] public async Task GetUsers() { ... } ``` @@ -188,23 +183,26 @@ GET /api/users?filter=Status:eq:active&sort=Name:asc&page=1&pageSize=10&select=I `FlexQueryParameters` is the public-facing DTO. Bind it directly from the query string in GET requests. +`FlexQueryParameters` inherits from `FlexQueryBase` and exposes all common query options: + ```csharp -public class FlexQueryParameters +public class FlexQueryParameters : FlexQueryBase { - public string? Query { get; set; } // JQL: query=status="active" - public string? Filter { get; set; } // DSL: filter=status:eq:active - public string? Sort { get; set; } // sort=name:asc,createdAt:desc - public string? Select { get; set; } // select=id,name,email - public string? Include { get; set; } // include=Orders,Profile - public string? GroupBy { get; set; } // groupBy=status - public string? Having { get; set; } // having=count:gt:5 - public int? Page { get; set; } // page=1 - public int? PageSize { get; set; } // pageSize=20 + // All properties inherited from FlexQueryBase: + public string? Filter { get; set; } // DSL: filter=status:eq:active + public string? Sort { get; set; } // sort=name:asc,createdAt:desc + public string? Select { get; set; } // select=id,name,email + public string? Include { get; set; } // include=Orders,Profile + public string? GroupBy { get; set; } // groupBy=status + public string? Having { get; set; } // having=count:gt:5 + public string? Aggregate { get; set; } // aggregate=SUM(Amount) AS Total + public int? Page { get; set; } // page=1 + public int? PageSize { get; set; } // pageSize=20 public bool? IncludeCount { get; set; } // includeCount=true public bool? Distinct { get; set; } // distinct=true - public string? Mode { get; set; } // mode=Flat - public bool? UseKeysetPagination { get; set; } - public string? Cursor { get; set; } + public string? Mode { get; set; } // mode=Flat + public bool UseKeysetPagination { get; set; } + public string? Cursor { get; set; } } ``` @@ -222,13 +220,13 @@ public class FlexQueryParameters ```text FlexQueryParameters - → Parse (QueryOptionsParser) + → Parse (ToQueryOptions()) → Validate (field access, operators, depth against Server Policy) → ApplyFilter → ApplySort → CountAsync (for totalCount) → ApplyPaging - → ApplyFilteredIncludes + → ApplyExpand → ApplySelect (if projection requested) → ToListAsync → QueryResult @@ -313,14 +311,13 @@ if (!result.IsValid) For extremely high-traffic APIs using the EF Core provider, you can enable **Expression Caching**. This instructs FlexQuery to cache the compiled LINQ Expression Trees for repeated query shapes, bypassing the CPU overhead of reflection and tree generation on subsequent identical requests. -In `Program.cs` (Global configuration): +Expression caching is **enabled by default**. You can tune it globally at startup: ```csharp using FlexQuery.NET.Caching; -// Enable global expression caching -FlexQueryCacheSettings.EnableCache = true; -FlexQueryCacheSettings.MaxCacheSize = 5000; +// Expression caching is enabled by default (MaxCacheSize = 2000) +FlexQueryCacheSettings.MaxCacheSize = 5000; // Increase for high-traffic APIs ``` ## Best Practices diff --git a/docs/guide/grouping.md b/docs/guide/grouping.md index 2495af6..f1f32fc 100644 --- a/docs/guide/grouping.md +++ b/docs/guide/grouping.md @@ -42,45 +42,60 @@ Use grouping for: ### Count by Status +**DSL:** ``` -GET /api/users?select=status,count()&groupBy=status +GET /api/users?select=status&aggregates=*:count&groupBy=status +``` + +**FQL:** +``` +GET /api/users?select=status&aggregates=COUNT(*)&groupBy=status ``` **Response:** ```json { "totalCount": 3, - "resultCount": 3, - "page": 1, - "pageSize": 20, "data": [ - { "status": "active", "allCount": 42 }, - { "status": "inactive", "allCount": 6 }, - { "status": "pending", "allCount": 12 } + { "status": "active", "Count": 42 }, + { "status": "inactive", "Count": 6 }, + { "status": "pending", "Count": 12 } ] } ``` ### Sum of Orders by User +**DSL:** +``` +GET /api/orders?select=userId&aggregates=Amount:sum&groupBy=userId +``` + +**FQL:** ``` -GET /api/orders?select=userId,sum(amount)&groupBy=userId +GET /api/orders?select=UserId&aggregates=SUM(Amount)&groupBy=UserId ``` **Response:** ```json { "data": [ - { "userId": 1, "amountSum": 1250.00 }, - { "userId": 2, "amountSum": 780.50 } + { "userId": 1, "AmountSum": 1250.00 }, + { "userId": 2, "AmountSum": 780.50 } ] } ``` ### Average with HAVING +**DSL:** +``` +GET /api/orders?select=customerId&aggregates=Amount:avg&groupBy=customerId&having=avg(Amount):gt:500 +``` + +**FQL:** ``` -GET /api/orders?select=customerId,avg(amount)&groupBy=customerId&having=avg(amount):gt:500 +GET /api/orders?select=CustomerId&aggregates=AVG(Amount)&groupBy=CustomerId&having=AVG(Amount) > 500 ``` Only returns groups where the average order amount exceeds 500. @@ -89,31 +104,56 @@ Only returns groups where the average order amount exceeds 500. ```json { "data": [ - { "customerId": 1, "amountAvg": 625.00 }, - { "customerId": 5, "amountAvg": 812.50 } + { "customerId": 1, "AmountAvg": 625.00 }, + { "customerId": 5, "AmountAvg": 812.50 } ] } ``` ### Sort by Aggregate +**DSL:** ``` -GET /api/users?select=status,count()&groupBy=status&sort=count():desc +GET /api/users?select=status&aggregates=*:count&groupBy=status&sort=Count:desc +``` + +**FQL:** +``` +GET /api/users?select=status&aggregates=COUNT(*)&groupBy=status&sort=Count DESC ``` --- ## Aggregate Syntax -In the `select` parameter, use function call syntax: +Aggregates are specified in the dedicated `aggregates` parameter, not in `select`. + +**DSL format:** +``` +aggregates=Field:Function[:Alias] +``` + +| DSL Example | Alias Generated | Description | +| :--- | :--- | :--- | +| `*:count` | `Count` | Count all rows in group | +| `Amount:sum` | `AmountSum` | Sum of Amount field | +| `Amount:avg` | `AmountAvg` | Average of Amount field | +| `Amount:sum:TotalSales` | `TotalSales` | Sum with explicit alias | + +**FQL format:** +``` +aggregates=FUNCTION(Field) [AS Alias] +``` -| Syntax | Alias Generated | Description | +| FQL Example | Alias Generated | Description | | :--- | :--- | :--- | -| `count()` | `allCount` | Count all rows in group | -| `sum(amount)` | `amountSum` | Sum of `amount` field | -| `avg(amount)` | `amountAvg` | Average of `amount` field | +| `COUNT(*)` | `Count` | Count all rows in group | +| `SUM(Amount)` | `AmountSum` | Sum of Amount field | +| `AVG(Amount)` | `AmountAvg` | Average of Amount field | +| `SUM(Amount) AS TotalSales` | `TotalSales` | Sum with explicit alias | -Alias format: `{field}{Function}` (camelCase field, PascalCase function). +Auto-generated aliases follow PascalCase convention (`AmountSum`, `PriceAvg`, `Count`). +Explicitly provided aliases are preserved exactly as written. --- @@ -121,13 +161,31 @@ Alias format: `{field}{Function}` (camelCase field, PascalCase function). The `having` parameter filters groups after aggregation. +**DSL:** +``` +having=function:field:operator:value +having=function(field):operator:value +``` + +| DSL Example | Meaning | +| :--- | :--- | +| `having=count:gt:5` | Groups with count > 5 | +| `having=sum(Amount):gt:1000` | Groups with sum > 1000 | +| `having=avg(Amount):between:100,500` | Groups with avg between 100 and 500 | + +**FQL:** ``` -having=count():gt:5 -having=sum(amount):gte:1000 -having=avg(amount):between:100,500 +having=FUNCTION(Field) OPERATOR value ``` -Supported operators in HAVING: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between` +| FQL Example | Meaning | +| :--- | :--- | +| `having=COUNT(*) > 5` | Groups with count > 5 | +| `having=SUM(Amount) > 1000` | Groups with sum > 1000 | +| `having=AVG(Price) >= 500` | Groups with avg >= 500 | + +Supported HAVING operators (DSL): `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between` +Supported HAVING operators (FQL): `=`, `!=`, `>`, `>=`, `<`, `<=` --- @@ -136,12 +194,15 @@ Supported operators in HAVING: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between` ### Programmatic Grouping ```csharp +using FlexQuery.NET.Models; +using FlexQuery.NET.Models.Aggregates; + var options = new QueryOptions { GroupBy = new List { "status" }, Aggregates = new List { - new AggregateModel { Function = "count", Alias = "allCount" } + new AggregateModel { Function = AggregateFunction.Count, Alias = "allCount" } } }; @@ -151,16 +212,19 @@ var result = await _context.Users.ApplySelect(options).ToListAsync(); ### With HAVING ```csharp +using FlexQuery.NET.Models; +using FlexQuery.NET.Models.Aggregates; + var options = new QueryOptions { GroupBy = new List { "status" }, Aggregates = new List { - new AggregateModel { Function = "count", Alias = "allCount" } + new AggregateModel { Function = AggregateFunction.Count, Alias = "allCount" } }, Having = new HavingCondition { - Function = "count", + Function = AggregateFunction.Count, Operator = "gt", Value = "5" } @@ -171,18 +235,32 @@ var options = new QueryOptions ## Common Mistakes +### ❌ Mixing aggregates in `select` + +Aggregates must use the dedicated `aggregates` parameter, not `select`. + +``` +# WRONG (v3 style) +GET /api/users?select=status,count()&groupBy=status +``` + +``` +# CORRECT (v4) +GET /api/users?select=status&aggregates=*:count&groupBy=status +``` + ### ❌ Selecting non-grouped fields When using GROUP BY, you can only select grouped fields and aggregates. Selecting `name` when grouping by `status` is undefined behavior. ``` # WRONG -GET /api/users?select=status,name,count()&groupBy=status +GET /api/users?select=status,name&aggregates=*:count&groupBy=status ``` ``` # CORRECT — only grouped fields + aggregates -GET /api/users?select=status,count()&groupBy=status +GET /api/users?select=status&aggregates=*:count&groupBy=status ``` ### ❌ Using HAVING without GROUP BY diff --git a/docs/guide/how-it-works.md b/docs/guide/how-it-works.md index eff61a4..1e88abc 100644 --- a/docs/guide/how-it-works.md +++ b/docs/guide/how-it-works.md @@ -19,7 +19,7 @@ HTTP Query String FlexQueryParameters ← Plain DTO bound from [FromQuery] │ ▼ - QueryOptionsParser.Parse() ← Auto-detects DSL / JQL / JSON / Indexed format + ToQueryOptions() ← Parses using configured QuerySyntax │ ▼ QueryOptions ← Internal AST: FilterGroup, SortNode[], PagingOptions, etc. @@ -54,9 +54,8 @@ It automatically detects which format was used: | Format | Detection | Example | | :--- | :--- | :--- | | **DSL** | `filter=` with colon syntax | `filter=status:eq:active` | -| **JQL** | `query=` parameter present | `query=status = "active"` | -| **JSON** | `filter=` value starts with `{` | `filter={"logic":"and",...}` | -| **Indexed** | Keys like `filter[0].field=` | `filter[0].field=status` | +| **FQL** | `filter=` parameter present | `filter=status = "active"` | + The parsed output is a structured `QueryOptions`: @@ -134,7 +133,7 @@ The expressions are applied to your `IQueryable` using standard LINQ methods: query = query.Where(filterPredicate); // ApplyFilter query = query.OrderBy(keySelector); // ApplySort query = query.Skip(skip).Take(take); // ApplyPaging -query = query.Include(...).Where(...); // ApplyFilteredIncludes +query = query.Include(...).Where(...); // ApplyExpand var projected = query.Select(projection); // ApplySelect ``` diff --git a/docs/guide/include-filtering.md b/docs/guide/include-filtering.md index 7cefd7a..c2ea222 100644 --- a/docs/guide/include-filtering.md +++ b/docs/guide/include-filtering.md @@ -101,7 +101,7 @@ var data = await query.ToListAsync(); `FlexQueryAsync` applies the include pipeline automatically: ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "id", "name", "orders.*" }; }); @@ -185,12 +185,12 @@ GET /api/users?filter=orders:any:status:eq:shipped Both are valid — just different use cases. Know which one you need. -### ❌ Calling ApplyFilteredIncludes after ToListAsync +### ❌ Calling ApplyExpand after ToListAsync ```csharp // WRONG — query already materialized var data = await query.ToListAsync(); -query = query.ApplyFilteredIncludes(options); // too late +query = query.ApplyExpand(options); // too late ``` ```csharp @@ -205,17 +205,17 @@ var data = await query.ToListAsync(); When including multiple collections (e.g., `?include=Orders,Profiles`), EF Core may generate a "cartesian explosion" where the result set grows exponentially. -To optimize this, you can enable **Split Queries** in the execution configuration: +To optimize this, configure EF Core's native query splitting behavior on the `DbContext`: ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, exec => +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - exec.UseSplitQuery = true; -}); + optionsBuilder.UseSqlServer(connectionString, opt => + opt.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); +} ``` -This ensures that each collection is fetched via a separate SQL query, reducing the amount of redundant data transferred. +> [!NOTE] +> The `UseSplitQuery` option was removed from FlexQuery options in v4. Use EF Core's native query splitting configuration instead. --- - -## Summary diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index c2aec74..effd2fb 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -105,7 +105,7 @@ Client input is treated as inherently hostile. The `QueryOptions` parser acts on ## Related Topics -- [Installation and Setup](/get-started/installation) +- [Installation and Setup](/guide/getting-started) - [Filtering and Sorting Syntax](/guide/filtering) - [Security and Governance](/guide/security) - [FlexQuery vs GraphQL/OData](/guide/comparison) diff --git a/docs/guide/linq-integration.md b/docs/guide/linq-integration.md index e383098..f4b45ab 100644 --- a/docs/guide/linq-integration.md +++ b/docs/guide/linq-integration.md @@ -10,13 +10,13 @@ You can apply these filters using standard LINQ `.Where()` calls before passing ### Basic Example (The Secure Way) -Using `ApplyValidatedQueryOptions` is the recommended way to apply dynamic logic. It automatically runs the validation pipeline and throws a `QueryValidationException` if the client input is malicious or malformed. +Using `ApplyFilter`, `ApplySort`, `ApplyPaging`, and `ApplySelect` gives you fine-grained control over each pipeline stage. For a unified approach, use `FlexQueryAsync` which handles parsing, validation, and execution in a single call. ```csharp [HttpGet] -public async Task Get([FromQuery] QueryRequest request) +public async Task Get([FromQuery] FlexQueryParameters parameters) { - var options = QueryOptionsParser.Parse(request); + var options = parameters.ToQueryOptions(); // 1. Define your base query with fixed logic var baseQuery = _context.Products @@ -24,9 +24,11 @@ public async Task Get([FromQuery] QueryRequest request) .Where(p => p.TenantId == _currentUser.TenantId); // 2. Layer FlexQuery on top securely - // This will throw if 'options' contains forbidden fields or invalid operators var results = await baseQuery - .ApplyValidatedQueryOptions(options) + .ApplyFilter(options) + .ApplySort(options) + .ApplyPaging(options) + .ApplySelect(options) .ToListAsync(); return Ok(results); @@ -82,15 +84,17 @@ OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY -- From dynamic Paging ## EF Core: Filtered Includes -If you are using **Entity Framework Core**, you can use `ApplyFilteredIncludes` to apply dynamic filters to related collections within an `Include` statement. This is useful for "Search inside Include" scenarios. +If you are using **Entity Framework Core**, you can use `ApplyExpand` to apply dynamic includes with inline filters to related collections. This is useful for "Search inside Include" scenarios. ```csharp -// Example Request: ?include=Orders(status = "Shipped") or ?include=Orders(status:eq:"Shipped") -var options = QueryOptionsParser.Parse(Request.Query); +// Example Request: ?include=Orders(status:eq:"Shipped") +var parameters = new FlexQueryParameters { Include = @"Orders(status:eq:""Shipped"")" }; +var options = parameters.ToQueryOptions(); var customers = await _context.Customers - .ApplyFilteredIncludes(options) // Applies the filtered navigation - .ApplyValidatedQueryOptions(options) // Applies root filters/paging + .ApplyExpand(options) // Applies the filtered navigation + .ApplyFilter(options) + .ApplySort(options) .ToListAsync(); ``` @@ -118,7 +122,10 @@ var query = _context.Users .Where(u => u.IsActive); // Base condition var results = await query - .ApplyValidatedQueryOptions(options) + .ApplyFilter(options) + .ApplySort(options) + .ApplyPaging(options) + .ApplySelect(options) .ToListAsync(); ``` diff --git a/docs/guide/paging.md b/docs/guide/paging.md index b301f29..4552b1a 100644 --- a/docs/guide/paging.md +++ b/docs/guide/paging.md @@ -224,4 +224,40 @@ if (options.Paging.PageSize > 200) options.Paging.PageSize = 200; - Disable `IncludeCount` (`?includeCount=false`) on high-frequency endpoints where total count is not needed. - Grouped or shaped queries may also calculate `ResultCount` from the shaped query before paging. - Always sort before paging. Without a deterministic `ORDER BY`, results are undefined. -- Use **Keyset Pagination** (`?useKeysetPagination=true&cursor=TOKEN`) for very large datasets to avoid `OFFSET` scanning penalties. FlexQuery.NET generates `WHERE Id > cursor` style queries for keyset navigation. +- Use **Keyset Pagination** (`?useKeysetPagination=true&cursor=TOKEN`) for very large datasets to avoid `OFFSET` scanning penalties. + +--- + +## Keyset (Cursor) Pagination + +For deep paging on large datasets, SQL `OFFSET` becomes exponentially slower because the database must scan and discard all preceding rows. Keyset Pagination (also known as cursor-based or seek pagination) solves this by using `WHERE` clauses (e.g., `WHERE Id > @cursor`) to resume exactly where the last page left off. + +FlexQuery.NET natively supports keyset pagination. + +### How to use Keyset Pagination + +1. **Initial Request:** Set `useKeysetPagination=true` (or `options.UseKeysetPagination = true`) along with your `sort` criteria. + +```http +GET /api/users?sort=createdAt:desc,id:desc&pageSize=50&useKeysetPagination=true +``` + +2. **Retrieve the Token:** The response `QueryResult` will include a `nextCursorToken` if there are more records. + +```json +{ + "data": [ ... ], + "nextCursorToken": "eyJ0eXAiOiJKV1QiLCJ..." +} +``` + +3. **Subsequent Requests:** Pass that token back in the `cursor` parameter to fetch the next page. + +```http +GET /api/users?sort=createdAt:desc,id:desc&pageSize=50&useKeysetPagination=true&cursor=eyJ0eXAiOiJKV1QiLCJ... +``` + +### Requirements for Keyset Pagination + +- **Deterministic Sorting:** The query *must* have a unique sort order. If you sort by `createdAt`, you must include a unique tie-breaker like `id` (e.g. `sort=createdAt:desc,id:desc`), otherwise records sharing the same timestamp may be skipped or duplicated. +- **Unsupported Features:** Keyset pagination cannot be used simultaneously with `GROUP BY` or `DISTINCT` queries. diff --git a/docs/guide/performance-tuning.md b/docs/guide/performance-tuning.md index bafbef3..13c74d5 100644 --- a/docs/guide/performance-tuning.md +++ b/docs/guide/performance-tuning.md @@ -137,37 +137,20 @@ Validation cost is generally proportional to query complexity rather than datase # Expression Caching -FlexQuery.NET can cache the expensive process of parsing query objects and building LINQ Expression trees. This is highly recommended for high-traffic APIs where the same query patterns (even with different values) are reused. +Expression caching is **enabled by default** (`FlexQueryCacheSettings.EnableCache = true`). FlexQuery.NET caches the expensive process of parsing query objects and building LINQ Expression trees, reusing cached expressions for repeated query shapes. -### 1. Global Enable +### Configuration -Enable caching globally in your `Program.cs`: +Caching can be tuned globally at startup: ```csharp using FlexQuery.NET.Caching; -// Enable the caching engine -FlexQueryCacheSettings.EnableCache = true; - -// Optional: Prevent memory leaks in extremely dynamic scenarios -FlexQueryCacheSettings.MaxCacheSize = 5000; - -// Optional: Cache the compiled delegates (useful for LINQ to Objects) -FlexQueryCacheSettings.CacheCompiledLambdas = true; -``` - -### 2. Per-Query Control - -You can override the global setting on individual requests via `QueryOptions`: - -```csharp -var options = parameters.ToQueryOptions(); - -// Force cache for this specific heavy query, even if global cache is off -options.EnableCache = true; +// Expression caching is enabled by default (MaxCacheSize = 2000) +FlexQueryCacheSettings.MaxCacheSize = 5000; // Increase for high-traffic APIs ``` -### 3. How it Works +### How it Works The caching engine generates a stable, canonical key based on the query's structure (fields, operators, logic) but **ignores the literal values**. This means: @@ -293,32 +276,21 @@ Actual execution plans depend on: --- -# Split Query Optimization - -When multiple collection includes are requested, EF Core may generate a single SQL query with many `LEFT JOIN` clauses. This can lead to: +# Split Query Considerations -- **Cartesian Explosion**: The number of result rows multiplies with each additional collection. -- **Redundant Data**: Parent entity data is duplicated for every child row, increasing memory and network usage. +EF Core's split query behavior (`AsSplitQuery`) is controlled directly through EF Core configuration, not through FlexQuery options. When multiple collection includes are requested, EF Core may generate a single SQL query with many `LEFT JOIN` clauses, potentially causing a Cartesian explosion. -FlexQuery.NET supports optional split query execution to mitigate these issues: +To configure split queries, use EF Core's configuration directly on the `DbContext` or query level: ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, exec => +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - // Force EF Core to execute collection includes as separate SQL queries - exec.UseSplitQuery = true; -}); + optionsBuilder.UseSqlServer(connectionString, opt => opt.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); +} ``` -This internally applies `.AsSplitQuery()` to the pipeline. - -> [!IMPORTANT] -> Split query behavior is intentionally configured **server-side** through `QueryExecutionOptions`. Clients define **what** data they want; the server defines **how** it is retrieved. - -### Performance Tradeoffs - -- **Pros**: Drastically reduces data redundancy and memory pressure for wide result sets with many collections. -- **Cons**: Requires multiple roundtrips to the database (one per collection). Not always faster for small datasets or low-latency networks. +> [!NOTE] +> The `UseSplitQuery` option was removed from FlexQuery options in v4. Use EF Core's native query splitting configuration instead. --- diff --git a/docs/guide/performance/api-benchmarks.md b/docs/guide/performance/api-benchmarks.md index c25f8a2..de842ab 100644 --- a/docs/guide/performance/api-benchmarks.md +++ b/docs/guide/performance/api-benchmarks.md @@ -274,7 +274,8 @@ A fully equivalent handwritten implementation that supports all query combinatio FlexQuery.NET supports optional AST and expression caching: ```csharp -FlexQueryCacheSettings.EnableCache = true; +// Expression caching is enabled by default +FlexQueryCacheSettings.MaxCacheSize = 5000; ``` With caching enabled, repeated queries with identical structure (different values) skip parsing and expression generation: @@ -503,7 +504,7 @@ Performance is only one dimension of API design. Flexibility, maintainability, a For a complete picture, see also: - [Parsing Performance](./parsing-performance.md) — upfront cost breakdown -- [Execution Benchmarks](../execution.md) — InMemory query pipeline +- [Execution Benchmarks](../execution-pipeline) — InMemory query pipeline - [Database Execution](./database-execution.md) — SQL Server performance - [Scalability](./scalability.md) — dataset scaling behavior - [Fairness Disclaimers](./fairness-disclaimers.md) — architectural context diff --git a/docs/guide/performance/benchmark-overview.md b/docs/guide/performance/benchmark-overview.md index b696997..8a0dba0 100644 --- a/docs/guide/performance/benchmark-overview.md +++ b/docs/guide/performance/benchmark-overview.md @@ -34,7 +34,7 @@ We separate our benchmarks into three distinct categories to provide maximum tra 2. **[Expression Generation Benchmarks](./expression-generation.md)** Measures the CPU cost and memory allocation of translating the internal model into a concrete `System.Linq.Expressions.Expression` tree. - 3. **[End-to-End Execution Benchmarks](../execution.md)** + 3. **[End-to-End Execution Benchmarks](../execution-pipeline)** Measures the full lifecycle from query string to materialized results using the EF Core InMemory provider. 4. **[API Benchmarks](./api-benchmarks.md)** @@ -58,6 +58,6 @@ We commit to the following principles in our benchmarks: ## Navigating the Results -If you are a library consumer deciding whether to adopt FlexQuery.NET, we recommend focusing entirely on the **[End-to-End Execution Benchmarks](../execution.md)**. This reflects the actual latency added to your HTTP requests. +If you are a library consumer deciding whether to adopt FlexQuery.NET, we recommend focusing entirely on the **[End-to-End Execution Benchmarks](../execution-pipeline)**. This reflects the actual latency added to your HTTP requests. If you are a contributor or interested in compiler design, the **[Parsing Performance](./parsing-performance.md)** and **[Expression Generation](./expression-generation.md)** benchmarks provide deep insights into our pipeline optimizations. diff --git a/docs/guide/performance/benchmarks.md b/docs/guide/performance/benchmarks.md index 92f7e5a..f058b9a 100644 --- a/docs/guide/performance/benchmarks.md +++ b/docs/guide/performance/benchmarks.md @@ -11,7 +11,7 @@ We measure every stage of the query pipeline separately and publish the full met ## Documentation Structure 1. **[Methodology](./methodology.md)** — Environment, hardware, dataset, reproducibility -2. **[Execution Benchmarks](../execution.md)** — Full query pipeline (filter, sort, page, projection, nested) +2. **[Execution Benchmarks](../execution-pipeline)** — Full query pipeline (filter, sort, page, projection, nested) 3. **[Parsing Performance](./parsing-performance.md)** — String → Abstract Syntax Tree (AST) conversion cost 4. **[Expression Generation](./expression-generation.md)** — AST → LINQ Expression translation 5. **[Database Execution](./database-execution.md)** — SQL Server LocalDB results @@ -29,9 +29,9 @@ If you want the bottom line without reading all pages: | Scenario | Expectation | Where to Look | |:----------|:-------------|:--------------| -| **Simple filter + sort + page (20 items)** | FlexQuery matches or exceeds handwritten LINQ; 1–2 ms total latency per request | [Execution](../execution.md) | -| **Dynamic projection (select specific fields)** | FlexQuery adds ~20 µs overhead vs strongly-typed handwritten; allocation-efficient | [Execution: Projection](../execution.md#scenario-2-dynamic-projection-select-subset-of-fields) | -| **Nested collection queries (`Any`/`All`)** | Library overhead is negligible (< 2% of total time); database I/O dominates | [Execution: Nested](../execution.md#scenario-3-nested-collection-queries-any) | +| **Simple filter + sort + page (20 items)** | FlexQuery matches or exceeds handwritten LINQ; 1–2 ms total latency per request | [Execution](../execution-pipeline) | +| **Dynamic projection (select specific fields)** | FlexQuery adds ~20 µs overhead vs strongly-typed handwritten; allocation-efficient | [Execution: Projection](../execution-pipeline#scenario-2-dynamic-projection-select-subset-of-fields) | +| **Nested collection queries (`Any`/`All`)** | Library overhead is negligible (< 2% of total time); database I/O dominates | [Execution: Nested](../execution-pipeline#scenario-3-nested-collection-queries-any) | | **Large result sets (10,000+ rows)** | Memory scales linearly; consider streaming or paging to prevent OOM | [Scalability](./scalability.md) | | **SQL Server production** | FlexQuery adds ~5% overhead vs handwritten; still fastest among dynamic libraries | [Database Execution](./database-execution.md) | | **Full API request (including serialization)** | FlexQuery 0.55–1.00× relative to baseline depending on page size | [API Benchmarks](./api-benchmarks.md) | diff --git a/docs/guide/performance/database-execution.md b/docs/guide/performance/database-execution.md index 99c7fd9..6ada795 100644 --- a/docs/guide/performance/database-execution.md +++ b/docs/guide/performance/database-execution.md @@ -71,12 +71,12 @@ var data = await _db.Users.AsNoTracking() .ToListAsync(); // FlexQuery with count disabled (single query) -var options = QueryOptionsParser.Parse(new FlexQueryParameters +var options = new FlexQueryParameters { Filter = "status:eq:active", PageSize = 100, IncludeCount = false // match handwritten's single query -}); +}.ToQueryOptions(); var data = await _db.Users.AsNoTracking().FlexQueryAsync(options); ``` @@ -133,7 +133,7 @@ Represents the cost of parsing + expression generation + validation + execution ## Comparison to Other Libraries This benchmark does **not** include Gridify, Sieve, or OData because `DatabaseExecutionBenchmarks.cs` only measures FlexQuery vs handwritten. To see those libraries' end-to-end performance (including parsing overhead, but still in-memory), refer to: -- [Execution Benchmarks](../execution.md) (InMemory, includes Gridify, Sieve, Dynamic.Core) +- [Execution Benchmarks](../execution-pipeline) (InMemory, includes Gridify, Sieve, Dynamic.Core) - [API Benchmarks](./api-benchmarks.md) (Full HTTP pipeline includes all libraries) Those benchmarks consistently show FlexQuery.NET performing within 0–5% of handwritten LINQ for filter+sort+page operations, while Gridify and Sieve are typically 18–40% slower. @@ -144,6 +144,6 @@ Those benchmarks consistently show FlexQuery.NET performing within 0–5% of han The raw database execution numbers in this artifact are **not representative** of FlexQuery.NET's real overhead because they include a mandatory COUNT query that handwritten does not. The overhead is thus dominated by an extra roundtrip, not the library's parsing/generation cost. -For actual filtering overhead, see **[Execution Benchmarks](../execution.md)** where all libraries execute entirely in-memory with comparable operations. There, FlexQuery.NET is 2.25× faster than handwritten (InMemory anomaly) or within margin of error on SQL. +For actual filtering overhead, see **[Execution Benchmarks](../execution-pipeline)** where all libraries execute entirely in-memory with comparable operations. There, FlexQuery.NET is 2.25× faster than handwritten (InMemory anomaly) or within margin of error on SQL. Future benchmark updates will align both sides to execute the same number of queries. diff --git a/docs/guide/performance/expression-generation.md b/docs/guide/performance/expression-generation.md index 5a88955..77db3e8 100644 --- a/docs/guide/performance/expression-generation.md +++ b/docs/guide/performance/expression-generation.md @@ -61,7 +61,7 @@ Parse (0.1 µs) → Validate (1–10 µs) → Expression Generation (~0.9 µs) At **~1 µs**, expression generation is negligible compared to database execution. Even for 10,000 QPS, the total CPU time is < 0.01 seconds per second — essentially free. -However, for **in-memory LINQ-to-Objects** scenarios (no database), expression generation cost becomes more visible (as seen in [Execution Benchmarks](../execution.md) where FlexQuery and handwritten are within milliseconds). +However, for **in-memory LINQ-to-Objects** scenarios (no database), expression generation cost becomes more visible (as seen in [Execution Benchmarks](../execution-pipeline) where FlexQuery and handwritten are within milliseconds). --- diff --git a/docs/guide/performance/index.md b/docs/guide/performance/index.md index 0259265..e109dee 100644 --- a/docs/guide/performance/index.md +++ b/docs/guide/performance/index.md @@ -13,7 +13,7 @@ We measure every stage of the query pipeline separately and publish the full met 1. **[Methodology](./methodology.md)** — Environment, hardware, dataset, reproducibility 2. **[Parsing Performance](./parsing-performance.md)** — String → Abstract Syntax Tree (AST) conversion cost 3. **[Expression Generation](./expression-generation.md)** — AST → LINQ Expression translation -4. **[Execution Benchmarks](../execution.md)** — Full query pipeline (filter, sort, page, projection, nested) +4. **[Execution Benchmarks](../execution-pipeline)** — Full query pipeline (filter, sort, page, projection, nested) 5. **[Database Execution](./database-execution.md)** — SQL Server LocalDB results 6. **[API Benchmarks](./api-benchmarks.md)** — Full ASP.NET Core pipeline vs OData/GraphQL/Gridify/Sieve 7. **[Scalability](./scalability.md)** — Performance across 100 to 10,000 records @@ -28,9 +28,9 @@ If you want the bottom line without reading all pages: | Scenario | Expectation | Where to Look | |:----------|:-------------|:--------------| -| **Simple filter + sort + page (20 items)** | FlexQuery matches or exceeds handwritten LINQ; 1–2 ms total latency per request | [Execution](../execution.md) | -| **Dynamic projection (select specific fields)** | FlexQuery adds ~20 µs overhead vs strongly-typed handwritten; allocation-efficient | [Execution: Projection](../execution.md#scenario-2-dynamic-projection-select-subset-of-fields) | -| **Nested collection queries (`Any`/`All`)** | Library overhead is negligible (< 2% of total time); database I/O dominates | [Execution: Nested](../execution.md#scenario-3-nested-collection-queries-any) | +| **Simple filter + sort + page (20 items)** | FlexQuery matches or exceeds handwritten LINQ; 1–2 ms total latency per request | [Execution](../execution-pipeline) | +| **Dynamic projection (select specific fields)** | FlexQuery adds ~20 µs overhead vs strongly-typed handwritten; allocation-efficient | [Execution: Projection](../execution-pipeline#scenario-2-dynamic-projection-select-subset-of-fields) | +| **Nested collection queries (`Any`/`All`)** | Library overhead is negligible (< 2% of total time); database I/O dominates | [Execution: Nested](../execution-pipeline#scenario-3-nested-collection-queries-any) | | **Large result sets (10,000+ rows)** | Memory scales linearly; consider streaming or paging to prevent OOM | [Scalability](./scalability.md) | | **SQL Server production** | FlexQuery adds ~5% overhead vs handwritten; still fastest among dynamic libraries | [Database Execution](./database-execution.md) | | **Full API request (including serialization)** | FlexQuery 0.55–1.00× relative to baseline depending on page size | [API Benchmarks](./api-benchmarks.md) | diff --git a/docs/guide/performance/interpretation-guide.md b/docs/guide/performance/interpretation-guide.md index c0f68d1..7135da7 100644 --- a/docs/guide/performance/interpretation-guide.md +++ b/docs/guide/performance/interpretation-guide.md @@ -16,7 +16,7 @@ This guide helps you translate benchmark numbers into decisions for your project | **Nested collection filters (`Any`/`All`)** | FlexQuery.NET only | No overhead — database dominates | For detailed numbers, see: -- [Execution Benchmarks](../execution.md) +- [Execution Benchmarks](../execution-pipeline) - [API Benchmarks](./api-benchmarks.md) - [Database Execution](./database-execution.md) @@ -229,10 +229,10 @@ Benchmarks can only guide you if you know where your time goes. | Your Stack | Most Relevant Benchmark | |:------------|:-----------------------| | EF Core + SQL Server | [Database Execution](./database-execution.md) | -| EF Core + InMemory (tests) | [End-to-End Execution](../execution.md) | +| EF Core + InMemory (tests) | [End-to-End Execution](../execution-pipeline) | - ASP.NET Core REST API | [API Benchmarks](./api-benchmarks.md) | - Microservices with large payloads | [Scalability](./scalability.md) | -- Need nested `Any` / `All` | [Execution: Nested](../execution.md#scenario-3-nested-collection-queries-any) | +- Need nested `Any` / `All` | [Execution: Nested](../execution-pipeline#scenario-3-nested-collection-queries-any) | --- diff --git a/docs/guide/performance/memory-usage.md b/docs/guide/performance/memory-usage.md index 9b62e2b..8a8497e 100644 --- a/docs/guide/performance/memory-usage.md +++ b/docs/guide/performance/memory-usage.md @@ -23,7 +23,7 @@ Allocation directly impacts garbage collection (GC) frequency and pause times: ### Scenario 1: Filter + Sort + Page (20–100 records) -From [Execution Benchmarks](../execution.md): +From [Execution Benchmarks](../execution-pipeline): | Page Size | FlexQuery.NET | Handwritten LINQ | Gridify | Sieve | OData | |:----------:|--------------:|-----------------:|--------:|------:|------:| @@ -175,10 +175,9 @@ However, **total working set** still scales with result count because entity mat ### 1. Set Maximum Page Size ```csharp -services.AddFlexQuery(options => +FlexQueryCore.Configure(options => { options.MaxPageSize = 1000; // Prevent memory exhaustion - options.MaxRecords = 10000; // Absolute hard limit }); ``` @@ -209,11 +208,7 @@ Use `dotnet-counters` or Application Insights to track: ### 4. Enable Caching for Repeated Queries -```csharp -FlexQueryCacheSettings.EnableCache = true; -``` - -Cached queries skip parsing and expression generation, reducing per-request allocation by ~40–50% for repeated query shapes. +Expression caching is **enabled by default**. Cached queries skip parsing and expression generation, reducing per-request allocation by ~40–50% for repeated query shapes. Tune via `FlexQueryCacheSettings.MaxCacheSize` if needed. ### 5. Consider Compression for Large Payloads @@ -238,6 +233,6 @@ For responses > 100 KB: ## Related Pages - [Scalability](./scalability.md) — How performance scales with dataset size -- [Execution Benchmarks](../execution.md) — Full pipeline including allocation numbers +- [Execution Benchmarks](../execution-pipeline) — Full pipeline including allocation numbers - [API Benchmarks](./api-benchmarks.md) — Real-world HTTP request memory usage - [Database Execution](./database-execution.md) — SQL Server memory characteristics diff --git a/docs/guide/performance/parsing-performance.md b/docs/guide/performance/parsing-performance.md index cadd140..5ea78b5 100644 --- a/docs/guide/performance/parsing-performance.md +++ b/docs/guide/performance/parsing-performance.md @@ -89,14 +89,14 @@ Gridify and Sieve are excluded here because they do not parse sort/pagination/se ## DSL Format Comparison (FlexQuery.NET Only) -FlexQuery.NET supports two textual formats: **DSL** (colon-delimited) and **JQL** (SQL-like). +FlexQuery.NET supports two textual formats: **DSL** (colon-delimited) and **FQL** (SQL-like). | Format | Example | Mean | Allocated | Relative | |:-------|---------|-----:|----------:|---------:| | **DSL** | `status:eq:active` | 313.9 ns | 784 B | 1.00× | -| **JQL** | `status = "active"` | 566.0 ns | 1,440 B | 1.80× | +| **FQL** | `status = "active"` | 566.0 ns | 1,440 B | 1.80× | -JQL parsing is ~1.8× slower due to more complex tokenization (handling quotes, whitespace, operator precedence). This is expected and still sub-microsecond. +FQL parsing is ~1.8× slower due to more complex tokenization (handling quotes, whitespace, operator precedence). This is expected and still sub-microsecond. --- @@ -134,4 +134,4 @@ Unless your workload consists of: ...then parsing overhead is unlikely to be your bottleneck. -**Focus on the [End-to-End Execution Benchmarks](../execution.md) instead.** Those reflect actual HTTP request latency. +**Focus on the [End-to-End Execution Benchmarks](../execution-pipeline) instead.** Those reflect actual HTTP request latency. diff --git a/docs/guide/projection.md b/docs/guide/projection.md index 94fb896..3e14427 100644 --- a/docs/guide/projection.md +++ b/docs/guide/projection.md @@ -139,7 +139,7 @@ var data = await projected.ToListAsync(); ### Restricting Selectable Fields ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.SelectableFields = new HashSet { @@ -214,7 +214,7 @@ GET /api/users?select=id,name,address.city&mode=nested ```csharp // WRONG — client could select passwordHash, internalNotes, etc. -var result = await _context.Users.FlexQueryAsync(parameters); +var result = await _context.Users.FlexQueryAsync(parameters); ``` ### ❌ Requesting non-existent fields diff --git a/docs/guide/query-composition.md b/docs/guide/query-composition.md index 46f6c33..d056b83 100644 --- a/docs/guide/query-composition.md +++ b/docs/guide/query-composition.md @@ -49,6 +49,7 @@ For internal logic, you can construct a `QueryOptions` object manually. This pro ```csharp using FlexQuery.NET.Models; using FlexQuery.NET.Constants; +using FlexQuery.NET.Builders; var options = new QueryOptions { @@ -92,14 +93,11 @@ var options = new QueryOptions }; // Apply directly to any IQueryable -var results = await query - .Apply(options) +var results = await QueryBuilder + .Apply(query, options) .ToListAsync(); ``` -> [!IMPORTANT] -> In FlexQuery.NET v2, the sorting model was renamed to **`SortNode`** to align with the AST-based architecture. - --- ## Nested Query Groups @@ -149,7 +147,7 @@ One of the most powerful features of FlexQuery.NET is the ability to **augment** public async Task Get([FromQuery] FlexQueryParameters parameters) { // 1. Parse the user's query from the URL - var options = QueryOptionsParser.Parse(parameters); + var options = parameters.ToQueryOptions(); // 2. Inject a mandatory tenant restriction options.Filter ??= new FilterGroup { Logic = LogicOperator.And }; diff --git a/docs/guide/query-formats.md b/docs/guide/query-formats.md index 9777905..4a25e31 100644 --- a/docs/guide/query-formats.md +++ b/docs/guide/query-formats.md @@ -1,16 +1,16 @@ # Query Formats Comparison -FlexQuery supports five query formats on the same endpoint simultaneously. This guide helps you understand when and why to choose each one. +FlexQuery supports three query formats on the same endpoint simultaneously. This guide helps you understand when and why to choose each one. ## The Formats at a Glance -| | NativeDsl | JSON | JQL | MiniOData | Generic (Indexed) | -|--|-----------|------|-----|-----------|-------------------| -| **Verbosity** | Compact | Verbose | Medium | Medium | Very verbose | -| **URL-safe** | ✅ | Needs encoding | ⚠️ | ✅ | ✅ | -| **Nested logic** | Limited | ✅ Full | ✅ Full | ✅ Full | ❌ Flat only | -| **Human-readable** | ⚠️ | ❌ | ✅ | ✅ | ❌ | -| **Best for** | Internal tools | SPAs / JSON APIs | Dev tools / Admin | OData migration | HTML forms | +| | NativeDsl | FQL | MiniOData | +|--|-----------|-----|-----------| +| **Verbosity** | Compact | Medium | Medium | +| **URL-safe** | ✅ | ⚠️ | ✅ | +| **Nested logic** | Limited | ✅ Full | ✅ Full | +| **Human-readable** | ⚠️ | ✅ | ✅ | +| **Best for** | Internal tools | Dev tools / Admin | OData migration | --- @@ -24,31 +24,7 @@ FlexQuery supports five query formats on the same endpoint simultaneously. This ``` **Limitation:** Nested AND/OR groups are difficult to express. Stick to simple conditions. -### JSON -``` -?filter={"logic":"and","filters":[ - {"field":"Status","operator":"eq","value":"Active"}, - {"field":"Price","operator":"gt","value":50} -]} -``` -Full support for nested groups with mixed `and`/`or` logic: -```json -{ - "logic": "or", - "groups": [ - { - "logic": "and", - "filters": [ - { "field": "Category", "operator": "eq", "value": "Electronics" }, - { "field": "Price", "operator": "lt", "value": 1000 } - ] - }, - { "field": "IsFeatured", "operator": "eq", "value": true } - ] -} -``` - -### JQL +### FQL ``` ?filter=Status = 'Active' AND Price > 50 ?filter=(Category = 'Electronics' AND Price < 1000) OR IsFeatured = true @@ -63,18 +39,11 @@ Most human-readable. Ideal for developer tools, admin panels, and internal APIs. ?$filter=(Category eq 'Electronics' and Price lt 1000) or IsFeatured eq true ``` -### Generic (Indexed) -``` -?filter[0].field=Status&filter[0].operator=eq&filter[0].value=Active -?filter[1].field=Price&filter[1].operator=gt&filter[1].value=50 -``` -No nested logic support. All conditions are implicitly ANDed together. - --- ## Sorting Examples -### NativeDsl & JQL +### NativeDsl & FQL ``` ?sort=Name:asc,CreatedAt:desc ?sort=Price:asc @@ -85,12 +54,6 @@ No nested logic support. All conditions are implicitly ANDed together. ?$orderby=Name asc,CreatedAt desc ``` -### Generic -``` -?sort[0].field=Name&sort[0].dir=asc -?sort[1].field=CreatedAt&sort[1].dir=desc -``` - ### JSON (via `sort` parameter) ``` ?sort=Name:asc,CreatedAt:desc @@ -123,7 +86,7 @@ Combined with aggregates (always expressed as query parameters, not inside the f ?include=Orders,Profile ``` -### Filtered includes (JQL inside parentheses) +### Filtered includes (FQL inside parentheses) ``` ?include=Orders(Status = 'Active') ?include=Orders(Status = 'Active').OrderItems(Quantity > 5) @@ -133,18 +96,13 @@ Combined with aggregates (always expressed as query parameters, not inside the f ``` ?$expand=Orders ``` -MiniOData does not support filtered includes — use the standard `include` parameter with JQL inline filters for that feature. +MiniOData does not support filtered includes — use the standard `include` parameter with FQL inline filters for that feature. --- ## When to Use Each Format -### Use **JSON** when: -- Your frontend sends filter payloads from a query builder UI (e.g., React Query Builder) -- You need deeply nested AND/OR logic -- The filter is constructed programmatically, not typed by a user - -### Use **JQL** when: +### Use **FQL** when: - You're building admin panels or internal tools where developers write filters - You want queries that are readable in logs and debugging tools - Your API is consumed by developers who understand query languages @@ -159,20 +117,13 @@ MiniOData does not support filtered includes — use the standard `include` para - You are migrating from `Microsoft.AspNetCore.OData` - You need backwards compatibility without rewriting client code -### Use **Generic (Indexed)** when: -- Your forms serialize query parameters using `filter[0].field` notation -- You use a form library that produces indexed query strings automatically - ---- - ## Common Mistakes | Mistake | Problem | Fix | |---------|---------|-----| | Sending JSON filter without encoding it | URL parser breaks the JSON | URL-encode the JSON value or use a request body | | Mixing `$filter` and `filter` in the same request | Parser detects `$` prefix and routes to MiniOData, ignoring `filter` | Pick one convention per request | -| Using Indexed format for complex logic | It only supports flat AND — no OR groups | Switch to JSON or JQL for multi-group logic | -| Assuming all formats support the same operators | NativeDsl has a smaller operator set than JQL/JSON | Check the [Operators reference](/shared/operators) | +| Assuming all formats support the same operators | NativeDsl has a smaller operator set than FQL/JSON | Check the [Operators reference](/shared/operators) | ## Related Features diff --git a/docs/guide/query-models.md b/docs/guide/query-models.md index 7ef95d7..3f6aa32 100644 --- a/docs/guide/query-models.md +++ b/docs/guide/query-models.md @@ -2,20 +2,20 @@ FlexQuery.NET provides two primary request models for handling dynamic queries from your API. While both models use the same underlying engine, they are designed for different levels of complexity and API surface area. -## QueryRequest (Full Feature) +## FlexQueryRequest (Full Feature) -`QueryRequest` is the comprehensive model that exposes every capability the library offers. Use this when your consumers need full control over the query pipeline. +`FlexQueryRequest` is the comprehensive model that exposes every capability the library offers. Use this when your consumers need full control over the query pipeline. - **Capabilities**: - - `Filter`, `Sort`, `Select`, `Includes` - - `GroupBy`, `Having`, `Join` + - `Filter`, `Sort`, `Select`, `Include` + - `GroupBy`, `Having`, `Aggregate` - `Mode` (Projection control: Nested, Flat, etc.) - - `Query` (Full JQL support) + - `Expand` (Deep, filtered navigation expansion) - `IncludeCount`, `Distinct` - **When to use**: - Advanced APIs requiring deep data shaping. - Internal administrative systems. - - Scenarios where SQL-like JOINs and aggregations are required from the client. + - Scenarios where aggregations and complex projections are required from the client. ## FlexQueryParameters (Simplified) @@ -33,7 +33,7 @@ FlexQuery.NET provides two primary request models for handling dynamic queries f As a general rule: 1. **Start with `FlexQueryParameters`**. It covers 90% of standard API filtering and paging needs. -2. **Upgrade to `QueryRequest`** only when you specifically need features like explicit Joins, Grouping, or complex Projection modes. +2. **Upgrade to `FlexQueryRequest`** only when you specifically need features like explicit Grouping, Aggregates, or complex Projection modes. ## Key Note @@ -43,23 +43,22 @@ Both models map to the same internal `QueryOptions` engine. Using the simplified ### Simple Usage (Recommended) ```csharp -using FlexQuery.NET.Parser; +using FlexQuery.NET.Models; [HttpGet] -public async Task Get([FromQuery] FlexQueryParameters request) +public async Task Get([FromQuery] FlexQueryParameters parameters) { - var options = QueryOptionsParser.Parse(request); - return Ok(await _context.Products.ToQueryResultAsync(options)); + var options = parameters.ToQueryOptions(); + return Ok(await _context.Products.FlexQueryAsync(options)); } ``` ### Advanced Usage ```csharp -using FlexQuery.NET.Parser; [HttpGet] -public async Task Search([FromQuery] QueryRequest request) +public async Task Search([FromQuery] FlexQueryParameters parameters) { - var options = QueryOptionsParser.Parse(request); - // Allows client to specify JOINs, GroupBy, and Having - return Ok(await _context.Orders.ToQueryResultAsync(options)); + var options = parameters.ToQueryOptions(); + // Allows client to specify includes, GroupBy, and Having + return Ok(await _context.Orders.FlexQueryAsync(options)); } ``` diff --git a/docs/guide/query-syntax.md b/docs/guide/query-syntax.md index 591ed2d..c59202a 100644 --- a/docs/guide/query-syntax.md +++ b/docs/guide/query-syntax.md @@ -2,175 +2,143 @@ ## Overview -FlexQuery supports multiple query syntax dialects, allowing clients to express filters, sorts, and projections in the format most natural to their stack. Whether your frontend team prefers JSON payloads, your legacy clients speak OData, or your internal tools use a compact DSL — FlexQuery handles all of them through a unified parser pipeline. +FlexQuery.NET supports multiple query syntax dialects, allowing clients to express filters, sorts, and projections in the format most natural to their stack. All syntaxes are parsed into a canonical `QueryOptions` AST so downstream processing is format-independent. ### What It Is -The `QuerySyntax` system is a pluggable parser architecture that accepts raw query input in any supported format and converts it into a canonical `QueryOptions` abstract syntax tree (AST). All downstream processing (validation, expression building, SQL generation) operates on the same `QueryOptions` regardless of the input format. +The `QuerySyntax` enum and parser registration system enable pluggable parsing. The active syntax is configured globally via `FlexQueryCore.Configure()` and is applied automatically when `FlexQueryParameters` is parsed via `ToQueryOptions()`. ### Why It Exists -Real-world APIs serve diverse clients. A React dashboard might send JSON filter objects, a legacy .NET client might use OData-style `$filter` strings, and a mobile app might use a compact query language. Without syntax abstraction, you would need separate endpoints or custom parsing logic for each client type. +Real-world APIs serve diverse clients. A React dashboard might use compact DSL strings, while a legacy enterprise client might speak OData-style `$filter` syntax. Without syntax abstraction, you would need separate endpoints or custom parsing logic for each client type. ### When to Use It - You have clients that use different query formats -- You are migrating from OData or another query framework and need backwards compatibility +- You are migrating from OData and need backwards compatibility - You want to explicitly control which parser handles a request - You are building a custom parser for a proprietary format ### When NOT to Use It -- If all your clients use the same format, you can rely on `AutoDetect` (the default) and never think about it +- If all your clients use the same format, the default DSL syntax works without any configuration - If you are only passing pre-built `QueryOptions` objects programmatically (no string parsing needed) +--- + ## Architecture ``` -HTTP Request - │ - ▼ +FlexQueryCore.Configure() ← Sets global QuerySyntax + │ + ▼ FlexQueryParameters ← Raw string properties (Filter, Sort, Select, etc.) - │ - ▼ -QueryOptionsParser.Parse() ← Entry point - │ - ├── QuerySyntax specified? ──Yes──► Use that parser directly - │ - └── AutoDetect ──► Walk registered parsers - │ - ├── Has indexed keys (filter[0].field)? ──► Generic/Indexed parser - ├── parser.CanParse() returns true? ──► Use that parser - └── No match? ──► Fall back to last parser - │ - ▼ - QueryOptions ← Canonical AST used by all downstream processing + │ + ▼ + ToQueryOptions() ← Uses the configured QuerySyntax parser + │ + ▼ + QueryOptions ← Canonical AST used by all downstream processing ``` ### Parser Registration -Parsers are registered in a prioritized list. New parsers registered via `RegisterParser()` are inserted at the **front** of the list, giving them first-match priority: +Syntax parsers are registered explicitly at startup. Built-in parsers are part of the Core package. Optional parsers (FQL, MiniOData) require separate packages: ```csharp -// Built-in parsers (registered by default): -// 1. JqlQueryParser -// 2. JsonQueryParser -// 3. DslQueryParser - -// MiniODataQueryParser is auto-registered if the assembly is loaded -// Custom parsers can be registered manually: -QueryOptionsParser.RegisterParser(new MyCustomParser()); +// Native DSL — built in, no registration needed (default) +FlexQueryCore.Configure(options => +{ + options.QuerySyntax = QuerySyntax.NativeDsl; +}); + +// FQL — requires FlexQuery.NET.Parsers.Fql package +Fql.Register(); +FlexQueryCore.Configure(options => +{ + options.QuerySyntax = QuerySyntax.Fql; +}); + +// MiniOData — requires FlexQuery.NET.Parsers.MiniOData package +MiniOData.Register(); +FlexQueryCore.Configure(options => +{ + options.QuerySyntax = QuerySyntax.MiniOData; +}); ``` +--- + ## Supported Syntax Types -### NativeDsl — Colon-Separated Syntax +### NativeDsl — Colon-Separated Syntax (Default) The most compact format. Fields, operators, and values are separated by colons: ``` -filter=name:eq:john -filter=age:gt:18 -filter=status:in:active,inactive +GET /api/products?filter=ListPrice:gte:1000&sort=Name:asc +GET /api/products?filter=ProductCategory.Name:eq:Bikes +GET /api/customers?filter=LastName:startswith:Smi&select=Id,LastName,Email ``` -**Best for:** Internal tools, compact query strings, URL-friendly APIs. - -### Json — Structured JSON Payloads - -Sends complex, nested filter trees as JSON objects: +**Compound (AND — URL-encode `&` as `%26`):** -```json -{ - "logic": "and", - "filters": [ - { "field": "Age", "operator": "gt", "value": 18 }, - { "field": "Status", "operator": "eq", "value": "Active" } - ] -} +``` +GET /api/products?filter=ListPrice:gte:1000%26ProductCategory.Name:eq:Bikes ``` -**Best for:** SPAs, complex multi-condition filters, programmatic query building. +**Best for:** Internal tools, compact query strings, URL-friendly APIs. -### Jql — JQL-like Expression Strings +### FQL — SQL-Like Expression Strings -Human-readable expressions using standard operators: +Requires the `FlexQuery.NET.Parsers.Fql` package and `Fql.Register()` at startup. Human-readable expressions using standard operators: ``` -filter=Age > 18 AND Status = 'Active' -filter=Name CONTAINS 'john' OR Email ENDSWITH '@acme.com' +GET /api/products?filter=ListPrice >= 1000 AND Category.Name = 'Bikes' +GET /api/customers?filter=LastName CONTAINS 'Smi' OR Email ENDSWITH '@adventure-works.com' ``` **Best for:** Developer tools, debugging, admin panels, human-written queries. ### MiniOData — OData Compatibility -Standard OData `$filter` and `$orderby` syntax (requires `FlexQuery.NET.Parsers.MiniOData` package): +Requires the `FlexQuery.NET.Parsers.MiniOData` package and `MiniOData.Register()` at startup. Standard OData `$filter` and `$orderby` syntax: ``` -$filter=Age gt 18 and Status eq 'Active' -$orderby=Name desc -$select=Id,Name,Email -$expand=Orders +GET /api/products?$filter=ListPrice ge 1000 and Category/Name eq 'Bikes' +GET /api/products?$orderby=Name desc&$select=Id,Name,ListPrice&$expand=ProductCategory ``` **Best for:** Migrating from OData, enterprise clients that already speak OData. -### Generic — Indexed Query Strings - -Array-style query parameters common in form-based UIs: - -``` -filter[0].field=name&filter[0].operator=eq&filter[0].value=john -sort[0].field=age&sort[0].dir=desc -``` - -**Best for:** HTML forms, auto-generated query strings, framework integrations. +--- ## Syntax Comparison Table -| Feature | NativeDsl | Json | Jql | MiniOData | Generic | -|---------|-----------|------|-----|-----------|---------| -| **Format** | `field:op:value` | JSON object | `Field OP Value` | `$filter=Field op Value` | `filter[0].field=X` | -| **Nested logic** | Limited | ✅ Full | ✅ Full | ✅ Full | ❌ Flat only | -| **URL-friendly** | ✅ Very | ❌ Needs body | ⚠️ Needs encoding | ✅ Good | ✅ Very | -| **Human-readable** | ⚠️ Medium | ❌ Low | ✅ High | ✅ High | ❌ Low | -| **Auto-detected by** | Colon separator | `{` prefix | Operator keywords | `$` key prefix | `[0]` index keys | -| **Package required** | Core | Core | Core | MiniOData | Core | +| Feature | NativeDsl | FQL | MiniOData | +|---------|-----------|-----|-----------| +| **Format** | `field:op:value` | `Field OP Value` | `$filter=Field op Value` | +| **Nested logic** | Limited | Full | Full | +| **URL-friendly** | Very | Needs encoding | Good | +| **Human-readable** | Medium | High | High | +| **Package required** | Core | `FlexQuery.NET.Parsers.Fql` | `FlexQuery.NET.Parsers.MiniOData` | +| **Registration** | None (default) | `Fql.Register()` | `MiniOData.Register()` | -## Parser Selection Flow +--- -The `QueryOptionsParser` selects a parser in this order: - -1. **Explicit syntax** — If `QuerySyntax` is set to anything other than `AutoDetect`, that parser is used directly -2. **Indexed detection** — If the raw parameters contain indexed keys like `filter[0].field`, the Generic parser handles it -3. **CanParse probe** — Each registered parser's `CanParse()` method is called. The first one to return `true` wins -4. **Fallback** — If no parser claims the input, the last registered parser handles it - -### Auto-Detection Rules - -| Parser | Detects When | -|--------|-------------| -| **MiniOData** | Any raw parameter key starts with `$`, or filter contains OData operators (`eq`, `ne`, `contains(`) | -| **Json** | Filter string starts with `{` or `[` | -| **Jql** | Filter contains comparison operators (`=`, `>`, `<`, `!=`, `CONTAINS`, `AND`, `OR`) | -| **NativeDsl** | Filter contains colon-separated segments | -| **Generic** | Raw parameters have indexed keys like `filter[0]` | - -## Real-World Example: Multi-Client API - -A single endpoint that accepts requests from different client types: +## Example: Single Endpoint with Explicit Syntax ```csharp [HttpGet("products")] public async Task GetProducts( [FromQuery] FlexQueryParameters parameters) { - // No need to know which syntax the client used! - // AutoDetect handles JSON, JQL, DSL, and OData transparently. var result = await _context.Products.FlexQueryAsync(parameters, opts => { - opts.AllowedFields = new HashSet { "Id", "Name", "Price", "Category" }; + opts.AllowedFields = new HashSet + { + "Id", "Name", "ListPrice", "ProductCategory" + }; opts.MaxPageSize = 100; }); @@ -178,72 +146,60 @@ public async Task GetProducts( } ``` -This single endpoint handles all of these requests: -``` -# JQL -GET /products?filter=Price > 50 AND Category = 'Electronics' - -# NativeDsl -GET /products?filter=Price:gt:50 +This endpoint handles the syntax configured at startup: -# MiniOData -GET /products?$filter=Price gt 50 and Category eq 'Electronics' - -# JSON (via POST or encoded) -POST /products/query -{ "logic": "and", "filters": [{ "field": "Price", "operator": "gt", "value": 50 }] } ``` +# NativeDsl (default) +GET /products?filter=ListPrice:gte:1000&sort=Name:asc -## Forcing a Specific Syntax - -If you need to bypass auto-detection: +# FQL (if registered) +GET /products?filter=ListPrice >= 1000 AND ProductCategory.Name = 'Bikes' -```csharp -var options = QueryOptionsParser.Parse(parameters, QuerySyntax.Json); -// Forces JSON parsing regardless of input format +# MiniOData (if registered) +GET /products?$filter=ListPrice ge 1000 and ProductCategory/Name eq 'Bikes' ``` -## Building a Custom Parser +--- + +## Forcing a Specific Syntax Per-Request -Implement `IQueryParser` to add support for a proprietary format: +To override the global syntax for a single request, pass `QueryOptions` directly instead of `FlexQueryParameters`: ```csharp -public class GraphQLFilterParser : IQueryParser +[HttpPost("products/query")] +public async Task QueryProducts( + [FromBody] MiniODataRequest request) { - public QuerySyntax Syntax => QuerySyntax.AutoDetect; // or a custom enum value - - public bool CanParse(FlexQueryParameters parameters) + var options = request.ToQueryOptions(); + // options is now parsed using MiniOData syntax + var result = await _context.Products.FlexQueryAsync(options, opts => { - return parameters.Filter?.Contains("{ query") == true; - } + opts.AllowedFields = new HashSet { "Id", "Name", "ListPrice" }; + }); - public QueryOptions Parse(FlexQueryParameters parameters) - { - // Parse your custom format into QueryOptions - return new QueryOptions { /* ... */ }; - } + return Ok(result); } - -// Register at startup -QueryOptionsParser.RegisterParser(new GraphQLFilterParser()); ``` +--- + ## Best Practices -1. **Use AutoDetect** unless you have a specific reason to force a syntax — it handles mixed-client scenarios gracefully -2. **Validate the output, not the syntax** — Apply `AllowedFields`, `MaxFieldDepth`, and `StrictFieldValidation` on the parsed `QueryOptions`, not on the raw input -3. **Standardize internally** — If all your clients are under your control, pick one syntax (typically JSON or JQL) and document it -4. **Use MiniOData for migration only** — It exists for backwards compatibility. For new APIs, prefer JSON or JQL -5. **Cache is syntax-aware** — The parser cache uses the syntax type as part of the cache key, so the same filter string parsed as JQL vs DSL produces different cache entries +1. **Pick one syntax and standardize** — If all clients are under your control, pick DSL or FQL and document it. +2. **Use MiniOData for migration only** — It exists for backwards compatibility. For new APIs, prefer DSL or FQL. +3. **Validate the output, not the syntax** — Apply `AllowedFields`, `MaxFieldDepth`, and `StrictFieldValidation` on the parsed `QueryOptions`, not on the raw input. +4. **Register parsers explicitly** — Even though some parsers auto-register on assembly load, explicit registration makes the configuration clear. + +--- ## Common Pitfalls | Pitfall | Solution | |---------|----------| -| Sending JSON in a query string without encoding | URL-encode the JSON, or use a POST body | -| Mixing OData and FlexQuery parameters in the same request | The parser detects `$` prefix and routes to MiniOData. Don't mix `$filter` with `filter` | -| Assuming case sensitivity | All operators are case-insensitive. `EQ`, `eq`, and `Eq` are all valid | -| Registering a custom parser but it never gets selected | Check your `CanParse()` logic. Custom parsers are inserted first but must return `true` for their inputs | +| Sending DSL filter with FQL syntax configured | The parser fails with a `QueryParseException`. Ensure `QuerySyntax` matches the expected input format. | +| Mixing OData `$filter` with DSL `filter` parameter | Use MiniOData syntax exclusively when parsing OData-style requests. Don't mix `$filter` with `filter`. | +| Forgetting to register a parser | FQL and MiniOData must be explicitly registered via `Fql.Register()` / `MiniOData.Register()` before use. | +| Assuming case sensitivity | All operators are case-insensitive. `EQ`, `eq`, and `Eq` are all valid. | ## Related Features diff --git a/docs/guide/security-governance.md b/docs/guide/security-governance.md index 446d472..76ee41d 100644 --- a/docs/guide/security-governance.md +++ b/docs/guide/security-governance.md @@ -106,13 +106,12 @@ opts.StrictFieldValidation = false; // Silently removes unauthorized fields ## FlexQueryOptions (Global Defaults) ```csharp -builder.Services.AddFlexQuery(opts => +FlexQueryCore.Configure(opts => { opts.MaxPageSize = 1000; opts.DefaultPageSize = 20; opts.MaxFieldDepth = 5; opts.StrictFieldValidation = true; - opts.UseNoTracking = true; }); ``` diff --git a/docs/guide/security.md b/docs/guide/security.md index f90fb82..34cc1e1 100644 --- a/docs/guide/security.md +++ b/docs/guide/security.md @@ -6,35 +6,32 @@ FlexQuery.NET has a multi-layered field security system. It validates every fiel ## Overview -The security model works at the `QueryExecutionOptions` level for endpoint-specific rules. Global defaults are configured via `FlexQueryOptions` in DI. +The security model works at the `BaseQueryOptions` level for endpoint-specific rules. Global defaults are configured via `FlexQueryCore.Configure()`. ### Two-Tier Configuration -**Global Configuration** (`FlexQueryOptions` - DI): +**Global Configuration** (`FlexQueryCore.Configure()`): - MaxPageSize - DefaultPageSize - CaseInsensitive - IncludeTotalCount - StrictFieldValidation - MaxFieldDepth -- UseNoTracking -**Per-Request Configuration** (`QueryExecutionOptions`): +**Per-Request Configuration** (`BaseQueryOptions`): - AllowedFields, BlockedFields - FilterableFields, SortableFields, SelectableFields - AllowedOperators -- FieldAccessResolver - RoleAllowedFields, CurrentRole - MaxFieldDepth (override), StrictFieldValidation (override) - MaxPageSize (override), IncludeTotalCount (override) -### QueryExecutionOptions (v2) - Server-Owned -`QueryExecutionOptions` represents: +### BaseQueryOptions - Server-Owned +`BaseQueryOptions` represents: * field-level security lists (per-request only) * operator restrictions * MaxFieldDepth override (nullable) * Role-based access -* Custom resolver These are SERVER policies and should **never** be bound directly from HTTP requests. @@ -51,12 +48,11 @@ These are SERVER policies and should **never** be bound directly from HTTP reque * allowed operators * max depth * execution strategy -* split query behavior **Validation order:** 1. Depth check (`MaxFieldDepth`) -2. Custom resolver (`FieldAccessResolver`) +2. Blocked fields (`BlockedFields`) 3. Blocked fields (`BlockedFields`) 4. Role-based access (`RoleAllowedFields`) 5. Operation-level rules (`FilterableFields`, `SortableFields`, `SelectableFields`) @@ -69,24 +65,20 @@ These are SERVER policies and should **never** be bound directly from HTTP reque | Property | Type | Location | Description | | :--- | :--- | :--- | :--- | -| `AllowedFields` | `HashSet?` | QueryExecutionOptions | Per-endpoint allow-list. If set, every field must be in this list. | -| `AllowedIncludes` | `HashSet?` | QueryExecutionOptions | Per-endpoint allow-list for navigation properties and includes. | -| `AllowedOperators` | `Dictionary>?` | QueryExecutionOptions | Per-field operator restrictions. | -| `BlockedFields` | `HashSet?` | QueryExecutionOptions | Fields explicitly denied — always checked. | -| `FilterableFields` | `HashSet?` | QueryExecutionOptions | Fields allowed in filter expressions only. | -| `SortableFields` | `HashSet?` | QueryExecutionOptions | Fields allowed in sort expressions only. | -| `SelectableFields` | `HashSet?` | QueryExecutionOptions | Fields allowed in select/projection only. | -| `MaxFieldDepth` | `int?` | FlexQueryOptions (global) or QueryExecutionOptions (override) | Maximum dot-notation path depth. | -| `StrictFieldValidation` | `bool?` | FlexQueryOptions (global) or QueryExecutionOptions (override) | Fail-fast on first violation. | -| `UseNoTracking` | `bool?` | FlexQueryOptions (global) or QueryExecutionOptions (override) | EF Core no-tracking behavior. | -| `MaxPageSize` | `int?` | FlexQueryOptions (global) or QueryExecutionOptions (override) | Maximum allowed page size. | -| `IncludeTotalCount` | `bool?` | FlexQueryOptions (global) or QueryExecutionOptions (override) | Include total count by default. | -| `RoleAllowedFields` | `Dictionary>?` | QueryExecutionOptions | Per-role field allow-lists. | -| `CurrentRole` | `string?` | QueryExecutionOptions | The current user's role name. | -| `FieldMappings` | `Dictionary?` | QueryExecutionOptions | Alias → real field name mappings. | -| `FieldAccessResolver` | `IFieldAccessResolver?` | QueryExecutionOptions | Custom programmatic resolver. | - ---- +| `AllowedFields` | `HashSet?` | BaseQueryOptions | Per-endpoint allow-list. If set, every field must be in this list. | +| `AllowedIncludes` | `HashSet?` | BaseQueryOptions | Per-endpoint allow-list for navigation properties and includes. | +| `AllowedOperators` | `Dictionary>?` | BaseQueryOptions | Per-field operator restrictions. | +| `BlockedFields` | `HashSet?` | BaseQueryOptions | Fields explicitly denied — always checked. | +| `FilterableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in filter expressions only. | +| `SortableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in sort expressions only. | +| `SelectableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in select/projection only. | +| `MaxFieldDepth` | `int?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Maximum dot-notation path depth. | +| `StrictFieldValidation` | `bool?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Fail-fast on first violation. | +| `MaxPageSize` | `int?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Maximum allowed page size. | +| `IncludeTotalCount` | `bool?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Include total count by default. | +| `RoleAllowedFields` | `Dictionary>?` | BaseQueryOptions | Per-role field allow-lists. | +| `CurrentRole` | `string?` | BaseQueryOptions | The current user's role name. | +| `FieldMappings` | `Dictionary?` | BaseQueryOptions | Alias → real field name mappings. | ## AllowedFields @@ -251,7 +243,7 @@ Best for simple, single-endpoint rules. [HttpGet] public async Task GetUsers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "id", "name", "email" }; }); @@ -281,9 +273,16 @@ Best for standardizing rules across a controller. public async Task GetUsers( [FromQuery] FlexQueryParameters parameters) { - // Execution options are resolved automatically from HttpContext - // via the FieldAccess filter. - var result = await _context.Users.FlexQueryAsync(parameters, HttpContext); + // Retrieve execution options populated by the [FieldAccess] filter + var execOptions = HttpContext.GetFlexQueryExecutionOptions(); + + var result = await _context.Users.FlexQueryAsync(parameters, exec => + { + exec.AllowedFields = execOptions.AllowedFields; + exec.BlockedFields = execOptions.BlockedFields; + exec.FilterableFields = execOptions.FilterableFields; + exec.MaxFieldDepth = execOptions.MaxFieldDepth; + }); return Ok(result); } @@ -305,29 +304,15 @@ builder.Services.AddControllers(options => ### Option 3: Centralized Resolver -Best for dynamic, runtime-computed rules (e.g., based on tenant, claims, database config). - -```csharp -public class TenantFieldAccessResolver : IFieldAccessResolver -{ - private readonly ITenantService _tenants; - - public TenantFieldAccessResolver(ITenantService tenants) => _tenants = tenants; - - public bool IsAllowed(string field, QueryOperation operation, QueryContext context) - { - var tenantConfig = _tenants.GetConfig(context.TenantId); - return tenantConfig.AllowedFields.Contains(field); - } -} - -// Register -builder.Services.AddScoped(); -``` +Best for dynamic, runtime-computed rules (e.g., based on tenant, claims, database config). Use the `AllowedFieldsResolver` delegate to dynamically determine allowed fields at runtime: ```csharp // In controller -exec.FieldAccessResolver = _resolver; +exec.AllowedFieldsResolver = entityType => +{ + // Dynamically compute allowed fields based on the current context + return new HashSet { "Id", "Name", "Email" }; +}; ``` **Pros:** Fully dynamic, database-driven, multi-tenant capable. @@ -341,7 +326,7 @@ exec.FieldAccessResolver = _resolver; | :--- | :--- | :--- | :--- | :--- | | Inline in controller | Low | Fixed | ❌ | Simple, single endpoints | | `[FieldAccess]` attribute | Low | Fixed | ✅ | Standard controller-level rules | -| Centralized resolver | High | Dynamic | ✅ | Multi-tenant, role-driven, DB-driven | +| Centralized resolver | Medium | Dynamic | ✅ | Multi-tenant, role-driven, DB-driven | --- @@ -366,7 +351,7 @@ Handle it in your controller: ```csharp try { - var result = await _context.Users.FlexQueryAsync(parameters, exec => { ... }); + var result = await _context.Users.FlexQueryAsync(parameters, exec => { ... }); return Ok(result); } catch (QueryValidationException ex) @@ -378,7 +363,7 @@ catch (QueryValidationException ex) Or use the soft-validation approach: ```csharp -var options = QueryOptionsParser.Parse(parameters); +var options = parameters.ToQueryOptions(); var result = options.ValidateSafe(execOptions); if (!result.IsValid) diff --git a/docs/guide/sorting.md b/docs/guide/sorting.md index fbb6fa3..93abb9c 100644 --- a/docs/guide/sorting.md +++ b/docs/guide/sorting.md @@ -170,12 +170,12 @@ GET /api/users?sort=orders.count():desc ```csharp // WRONG — client could sort by passwordHash or internal fields -var result = await _context.Users.FlexQueryAsync(parameters); +var result = await _context.Users.FlexQueryAsync(parameters); ``` ```csharp // CORRECT -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.SortableFields = new HashSet { "name", "email", "createdAt" }; }); diff --git a/docs/guide/swagger-integration.md b/docs/guide/swagger-integration.md index 7e008d6..60ee79f 100644 --- a/docs/guide/swagger-integration.md +++ b/docs/guide/swagger-integration.md @@ -10,7 +10,7 @@ When you use `FlexQueryParameters` in your controller actions, ASP.NET Core and [HttpGet] public async Task Get([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, options => + var result = await _context.Users.FlexQueryAsync(parameters, options => { options.AllowedFields = ["Id", "Name", "Email"]; }); @@ -64,7 +64,7 @@ builder.Services.AddSwaggerGen(c => Once configured, the Swagger UI will show a rich interface for your dynamic endpoint: - **filter**: Shows examples for the DSL format (`field:operator:value`). -- **query**: Explains the JQL (Jira Query Language) syntax. +- **query**: Explains the FQL (Jira Query Language) syntax. - **select**: Lists available fields for dynamic projection. - **page/pageSize**: Clearly documented integer parameters for pagination. - **Default Values**: Swagger will correctly show the default page size (20) and other settings. diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 2f79118..c4ac818 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -43,11 +43,11 @@ if (!result.IsValid) ### With Global Configuration -When `FlexQueryOptions` is registered via DI, the validator automatically merges global defaults with per-request overrides: +Global defaults are configured once at startup via `FlexQueryCore.Configure`. The validator automatically merges global defaults with per-request overrides: ```csharp // In Program.cs - global defaults -builder.Services.AddFlexQuery(options => +FlexQueryCore.Configure(options => { options.MaxPageSize = 1000; options.MaxFieldDepth = 5; @@ -241,7 +241,7 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame MaxFieldDepth = 2 }; - var result = await _context.Users.FlexQueryAsync(parameters, execOptions); + var result = await _context.Users.FlexQueryAsync(parameters, execOptions); return Ok(result); } ``` @@ -252,7 +252,7 @@ Or with inline configuration: [HttpGet] public async Task GetUsers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "id", "name", "email", "status" }; exec.BlockedFields = new HashSet { "passwordHash" }; diff --git a/docs/guide/why-flexquery.md b/docs/guide/why-flexquery.md index 19b92cf..d7a3670 100644 --- a/docs/guide/why-flexquery.md +++ b/docs/guide/why-flexquery.md @@ -67,7 +67,7 @@ FlexQuery.NET replaces all of that with a **query abstraction layer**: [HttpGet("users")] public async Task GetUsers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { diff --git a/docs/index.md b/docs/index.md index f54902e..2e9a2b2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ hero: features: - icon: 🔍 title: Dynamic Filtering - details: Support for DSL, JQL, JSON, and indexed query-string formats. Nested AND/OR, collection predicates (any/all), and 20+ operators out of the box. + details: Support for DSL and FQL query-string formats. Nested AND/OR, collection predicates (any/all), and 20+ operators out of the box. - icon: ↕️ title: Sorting & Paging details: Multi-field sorting with aggregate sort support. Server-safe paging with automatic default ordering to prevent EF Core errors. @@ -73,7 +73,7 @@ GET /api/users?filter=status:eq:active&sort=createdAt:desc&page=1&pageSize=10&se [HttpGet] public async Task GetUsers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Users.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "id", "name", "email", "status", "createdAt" }; }); @@ -129,7 +129,7 @@ FlexQueryParameters (HTTP DTO) ├── ApplyFilter() ← WHERE clause (expression tree) ├── ApplySort() ← ORDER BY ├── ApplyPaging() ← SKIP / TAKE - ├── ApplyFilteredIncludes() ← Include pipeline (independent) + ├── ApplyExpand() ← Include pipeline (independent) └── ApplySelect() ← Dynamic projection │ ▼ @@ -150,9 +150,7 @@ FlexQuery.NET auto-detects the input format: | Format | Example | | :--- | :--- | | **DSL** | `filter=status:eq:active` | -| **JQL** | `query=status = "active" AND age >= 18` | -| **JSON** | `filter={"logic":"and","filters":[{"field":"status","operator":"eq","value":"active"}]}` | -| **Indexed** | `filter[0].field=status&filter[0].operator=eq&filter[0].value=active` | +| **FQL** | `query=status = "active" AND age >= 18` | --- @@ -161,7 +159,8 @@ FlexQuery.NET auto-detects the input format: | Package | Purpose | | :--- | :--- | | `FlexQuery.NET` | Core library — filtering, sorting, paging, projection, validation | -| `FlexQuery.NET.EntityFrameworkCore` | EF Core async execution — `FlexQueryAsync`, `ApplyFilteredIncludes` | +| `FlexQuery.NET.EntityFrameworkCore` | EF Core async execution — `FlexQueryAsync`, `ApplyExpand` | +| `FlexQuery.NET.Dapper` | Dapper async execution — `FlexQueryAsync` for raw ADO.NET connections | | `FlexQuery.NET.AspNetCore` | ASP.NET Core integration — `FieldAccessFilter`, `[FieldAccess]` attribute | --- @@ -179,7 +178,7 @@ Detailed documentation for each NuGet package: | `FlexQuery.NET.Diagnostics` | [README](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Diagnostics/README.md) | | `FlexQuery.NET.Adapters.AgGrid` | [README](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Adapters.AgGrid/README.md) | | `FlexQuery.NET.Adapters.Kendo` | [README](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Adapters.Kendo/README.md) | -| `FlexQuery.NET.Parsers.Jql` | [README](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.Jql/README.md) | +| `FlexQuery.NET.Parsers.FQL` | [README](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.FQL/README.md) | | `FlexQuery.NET.Parsers.MiniOData` | [README](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.MiniOData/README.md) | diff --git a/docs/migration/v2-to-v3.md b/docs/migration/v2-to-v3.md index 0595f1c..d380f7d 100644 --- a/docs/migration/v2-to-v3.md +++ b/docs/migration/v2-to-v3.md @@ -1,68 +1,68 @@ # Migration Guide: v2 → v3 -FlexQuery.NET v3.0.0 introduces a **modular, provider-agnostic architecture** along with new first-party integrations. The query engine is decoupled from Entity Framework, and JQL parsing has been extracted into an install-on-demand package. +FlexQuery.NET v3.0.0 introduces a **modular, provider-agnostic architecture** along with new first-party integrations. The query engine is decoupled from Entity Framework, and FQL parsing has been extracted into an install-on-demand package. --- ## 🔥 Breaking Changes Summary -### 1. JQL Parser Extracted to Separate Package +### 1. FQL Parser Extracted to Separate Package -The JQL parser has been **removed from FlexQuery.NET Core** and is now available as `FlexQuery.NET.Parsers.Jql`. +The FQL parser has been **removed from FlexQuery.NET Core** and is now available as `FlexQuery.NET.Parsers.FQL`. | Before (v2.x) | After (v3) | |:---|:---| -| JQL parser bundled in `FlexQuery.NET` | Must install `dotnet add package FlexQuery.NET.Parsers.Jql` | -| `QuerySyntax.Jql` enum value existed | Enum value removed — JQL is an external parser | -| `FilteredIncludeParser` fell back to JQL for inline filters | Inline include filters require **DSL syntax** only (`field:op:value`) | +| FQL parser bundled in `FlexQuery.NET` | Must install `dotnet add package FlexQuery.NET.Parsers.FQL` | +| `QuerySyntax.FQL` enum value existed | Enum value removed — FQL is an external parser | +| `FilteredIncludeParser` fell back to FQL for inline filters | Inline include filters require **DSL syntax** only (`field:op:value`) | -#### JQL Migration Steps +#### FQL Migration Steps ```bash -# Install the JQL package if you use query=... parameters -dotnet add package FlexQuery.NET.Parsers.Jql +# Install the FQL package if you use query=... parameters +dotnet add package FlexQuery.NET.Parsers.FQL ``` **If you used `query=` parameter in v2:** ```csharp -// Before (v2 — JQL bundled in Core) +// Before (v2 — FQL bundled in Core) var options = QueryOptionsParser.Parse(new Dictionary { ["query"] = "status = 'Active' AND amount > 1000" }); -// After (v3 — use JqlParser directly) -using FlexQuery.NET.Parsers.Jql; +// After (v3 — use FQLParser directly) +using FlexQuery.NET.Parsers.FQL; -var filter = new JqlParser().Parse("status = 'Active' AND amount > 1000"); +var filter = new FQLParser().Parse("status = 'Active' AND amount > 1000"); var options = new QueryOptions { Filter = filter }; ``` **If you used inline include filters:** ```csharp -// Before (v2 — JQL syntax was accepted) +// Before (v2 — FQL syntax was accepted) var result = FilteredIncludeParser.Parse("orders(Status = 'Cancelled')"); // After (v3 — DSL syntax required) var result = FilteredIncludeParser.Parse("orders(Status:eq:Cancelled)"); ``` -### 2. JQL `Parse(string query)` → `Parse(string filter)` +### 2. FQL `Parse(string query)` → `Parse(string filter)` -The parameter name in `JqlParser.Parse()` and `IQueryParser.Parse()` has been renamed from `query` to `filter` to align with DSL and MiniOData conventions. +The parameter name in `FQLParser.Parse()` and `IQueryParser.Parse()` has been renamed from `query` to `filter` to align with DSL and MiniOData conventions. ```csharp // Before -new JqlParser().Parse(query: "status = 'Open'"); +new FQLParser().Parse(query: "status = 'Open'"); // After -new JqlParser().Parse(filter: "status = 'Open'"); +new FQLParser().Parse(filter: "status = 'Open'"); ``` > [!NOTE] -> Callers using positional arguments (e.g., `new JqlParser().Parse("status = 'Open'")`) do not need any change. +> Callers using positional arguments (e.g., `new FQLParser().Parse("status = 'Open'")`) do not need any change. ### 3. Package Rename: EFCore → EntityFrameworkCore @@ -91,7 +91,7 @@ using FlexQuery.NET.EntityFrameworkCore; |:---|:---|:---| | `QueryRequest` | `FlexQueryParameters` | Deprecated in v2, removed in v3 | | `FlexQueryRequest` | `FlexQueryParameters` | Deprecated in v2, removed in v3 | -| `QuerySyntax.Jql` | N/A | JQL is now an external parser | +| `QuerySyntax.FQL` | N/A | FQL is now an external parser | | `ApplyValidatedQueryOptions` | `FlexQueryAsync` | Deprecated in v2 | | `ToQueryResultAsync` | `FlexQueryAsync` | Deprecated in v2 | | `ToProjectedQueryResultAsync` | `FlexQueryAsync` | Deprecated in v2 | @@ -112,7 +112,7 @@ using FlexQuery.NET.EntityFrameworkCore; |:---|:---| | `FlexQuery.NET.Dapper` | Dapper and raw SQL provider with dialect support (SQL Server, SQLite, MySQL, PostgreSQL) | | `FlexQuery.NET.Parsers.MiniOData` | OData-style `$filter`, `$orderby`, `$select`, `$top`/`$skip` parser | -| `FlexQuery.NET.Parsers.Jql` | Extracted JQL parser (install-on-demand) | +| `FlexQuery.NET.Parsers.FQL` | Extracted FQL parser (install-on-demand) | | `FlexQuery.NET.Adapters.AgGrid` | AG Grid Enterprise Server-Side Row Model adapter | | `FlexQuery.NET.Adapters.Kendo` | Kendo UI DataSource adapter | @@ -277,7 +277,7 @@ FlexQuery.NET (Core) ├── FlexQuery.NET.EntityFrameworkCore (renamed from FlexQuery.NET.EFCore) ├── FlexQuery.NET.AspNetCore (unchanged) ├── FlexQuery.NET.Dapper (new) -├── FlexQuery.NET.Parsers.Jql (extracted from Core) +├── FlexQuery.NET.Parsers.FQL (extracted from Core) ├── FlexQuery.NET.Parsers.MiniOData (new) ├── FlexQuery.NET.Adapters.AgGrid (new) └── FlexQuery.NET.Adapters.Kendo (new) @@ -301,7 +301,7 @@ dotnet add package FlexQuery.NET.AspNetCore # Optional: Install new packages as needed dotnet add package FlexQuery.NET.Dapper -dotnet add package FlexQuery.NET.Parsers.Jql +dotnet add package FlexQuery.NET.Parsers.FQL dotnet add package FlexQuery.NET.Parsers.MiniOData dotnet add package FlexQuery.NET.Adapters.AgGrid dotnet add package FlexQuery.NET.Adapters.Kendo @@ -337,20 +337,20 @@ var projected = await query.ToProjectedQueryResultAsync(options); var result = await _context.Users.FlexQueryAsync(parameters, exec => { ... }); ``` -### Step 4: Install JQL Parser If Needed +### Step 4: Install FQL Parser If Needed -If your application uses `query=` parameters or JQL syntax, install the separate package: +If your application uses `query=` parameters or FQL syntax, install the separate package: ```bash -dotnet add package FlexQuery.NET.Parsers.Jql +dotnet add package FlexQuery.NET.Parsers.FQL ``` ### Step 5: Update Inline Include Filters -If you use filtered includes with JQL-style syntax, update to DSL: +If you use filtered includes with FQL-style syntax, update to DSL: ```csharp -// Before (v2 — JQL fallback) +// Before (v2 — FQL fallback) "orders(Status = 'Cancelled')" // After (v3 — DSL only) @@ -361,15 +361,15 @@ If you use filtered includes with JQL-style syntax, update to DSL: ## 🛠️ Common Migration Pitfalls -### ❌ Missing JQL Package +### ❌ Missing FQL Package ``` -// ERROR: The type or namespace 'JqlParser' does not exist +// ERROR: The type or namespace 'FQLParser' does not exist ``` -Install the package: `dotnet add package FlexQuery.NET.Parsers.Jql` +Install the package: `dotnet add package FlexQuery.NET.Parsers.FQL` -### ❌ Inline Include Filter Still Using JQL Syntax +### ❌ Inline Include Filter Still Using FQL Syntax ``` FilteredIncludeParser.Parse("orders(Status = 'Cancelled')") // Returns null in v3 diff --git a/docs/migration/v3-to-v4.md b/docs/migration/v3-to-v4.md new file mode 100644 index 0000000..7497532 --- /dev/null +++ b/docs/migration/v3-to-v4.md @@ -0,0 +1,816 @@ +# Migrating from FlexQuery.NET 3.1.1 to 4.0 + +This guide walks through upgrading an existing FlexQuery.NET v3.1.1 application to v4. It focuses on the changes developers must make—organized by developer task rather than by individual API. + +If you are starting fresh with FlexQuery.NET v4, see the [Getting Started](../guide/getting-started) documentation instead. + +--- + +## Overview + +**Who should read this guide:** Developers maintaining production applications built on FlexQuery.NET v3.1.1 who are upgrading to v4. + +**Target frameworks:** .NET 6.0, .NET 8.0, .NET 10.0 (.NET 7.0 was removed). + +**Upgrade effort:** Moderate. Most changes are mechanical—renames, namespace updates, and parameter replacements. Applications that use FQL syntax, aggregates, Dapper, or custom parsers require additional attention. + +**What changed in v4:** The parser architecture was redesigned for deterministic behavior, aggregates moved to a typed system, provider options were separated from core options, several implementation details were internalized, and the public API surface was reduced to prevent misuse. + +--- + +## Migration Impact + +The upgrade effort depends on which FlexQuery.NET features your application uses: + +| If your application uses... | Upgrade Impact | +| --------------------------- | -------------- | +| Native DSL only | Low | +| FQL | Medium | +| EF Core | Medium | +| Dapper | High | +| ASP.NET Core | Medium | +| Custom Parsers | High | + +Most changes are mechanical—renames, namespace updates, and parameter replacements. Applications combining multiple features (e.g., FQL + Dapper) will naturally have more migration surface area. + +--- + +## Quick Migration + +For the most common upgrade path—an application using default DSL syntax with EF Core: + +1. **Update NuGet packages** to v4 across all projects. +2. **If using FQL:** Replace the `FlexQuery.NET.Parsers.Jql` package reference with `FlexQuery.NET.Parsers.Fql`. +3. **Register parser packages:** Call `Fql.Register()` or `MiniOData.Register()` during startup. Parsers are no longer auto-discovered. +4. **Replace removed APIs:** `QueryOptionsParser.Parse()` → `parameters.ToQueryOptions()`, `ApplyFilteredIncludes()` → `ApplyExpand()`, `QuerySyntax.Jql` → `QuerySyntax.Fql`. +5. **Rebuild and run tests.** Most compilation errors are resolved by steps 2–4. + +The sections below explain each change in detail. + +--- + +## Package Changes + +Only one package name changed: + +| Package | v3.1.1 | v4 | +|---|---|---| +| JQL FQL Parser | `FlexQuery.NET.Parsers.Jql` | `FlexQuery.NET.Parsers.Fql` | + +All other packages—`FlexQuery.NET`, `FlexQuery.NET.EntityFrameworkCore`, `FlexQuery.NET.Dapper`, `FlexQuery.NET.AspNetCore`, `FlexQuery.NET.Parsers.MiniOData`, `FlexQuery.NET.Adapters.AgGrid`, `FlexQuery.NET.Adapters.Kendo`, and `FlexQuery.NET.Diagnostics`—retain their names. Only the version needs updating. + +To update package references via CLI: +```bash +dotnet remove package FlexQuery.NET.Parsers.Jql +dotnet add package FlexQuery.NET.Parsers.Fql + +# If referencing DI extensions from the Core package directly, +# remove the now-unnecessary using directives: +dotnet remove package FlexQuery.NET # only if not needed elsewhere +``` + +--- + +## What's New in v4 + +### Parser Architecture + +The parser system was redesigned for deterministic behavior. Parsers must be explicitly registered via a static `Register()` call (`Fql.Register()`, `MiniOData.Register()`) instead of being auto-discovered. The `QuerySyntax` enum was simplified to three values: `NativeDsl`, `Fql`, and `MiniOData`. The `IQueryParser` interface no longer includes `CanParse()`, making parser selection explicit and predictable. + +### Keyset Pagination + +Cursor-based pagination for large datasets. Use the `Cursor` and `UseKeysetPagination` properties on `FlexQueryBase`. Keyset pagination uses WHERE predicates instead of Skip/Take, providing significantly better performance for deep pages. + +```csharp +var parameters = new FlexQueryParameters +{ + Filter = "status:eq:active", + PageSize = 50, + UseKeysetPagination = true, + Cursor = previousResponse.NextCursorToken +}; +``` + +### Typed Exception Hierarchy + +A new exception hierarchy replaces the removed `InvalidFilterFieldException` and `InvalidSortFieldException`: + +- `FlexQueryException` — abstract base for all FlexQuery errors. +- `QueryParseException` — thrown when a query parameter value is malformed. Exposes `ParameterName`, `Syntax`, and `ReceivedValue` properties. +- `ParserNotRegisteredException` — thrown when no parser has been registered for the configured `QuerySyntax`. + +### Provider-Specific Options + +EF Core and Dapper now use dedicated options types (`EfCoreQueryOptions`, `DapperQueryOptions`) instead of sharing `QueryExecutionOptions`. This keeps core options provider-agnostic while enabling provider-specific configuration such as `UseNoTracking` for EF Core. + +### ASP.NET Core Enhancements + +New `FlexQueryRequest` and `MiniODataRequest` models provide dedicated types for POST and MiniOData endpoints. All `ServiceCollectionExtensions` classes now use the `Microsoft.Extensions.DependencyInjection` namespace, making extension methods discoverable without explicit `using` directives. + +### OpenAPI Integration + +A new `FlexQuery.NET.OpenApi` package provides OpenAPI/Swagger support for FlexQuery endpoints. + +### Execution APIs + +Queries are executed through the existing `IQueryable.FlexQuery(...)` . Global options are configured once at application startup through the Core package facade. + +```csharp +// Configure global options once at startup (console app, background service, etc.) +FlexQueryCore.Configure(o => o.MaxPageSize = 500); + + +// Extension-method entry point +var result = db.Users.FlexQuery(queryOptions); +``` + +--- + +## Required Migration + +### Parser System + +The parser system was redesigned to be deterministic. Every change in this section requires a code update. + +#### QueryOptionsParser Is No Longer Accessible + +`QueryOptionsParser` was made `internal`. Its `Parse()` methods remain public within the assembly but are no longer accessible from application code. + +**Before:** +```csharp +var options = QueryOptionsParser.Parse(parameters); +``` + +**After:** +```csharp +var options = parameters.ToQueryOptions(); +``` + +The `ToQueryOptions()` extension method is available on `FlexQueryParameters`, `FlexQueryRequest`, and `MiniODataRequest`. + +#### Parsers Must Be Registered Explicitly + +v3.1.1 auto-discovered parser packages via reflection at startup. v4 requires explicit registration through a static `Register()` call. Without registration, requests targeting FQL or MiniOData syntax throw `ParserNotRegisteredException`. + +**Before:** +```csharp +// No registration needed — parsers auto-discovered +services.AddFlexQuery(); +``` + +**After:** +```csharp +// Configure global options and register parsers once at startup. +// No additional using statements needed — these are static methods on +// the parser classes in their own namespaces. +FlexQueryCore.Configure(); +Fql.Register(); // Register the FQL parser +// or: MiniOData.Register(); // Register the MiniOData parser +``` + +#### IQueryParser.CanParse() Removed + +The `CanParse()` method was removed from the `IQueryParser` interface. Custom parser implementations must delete this method. + +**Migration:** Remove `CanParse()` from any class implementing `IQueryParser`. The interface now requires only `Syntax` and `Parse()`. + +#### QuerySyntax Changes + +`QuerySyntax.AutoDetect`, `QuerySyntax.Json`, and `QuerySyntax.Generic` were removed. Only `NativeDsl`, `Fql`, and `MiniOData` remain. `QuerySyntax.Jql` was renamed to `QuerySyntax.Fql`. + +**Before:** +```csharp +options.QuerySyntax = QuerySyntax.Jql; +``` + +**After:** +```csharp +options.QuerySyntax = QuerySyntax.Fql; +``` + +The default syntax in v4 is `NativeDsl`. Applications that relied on `AutoDetect` must now set the syntax explicitly and register the corresponding parser. + +#### Startup Registration Is Now Static (No DI for Core, Parsers, and Providers) + +In v3.1.1, `FlexQuery.NET` and every provider/parser package exposed `IServiceCollection` extension methods (e.g. `AddFlexQuery()`, `AddFqlParser()`, `AddFlexQueryDapper()`) that performed initialization inside the DI container. In v4 these packages are framework-agnostic and no longer depend on `Microsoft.Extensions.DependencyInjection`. Initialization is performed with **static** methods called once at startup: + +```csharp + +FlexQueryCore.Configure(...) +FlexQueryEFCore.Configure(...) +FlexQueryDapper.Configure(...) + +``` +These static facades live in the root namespace of their respective packages. + +| Package | v3.1.1 | v4 Static API | +|---------|--------|---------------| +| Core (`FlexQuery.NET`) | `services.AddFlexQuery()` | `FlexQueryCore.Configure(...)` | +| AspNetCore (`FlexQuery.NET.AspNetCore`) | `services.AddFlexQuery()` | `FlexQueryCore.Configure(...)` | +| FQL Parser | `services.AddFqlParser()` | `Fql.Register()` | +| MiniOData Parser | `services.AddMiniOData()` | `MiniOData.Register()` | +| EF Core | `services.AddFlexQueryEntityFrameworkCore()` | `FlexQueryEFCore.Configure(...)` | +| Dapper | `services.AddFlexQueryDapper(...)` | `FlexQueryDapper.Configure(...)` | + +Only the ASP.NET Core and OpenAPI packages retain genuine DI extensions, because they integrate with the MVC / Swagger pipelines: + +| Package | v4 DI Extension (namespace `Microsoft.Extensions.DependencyInjection`) | +|---------|--------------------------------------------------------------------------| +| ASP.NET Core | `AddFlexQuerySecurity(IMvcBuilder)` — registers the field-access filter | +| OpenAPI | `AddFlexQueryOpenApi(IServiceCollection)` — registers the OpenAPI transformer | + +**Migration:** Remove old `using FlexQuery.NET.*.DependencyInjection` or `using FlexQuery.NET.*.Extensions` directives. Replace each `services.AddXxx(...)` initialization call with the corresponding static method. The ASP.NET Core `AddFlexQuerySecurity()` method lives in the `Microsoft.Extensions.DependencyInjection` namespace and is implicitly available in ASP.NET Core projects. + +#### AddJqlParser Replaced by Fql.Register() + +The `AddJqlParser()` / `AddFqlParser()` DI extension was removed. Registration is now a static call. + +**Before:** +```csharp +services.AddJqlParser(); +``` + +**After:** +```csharp +Fql.Register(); +``` + +#### MiniOData Endpoints Must Use MiniODataRequest + +MiniOData endpoints that previously accepted `FlexQueryParameters` must now use `MiniODataRequest`. + +**Before:** +```csharp +[HttpGet] +public async Task Get([FromQuery] FlexQueryParameters parameters) +{ + var options = QueryOptionsParser.Parse(parameters, QuerySyntax.MiniOData); + // ... +} +``` + +**After:** +```csharp +[HttpGet] +public async Task Get([FromQuery] MiniODataRequest request) +{ + var options = request.ToQueryOptions(); + // ... +} +``` + +--- + +### Core Library + +#### Core Package No Longer Registers DI Services + +The `FlexQuery.NET` core package no longer exposes any `IServiceCollection` extension methods. Its `DependencyInjection` directory was removed and the dependency on `Microsoft.Extensions.DependencyInjection.Abstractions` was dropped, keeping the core framework-agnostic. + +Global options are now configured once at startup with the static `FlexQueryCore.Configure()` method: + +```csharp +// Configure global defaults — call once at startup, before executing queries +FlexQueryCore.Configure(o => +{ + o.MaxPageSize = 500; + o.QuerySyntax = QuerySyntax.Fql; +}); +``` + +Non-ASP.NET Core applications (e.g., console apps or background services) should: +- Call `FlexQueryCore.Configure(...)` for global options +- Resolve queries via the `IQueryable.FlexQuery(...)` extension methods + +The `FlexQueryProcessor` implementation and its `IFlexQueryProcessor` contract are `internal` and are not part of the public API, so applications should not construct or reference them directly. + +**Migration:** Remove `using FlexQuery.NET.DependencyInjection;` if present. Replace any `services.AddFlexQuery(...)` call with `FlexQueryCore.Configure(...)`. + +#### FlexQueryParameters.Query Removed + +The `Query` property was removed. Use `Filter` for all query string content. + +**Migration:** Replace all references to `parameters.Query` with `parameters.Filter`. + +#### Exception Handling + +`InvalidFilterFieldException` and `InvalidSortFieldException` were removed. They are replaced by the new exception hierarchy: + +| Exception | Purpose | +|---|---| +| `FlexQueryException` | Abstract base for all FlexQuery errors | +| `QueryParseException` | Parse failures (includes parameter name, syntax, and received value) | +| `ParserNotRegisteredException` | No parser registered for the configured syntax | + +**Migration:** +```csharp +// Before +catch (InvalidFilterFieldException ex) { ... } +catch (InvalidSortFieldException ex) { ... } + +// After +catch (QueryParseException ex) { ... } +catch (ParserNotRegisteredException ex) { ... } +catch (FlexQueryException ex) { ... } // base for all FlexQuery errors +``` + +#### Public API Cleanup + +Several implementation-detail APIs that were inadvertently exposed in v3 have been internalized or removed in v4. No migration path is provided for replicating the internal behavior—these were never guaranteed to remain stable. + +| API | Status in v4 | +|---|---| +| `FlexQueryParameters.RawParameters` | Internal | +| `QueryOptions.CaseInsensitive` | Internal | +| `QueryOptions.EnableCache` | Internal | +| `QueryOptions.Items` | Internal | +| `QueryOptions.Skip` | Removed | +| `QueryOptions.Top` | Removed | +| `QueryOptions.GetCacheKey()` | Removed | +| `QueryOptions.Clone()` | Removed | +| `QueryOptions.CloneWithFilter()` | Removed | +| `QueryOptions.Ast` | Removed | +| `QueryExecutionOptions.UseNoTracking` | Removed (moved to `EfCoreQueryOptions`) | +| `QueryExecutionOptions.UseSplitQuery` | Removed (no v4 equivalent — split-query execution is no longer configurable) | +| `BaseQueryOptions.FieldAccessResolver` | Internal | +| `FlexQueryOptions.UseNoTracking` | Removed | + +**Migration:** Remove all references to these members. If you were accessing `QueryOptions.CaseInsensitive` or `QueryOptions.Items`, consider whether the value can be configured through `FlexQueryOptions` or `BaseQueryOptions` instead. + +--- + +### ASP.NET Core + +#### DI Registration Namespaces Changed + +The `ServiceCollectionExtensions` class for ASP.NET Core moved from `FlexQuery.NET.AspNetCore.Extensions` to `Microsoft.Extensions.DependencyInjection`. The only remaining DI extension is `AddFlexQuerySecurity()`, which registers the field-access filter with MVC. Global option configuration is no longer a DI concern — use the static `FlexQueryCore.Configure()` instead. + +**Migration:** Remove `using FlexQuery.NET.AspNetCore.Extensions;`. The `Microsoft.Extensions.DependencyInjection` namespace is implicitly available in ASP.NET Core projects. Replace any `services.AddFlexQuery(...)` call with `FlexQueryCore.Configure(...)`. + +#### POST Endpoints Must Use FlexQueryRequest + +HTTP POST endpoints that previously accepted `FlexQueryParameters` must now use `FlexQueryRequest`. GET endpoints continue to use `FlexQueryParameters`. + +**Before:** +```csharp +[HttpPost] +public async Task Search([FromBody] FlexQueryParameters parameters) +{ + var options = QueryOptionsParser.Parse(parameters); + // ... +} +``` + +**After:** +```csharp +[HttpPost] +public async Task Search([FromBody] FlexQueryRequest request) +{ + var options = request.ToQueryOptions(); + // ... +} +``` + +--- + +### Aggregate System + +#### AggregateFunction Enum Replaces Strings + +`AggregateModel.Function` and `HavingCondition.Function` changed from `string` to the `AggregateFunction` enum. Code that assigns or compares string values will not compile. + +**Before:** +```csharp +new AggregateModel { Function = "sum", Field = "Total" }; +new HavingCondition { Function = "count", Operator = "gt", Value = "5" }; + +if (aggregate.Function == "sum") { ... } +``` + +**After:** +```csharp +new AggregateModel { Function = AggregateFunction.Sum, Field = "Total" }; +new HavingCondition { Function = AggregateFunction.Count, Operator = "gt", Value = "5" }; + +if (aggregate.Function == AggregateFunction.Sum) { ... } +``` + +#### Aggregates Require a Dedicated Parameter + +Aggregates embedded in the `select` parameter are no longer recognized. A dedicated `aggregates` query parameter is required. + +**Before:** +```http +GET /api/orders?select=status,sum(amount),count(id)&groupBy=status +``` + +**After:** +```http +GET /api/orders?select=status&aggregates=amount:sum,id:count&groupBy=status +``` + +#### AggregateModel and HavingCondition Namespaces Changed + +Both types moved from `FlexQuery.NET.Models` to `FlexQuery.NET.Models.Aggregates`. If your code references them with fully qualified names, update the namespace. + +#### Aggregate Alias Format Changed + +Auto-generated aggregate aliases changed from camelCase to PascalCase. Client code referencing aggregate results by alias must use the new casing (e.g., `AmountSum` instead of `amountSum`). + +--- + +### Provider Integrations + +#### EF Core Integration Improvements + +##### Startup Registration Is Now Static + +The `AddFlexQueryEntityFrameworkCore()` DI extension was removed. EF Core is now initialized once at startup with the static `FlexQueryEFCore.Configure(...)` method, which stores the global `FlexQueryEfCoreOptions` used by `FlexQueryAsync` and registers the EF Core-specific operators: + +**Before:** +```csharp +services.AddFlexQueryEntityFrameworkCore(); +``` + +**After:** +```csharp +FlexQueryEFCore.Configure(cfg => +{ + cfg.UseNoTracking = true; +}); +``` + +**Migration:** Remove the `AddFlexQueryEntityFrameworkCore()` call and the corresponding `using FlexQuery.NET.EntityFrameworkCore.DependencyInjection;` directive. Call `FlexQueryEFCore.Configure(...)` once during startup (or `FlexQueryEFCore.Setup()` if you only need operator registration). + +##### ApplyFilteredIncludes Renamed to ApplyExpand + +**Before:** +```csharp +query.ApplyFilteredIncludes(options); +``` + +**After:** +```csharp +query.ApplyExpand(options); +``` + +##### FlexQueryAsync Accepts EfCoreQueryOptions + +The `configure` delegate parameter type changed from `QueryExecutionOptions` to `EfCoreQueryOptions`. The `configureExecution` parameter (used for event listeners) was removed. A `CancellationToken` parameter was added to all overloads. + +**Before:** +```csharp +await query.FlexQueryAsync(parameters, + configure: opts => opts.UseNoTracking = true, + configureExecution: cfg => cfg.Listener = new MyListener()); +``` + +**After:** +```csharp +await query.FlexQueryAsync(parameters, + configure: opts => { + opts.UseNoTracking = true; + opts.Listener = new MyListener(); + }); +``` + +If you were using `configureExecution` to attach an execution listener, set it via `BaseQueryOptions.Listener` instead: + +```csharp +var efOptions = new EfCoreQueryOptions { Listener = new MyListener() }; +await query.FlexQueryAsync(parameters, efOptions); +``` + +##### UseNoTracking Moved + +The `UseNoTracking` property was removed from `QueryExecutionOptions` and is now available on `EfCoreQueryOptions`. Note that `QueryExecutionOptions.UseSplitQuery` was also removed in v4 and has no equivalent in `EfCoreQueryOptions` — split-query execution is no longer configurable. + +**Before:** +```csharp +new QueryExecutionOptions { UseNoTracking = false }; +``` + +**After:** +```csharp +new EfCoreQueryOptions { UseNoTracking = false }; +``` + +#### Dapper Integration Modernization + +##### Startup Registration Is Now Static + +The `AddFlexQueryDapper(...)` DI extension was removed. Dapper entity mappings are now configured once at startup with the static `FlexQueryDapper.Configure(...)` method, which builds the model and stores it internally as the global runtime model: + +**Before:** +```csharp +services.AddFlexQueryDapper(cfg => +{ + cfg.Model.Entity().ToTable("Customers"); +}); +``` + +**After:** +```csharp +FlexQueryDapper.Configure(cfg => +{ + cfg.Model.Entity().ToTable("Customers"); +}); +``` + +**Migration:** Remove the `AddFlexQueryDapper(...)` call and the corresponding `using FlexQuery.NET.Dapper.DependencyInjection;` directive. Call `FlexQueryDapper.Configure(...)` once during startup. `FlexQueryAsync` automatically uses the configured global model; you no longer need to capture and re-apply the built model on each call. + +##### DapperQueryOptions Namespace Changed + +`DapperQueryOptions` moved from `FlexQuery.NET.Dapper` to `FlexQuery.NET.Dapper.Options`. + +**Migration:** Update `using FlexQuery.NET.Dapper;` to `using FlexQuery.NET.Dapper.Options;` where `DapperQueryOptions` is referenced. + +##### DapperQueryOptions API Simplified + +The following members were removed. The class is now configuration-only: + +- `Dialect`, `MappingRegistry`, `EntityType`, `Entity()`, `ScanEntitiesFromAssembly()` +- `GlobalDefaultDialect`, `GlobalDialectResolver` +- `ToQueryExecutionOptions()`, the copy constructor +- `DapperQueryOptionsExtensions.UseSqlServer()`, `UsePostgreSql()`, `UseSqlite()`, `UseMappingRegistry()` + +**Migration:** Remove references to these members. `DapperQueryOptions` now exposes only `CommandTimeout` (plus the internal `UseModel()` escape hatch for supplying a per-request model that overrides the global one). Entity metadata is configured once at startup via `FlexQueryDapper.Configure(...)`. + +##### FlexQueryAsync Signature Changes + +The `configureExecution` parameter (for event listeners) was removed. `CancellationToken` was added to all overloads. Two overloads were removed. + +**Migration:** Remove `configureExecution` arguments. Add `CancellationToken` if needed. + +--- + +### Adapters + +#### AG Grid + +`FromAgGridJson()` was replaced with `ToQueryOptions(JsonElement)`. + +**Before:** +```csharp +var options = agGridJsonString.FromAgGridJson(); +``` + +**After:** +```csharp +var options = agGridJsonElement.ToQueryOptions(); +``` + +#### Kendo + +`FromKendoJson()` was replaced with `ToQueryOptions(JsonElement)`. + +**Before:** +```csharp +var options = kendoJsonString.FromKendoJson(); +``` + +**After:** +```csharp +var options = kendoJsonElement.ToQueryOptions(); +``` + +--- + +## Behavioral Changes + +These changes affect runtime behavior without breaking compilation. Review each one. + +#### Parser Auto-Detection Removed + +v3.1.1 defaulted to `AutoDetect`, which tried registered parsers in order and fell back to DSL. v4 defaults to `NativeDsl` and only uses the parser registered for the configured syntax. Applications that relied on auto-detection for FQL or MiniOData queries will receive `ParserNotRegisteredException` at runtime. + +**Review:** Ensure every endpoint that uses FQL or MiniOData syntax has `QuerySyntax` configured and the parser registered. + +#### Strict Parameter Validation + +v3.1.1 silently ignored malformed page numbers, sort expressions, and other parameter values. v4 throws `QueryParseException` with details about the invalid parameter, including the parameter name, expected syntax, and received value. + +**Review:** Client code passing user-supplied query parameters should handle `QueryParseException` or validate parameters before sending. + +#### Inline Aggregates in select No Longer Supported + +v3.1.1 extracted aggregate expressions like `sum(amount)` from the `select` parameter. v4 treats everything in `select` as field names. Aggregate expressions must use the dedicated `aggregates` parameter. + +**Review:** If your application embeds aggregates in `select`, move them to the `aggregates` parameter. + +--- + +## Troubleshooting + +#### `ParserNotRegisteredException` at runtime + +**Cause:** No parser has been registered for the configured `QuerySyntax`. FQL or MiniOData queries will fail if the parser was not registered during startup. + +**Fix:** Register the parser statically: +```csharp +Fql.Register(); // for FQL syntax +MiniOData.Register(); // for MiniOData syntax +``` + +#### `QueryParseException` when parsing parameters + +**Cause:** A query parameter value is malformed—for example, a non-numeric page value or unbalanced filter parentheses. + +**Fix:** Validate query strings before sending, or handle `QueryParseException` and return a descriptive error response. The exception exposes `ParameterName`, `Syntax`, and `ReceivedValue` for diagnostics. + +#### `'QueryOptionsParser' is inaccessible due to its protection level` + +**Cause:** The class was made `internal` in v4. + +**Fix:** Replace `QueryOptionsParser.Parse(parameters)` with `parameters.ToQueryOptions()`. + +#### `'QuerySyntax' does not contain a definition for 'Jql'` + +**Cause:** The enum value was renamed. + +**Fix:** Replace all references to `QuerySyntax.Jql` with `QuerySyntax.Fql`. + +#### `Cannot implicitly convert type 'string' to 'AggregateFunction'` (or the namespaced equivalent) + +**Cause:** `AggregateModel.Function` and `HavingCondition.Function` now use the `AggregateFunction` enum. The exact error shows the resolved namespace (e.g., `FlexQuery.NET.Models.Aggregates.AggregateFunction`). + +**Fix:** Use enum members instead of string literals: `AggregateFunction.Sum` instead of `"sum"`. + +#### `The name 'AddFlexQuery' does not exist in the current context` + +**Cause:** The `FlexQuery.NET.DependencyInjection` namespace was removed from the Core package, and the `AddFlexQuery()` DI extension no longer exists. Global options are now configured with the static `FlexQueryCore.Configure()` method. + +**Fix:** Call `FlexQueryCore.Configure(...)` once at startup. In ASP.NET Core projects, remove the old `using FlexQuery.NET.DependencyInjection;` directive — the `Microsoft.Extensions.DependencyInjection` namespace is implicitly available for the remaining `AddFlexQuerySecurity()` extension. + +#### `'FqlParser' does not contain a definition for 'Register'` / no `AddFqlParser` available + +**Cause:** The package reference still points to the old `FlexQuery.NET.Parsers.Jql` package, or code still calls the removed `AddFqlParser()` DI extension. + +**Fix:** Update the package reference to `FlexQuery.NET.Parsers.Fql`. Replace `services.AddFqlParser()` with the static `Fql.Register()` call (namespace `FlexQuery.NET.Parsers.Fql`, no `using` needed if fully qualified or imported). + +#### `'ApplyFilteredIncludes' does not exist in the current context` + +**Cause:** The extension method was renamed. + +**Fix:** Replace `ApplyFilteredIncludes()` with `ApplyExpand()`. + +--- + +## Startup Methods Reference + +v4 replaces the previous DI-based startup APIs with package-level static facades. Each package exposes its own startup entry point responsible for global configuration or one-time registration. All of them are framework-agnostic (no dependency on `Microsoft.Extensions.DependencyInjection`) and live in their own type namespaces, so they need no special `using` directive beyond the type itself. + +### `FlexQueryCore.Configure(...)` + +**Namespace:** `FlexQuery.NET` · **Package:** `FlexQuery.NET` + +Configures the global `FlexQueryOptions` that serve as defaults for every query in the application. It: + +- builds a fresh `FlexQueryOptions` instance, +- applies the optional `Action` delegate you pass, +- sets the global parser syntax via `QueryOptionsParser.SetGlobalSyntax(options.QuerySyntax)`, +- stores the result as the process-wide default options (`FlexQueryCore.DefaultOptions`), which the `IQueryable.FlexQuery(...)` extension methods use at execution time. + +Call it **once** during startup, before any queries run: + +```csharp +FlexQueryCore.Configure(o => +{ + o.MaxPageSize = 500; + o.MaxFieldDepth = 3; + o.DefaultPageSize = 50; + o.QuerySyntax = QuerySyntax.Fql; +}); +``` + +### `Fql.Register()` + +**Namespace:** `FlexQuery.NET.Parsers.Fql` · **Package:** `FlexQuery.NET.Parsers.Fql` + +Registers the FQL query parser (`FqlQueryParser`) in the global parser registry under `QuerySyntax.Fql`. After this call, requests configured with FQL syntax can be parsed. Must be called **once** at startup if your application uses FQL. Replaces the old `AddJqlParser()` / `AddFqlParser()` DI extension. + +```csharp +Fql.Register(); +``` + +### `MiniOData.Register()` + +**Namespace:** `FlexQuery.NET.Parsers.MiniOData` · **Package:** `FlexQuery.NET.Parsers.MiniOData` + +Registers the Mini OData query parser (`MiniODataQueryParser`) in the global parser registry under `QuerySyntax.MiniOData`. After this call, requests configured with MiniOData syntax can be parsed. Must be called **once** at startup if your application uses MiniOData. Replaces the old `AddMiniOData()` / `AddFlexQueryMiniOData()` DI extension. + +```csharp +MiniOData.Register(); +``` + +### `FlexQueryEFCore.Configure(...)` and `FlexQueryEFCore.Setup()` + +**Namespace:** `FlexQuery.NET.EntityFrameworkCore` · **Package:** `FlexQuery.NET.EntityFrameworkCore` + +`FlexQueryEFCore.Configure(Action?)` configures the global EF Core execution options used by `FlexQueryAsync` at runtime and also ensures the EF Core-specific query operators are registered. It stores the supplied `FlexQueryEfCoreOptions` as the global default, so `FlexQueryAsync` automatically applies them when no per-execution options are provided. It replaces the old `AddFlexQueryEntityFrameworkCore()` DI extension. + +`FlexQueryEFCore.Setup()` performs the same one-time operator registration without configuring options. Use it directly only if you do not need global EF Core options. + +```csharp +FlexQueryEFCore.Configure(cfg => +{ + cfg.UseNoTracking = true; +}); +``` + +### `FlexQueryDapper.Configure(...)` + +**Namespace:** `FlexQuery.NET.Dapper` · **Package:** `FlexQuery.NET.Dapper` + +Configures the Dapper entity-mapping model from the supplied `Action` delegate and stores the resulting `FlexQueryModel` internally as the global runtime model. It: + +- creates a `FlexQueryDapperOptions` instance, +- invokes your delegate so you can configure entity mappings (`o.Model.Entity()` and relationships), +- calls `Model.Build()` to produce the immutable `FlexQueryModel`, +- stores that model as the process-wide default used by `FlexQueryAsync` at execution time. + +`FlexQueryModel` itself is an internal implementation detail and is not returned. This replaces the old `AddFlexQueryDapper(...)` DI extension (including the `AddFlexQueryDapperSqlServer/PostgreSql/Sqlite` dialect variants). Must be called **once** at startup if your application uses Dapper. + +```csharp +FlexQueryDapper.Configure(o => +{ + o.Model.Entity().ToTable("Customers"); + o.Model.Entity().ToTable("Orders"); +}); +``` + +--- + +## FAQ + +#### Why was JQL renamed to FQL? + +"FQL" (FlexQuery Language) better reflects that this is the project's native query language, not a Jira Query Language compatibility layer. The rename aligns naming across the ecosystem. + +#### Why is parser registration explicit? + +v3 auto-discovery caused ambiguity when multiple parsers could handle the same input. Explicit registration makes parser selection predictable, documents the dependency clearly, and eliminates runtime surprises. + +#### Why was automatic syntax detection removed? + +`CanParse()` was unreliable and ambiguous—multiple parsers could return `true` for the same input, and the first match wasn't always the correct one. Explicit `QuerySyntax` selection removes ambiguity and makes request processing deterministic. + +#### Why are aggregates separate from `select`? + +Mixing projection fields with aggregate functions in `select` (e.g., `select=status,sum(amount)`) created ambiguity. The dedicated `aggregates` parameter cleanly separates concerns, matching how SQL dialects and reporting tools model the distinction. + +#### Why use AggregateFunction instead of strings? + +String-typed function names allowed invalid values at compile time. The enum provides type safety, IntelliSense, and eliminates runtime errors from typos. + +#### Why were many APIs made internal? + +Properties like `CaseInsensitive`, `Items`, and `Clone()` were implementation details exposed inadvertently. Internalizing them reduces the API surface, prevents misuse, and allows the team to evolve the internal implementation without breaking changes. + +#### Why was DI registration removed from the Core, parser, and provider packages? + +In v3 most packages exposed `IServiceCollection` extension methods (`AddFlexQuery`, `AddJqlParser`, `AddMiniOData`, `AddFlexQueryDapper`, `AddFlexQueryEntityFrameworkCore`, etc.). Auditing v3.1.1 showed that the majority of these were *fake* registrations: the services they added were never actually resolved from the container. For example, parsers were discovered through a static registry / reflection auto-discovery rather than DI, and the Dapper options were consumed through a static `GlobalDefaultDialect` property rather than an injected `DapperQueryOptions`. The DI registrations added packages, public surface area, and a misleading "this is wired into the container" signal without providing any real benefit. + +In v4 the Core, parser (FQL/MiniOData), EF Core, and Dapper packages are framework-agnostic and no longer depend on `Microsoft.Extensions.DependencyInjection.Abstractions`. Initialization is performed with explicit **static** methods called once at startup — `FlexQueryCore.Configure()`, `Fql.Register()`, `MiniOData.Register()`, `FlexQueryEFCore.Setup()`, `FlexQueryDapper.Configure(...)` — which makes registration deterministic and removes the dead DI surface. Only the packages that genuinely integrate with the ASP.NET Core pipeline keep DI extensions: `AddFlexQuerySecurity()` (registers an MVC filter) and `AddFlexQueryOpenApi()` (registers the OpenAPI transformer). + +#### Why did the DI extension namespaces change? + +The old `ServiceCollectionExtensions` classes lived in package-specific namespaces such as `FlexQuery.NET.DependencyInjection`, `FlexQuery.NET.AspNetCore.Extensions`, `FlexQuery.NET.Parsers.Jql`, `FlexQuery.NET.Parsers.MiniOData.Extensions`, `FlexQuery.NET.EntityFrameworkCore.DependencyInjection`, and `FlexQuery.NET.Dapper.DependencyInjection`. Because each package used its own namespace, callers had to add a different `using` directive per package — a divergence from how the wider .NET ecosystem works, where DI extensions are conventionally placed in `Microsoft.Extensions.DependencyInjection` so they are discoverable without explicit `using` statements. + +Since v4 dropped DI from the Core, parser, EF Core, and Dapper packages entirely, those namespaces are gone. The remaining DI extensions — `AddFlexQuerySecurity()` (ASP.NET Core) and `AddFlexQueryOpenApi()` (OpenAPI) — were consolidated into the `Microsoft.Extensions.DependencyInjection` namespace. With those `using` directives removed, the static startup methods (e.g. `Fql.Register()`, `FlexQueryEFCore.Configure()`) are reached through their own type namespaces and no longer require DI namespace imports. + + +#### Can v3 and v4 coexist? + +No. Both versions cannot be referenced in the same project. Upgrade all FlexQuery.NET packages to v4 simultaneously. + +#### Can I continue using DSL after upgrading? + +Yes. DSL (default syntax) remains fully supported and unchanged. The only required change is replacing `QueryOptionsParser.Parse(parameters)` with `parameters.ToQueryOptions()`. + +--- + +## Upgrade Checklist + +- [ ] All FlexQuery.NET packages updated to v4 +- [ ] `FlexQuery.NET.Parsers.Jql` → `FlexQuery.NET.Parsers.Fql` (if using FQL) +- [ ] `FlexQueryCore.Configure(...)` called once at startup for global options +- [ ] Parser packages registered via `Fql.Register()` / `MiniOData.Register()` +- [ ] EF Core: `FlexQueryEFCore.Configure(...)` called at startup (replaces `AddFlexQueryEntityFrameworkCore()`) +- [ ] Dapper: `FlexQueryDapper.Configure(...)` called at startup (replaces `AddFlexQueryDapper()`) +- [ ] `using FlexQuery.NET.DependencyInjection;` removed (Core is DI-free; use static `FlexQueryCore.Configure()`) +- [ ] `using FlexQuery.NET.AspNetCore.Extensions;` removed (now `Microsoft.Extensions.DependencyInjection`) +- [ ] `using FlexQuery.NET.Parsers.Fql.DependencyInjection;` removed (use static `Fql.Register()`) +- [ ] `using FlexQuery.NET.Parsers.MiniOData.DependencyInjection;` / `.Extensions` removed (use static `MiniOData.Register()`) +- [ ] `using FlexQuery.NET.EntityFrameworkCore.DependencyInjection;` removed (use static `FlexQueryEFCore.Configure()`) +- [ ] `using FlexQuery.NET.Dapper.DependencyInjection;` removed (use static `FlexQueryDapper.Configure()`) +- [ ] `QueryOptionsParser.Parse()` → `parameters.ToQueryOptions()` +- [ ] `ApplyFilteredIncludes()` → `ApplyExpand()` +- [ ] `QuerySyntax.Jql` → `QuerySyntax.Fql` +- [ ] POST endpoints use `FlexQueryRequest`, MiniOData endpoints use `MiniODataRequest` +- [ ] Aggregates moved from `select` to `aggregates` parameter +- [ ] `AggregateModel.Function` / `HavingCondition.Function` use `AggregateFunction` enum +- [ ] Catch blocks updated for new exception types +- [ ] DapperQueryOptions namespace updated (if using Dapper) +- [ ] Project builds without errors +- [ ] All tests pass diff --git a/docs/package-lock.json b/docs/package-lock.json index f46670f..69b410e 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -1,618 +1,1070 @@ { - "name": "dynamic-queryable-docs", + "name": "flexquery.net-docs", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@algolia/abtesting": { + "packages": { + "": { + "name": "flexquery.net-docs", + "version": "1.0.0", + "devDependencies": { + "vitepress": "^1.5.0" + } + }, + "node_modules/@algolia/abtesting": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.0.tgz", "integrity": "sha512-8siuLG+FIns1AjZ/g2SDVwHz9S+ObacDQISEJvS8XsNei1zl3FXqfqQrBpmrG7ACWCyesXHbicMJtvRbg00FEw==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/autocomplete-core": { + "node_modules/@algolia/autocomplete-core": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", "dev": true, - "requires": { + "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, - "@algolia/autocomplete-plugin-algolia-insights": { + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", "dev": true, - "requires": { + "dependencies": { "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" } }, - "@algolia/autocomplete-preset-algolia": { + "node_modules/@algolia/autocomplete-preset-algolia": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", "dev": true, - "requires": { + "dependencies": { "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" } }, - "@algolia/autocomplete-shared": { + "node_modules/@algolia/autocomplete-shared": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", - "dev": true + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } }, - "@algolia/client-abtesting": { + "node_modules/@algolia/client-abtesting": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.0.tgz", "integrity": "sha512-wtwPgyPmO7b7sQPVgoK29c1VpfS08DnnJCmxX/oU1pV2DlMRJCzQcLN7JSloYpodyKHwM8+9wOzlAM0co3TDmA==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/client-analytics": { + "node_modules/@algolia/client-analytics": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.0.tgz", "integrity": "sha512-9KY36bRl4AH7RjqSeDDOKnjsz4IxQFBEOB8/fWmEbdQe+Isbs5jGzVJu9NEPQ1Tgwxlf8Uf07Swj3jZyMNUZ2g==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/client-common": { + "node_modules/@algolia/client-common": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.0.tgz", "integrity": "sha512-3a/qM3dzJqqfTx7Yrw7uGQ98I3Q0rDfb4Vkv0wEzko96l7YQMxfBVz/VbLq2N+c59GweYv6Vhp8mPeqnWJSITw==", - "dev": true + "dev": true, + "engines": { + "node": ">= 14.0.0" + } }, - "@algolia/client-insights": { + "node_modules/@algolia/client-insights": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.0.tgz", "integrity": "sha512-Rki7ACbMcvbQW0BuM84x9dkGHY47ABmv4jU6tYssat2k02p3mIUms2YOLUAMeknhmnFsj6lb6ZzOXdMWMyc1sA==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/client-personalization": { + "node_modules/@algolia/client-personalization": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.0.tgz", "integrity": "sha512-96s4Uzc3kk+/f4jJXIVVGWP5XlngOGNQ1x6hW9AT59pOixHlOs5tqJg+ZUS/GQ6h/iYP0ceQcmxDQeLyCLTaDQ==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/client-query-suggestions": { + "node_modules/@algolia/client-query-suggestions": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.0.tgz", "integrity": "sha512-lqeycNpSPe5Qa0OUWpejVvYQjQWV5nQuLT0a4aq7XzRAvCxprV/6Lf841EygdD2nrFnuS58ok7Au1uOtXzpnkg==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/client-search": { + "node_modules/@algolia/client-search": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.0.tgz", "integrity": "sha512-ly1wETVGRo30cx61O7fetESN+ElL9c9K+bD/AVgnT1ar4c6v+/Yqjrhdtu6Fm4D0s4NZP081Isf6tunH1wUXHg==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/ingestion": { + "node_modules/@algolia/ingestion": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.0.tgz", "integrity": "sha512-U4EeTvgmluRjj39ykZSAd5X+a6LD5m7/mcOWDmB7hqm1R6QY0yT8jLxpNVEjYhzgEN5hcDGW6X67EWQY8KiYGQ==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/monitoring": { + "node_modules/@algolia/monitoring": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.0.tgz", "integrity": "sha512-FCPnDcILfpTE94u7BVlV4DmnSV5wE3+j25EEF+3dYPrVzkVCSoAHs318oWDGxnxsAgiL4HpL12Jc4XHmw9shpA==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/recommend": { + "node_modules/@algolia/recommend": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.0.tgz", "integrity": "sha512-br3DO7n4N8CXwTRbZS0MnB4WQ9YHfNjCwkCEzVR/wek/qNTDQKDb0nROmkFaNZ8ucUqUVKZi074dbwMwRDlK8Q==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0", "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/requester-browser-xhr": { + "node_modules/@algolia/requester-browser-xhr": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.0.tgz", "integrity": "sha512-b0T/Ca2c9KyEslKsVrGZvbe1UrrKKSdfXhBZ2pbpKahFUzJfziRZ0urbOm7V65O0tO/jwU+Lo/+bIiiyhzGt8w==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/requester-fetch": { + "node_modules/@algolia/requester-fetch": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.0.tgz", "integrity": "sha512-ozBT8J/mtD4H4IAojw8QPirlcL2gHrI1BGuZ4/ZXXO/rTE1yQ4VIPJj4mTTbwo4FbkS1MoJsD/DsrqLzhnc4/g==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@algolia/requester-node-http": { + "node_modules/@algolia/requester-node-http": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.0.tgz", "integrity": "sha512-gyyWcLD22tnabmoit4iukCXuoRc5HYJuUjPSEa8a0D/f/NlRafpWi52AlAaa4Uu/rsl7saHsJFTNjTptWbu2+A==", "dev": true, - "requires": { + "dependencies": { "@algolia/client-common": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "@babel/helper-string-parser": { + "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helper-validator-identifier": { + "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/parser": { + "node_modules/@babel/parser": { "version": "7.29.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, - "requires": { + "dependencies": { "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "@babel/types": { + "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "@docsearch/css": { + "node_modules/@docsearch/css": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", "dev": true }, - "@docsearch/js": { + "node_modules/@docsearch/js": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", "dev": true, - "requires": { + "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, - "@docsearch/react": { + "node_modules/@docsearch/react": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", "dev": true, - "requires": { + "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" - } - }, - "@esbuild/aix-ppc64": { + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/android-arm": { + "node_modules/@esbuild/android-arm": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/android-arm64": { + "node_modules/@esbuild/android-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/android-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/darwin-arm64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/darwin-x64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/freebsd-arm64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/freebsd-x64": { + "node_modules/@esbuild/freebsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-arm": { + "node_modules/@esbuild/linux-arm": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-arm64": { + "node_modules/@esbuild/linux-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-ia32": { + "node_modules/@esbuild/linux-ia32": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-loong64": { + "node_modules/@esbuild/linux-loong64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-mips64el": { + "node_modules/@esbuild/linux-mips64el": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-ppc64": { + "node_modules/@esbuild/linux-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-riscv64": { + "node_modules/@esbuild/linux-riscv64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-s390x": { + "node_modules/@esbuild/linux-s390x": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-x64": { + "node_modules/@esbuild/linux-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/netbsd-x64": { + "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/openbsd-x64": { + "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/sunos-x64": { + "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-arm64": { + "node_modules/@esbuild/win32-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-ia32": { + "node_modules/@esbuild/win32-ia32": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-x64": { + "node_modules/@esbuild/win32-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@iconify-json/simple-icons": { + "node_modules/@iconify-json/simple-icons": { "version": "1.2.80", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.80.tgz", "integrity": "sha512-iglncJJ6X/dVuzFDU32MrHwwo4RBwivGf108dgyYg+HKS78ifx0h7sTenpDZMVT+UhdS6CSgZcvY/SvRXlIEUg==", "dev": true, - "requires": { + "dependencies": { "@iconify/types": "*" } }, - "@iconify/types": { + "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "dev": true }, - "@jridgewell/sourcemap-codec": { + "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, - "@rollup/rollup-android-arm-eabi": { + "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ] }, - "@rollup/rollup-android-arm64": { + "node_modules/@rollup/rollup-android-arm64": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ] }, - "@rollup/rollup-darwin-arm64": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ] }, - "@rollup/rollup-darwin-x64": { + "node_modules/@rollup/rollup-darwin-x64": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ] }, - "@rollup/rollup-freebsd-arm64": { + "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ] }, - "@rollup/rollup-freebsd-x64": { + "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ] }, - "@rollup/rollup-linux-arm-gnueabihf": { + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-arm-musleabihf": { + "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-arm64-gnu": { + "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-arm64-musl": { + "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-loong64-gnu": { + "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-loong64-musl": { + "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-ppc64-gnu": { + "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-ppc64-musl": { + "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-riscv64-gnu": { + "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-riscv64-musl": { + "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-s390x-gnu": { + "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-x64-gnu": { + "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-linux-x64-musl": { + "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "@rollup/rollup-openbsd-x64": { + "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "openbsd" + ] }, - "@rollup/rollup-openharmony-arm64": { + "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "openharmony" + ] }, - "@rollup/rollup-win32-arm64-msvc": { + "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, - "@rollup/rollup-win32-ia32-msvc": { + "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, - "@rollup/rollup-win32-x64-gnu": { + "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, - "@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, - "@shikijs/core": { + "node_modules/@shikijs/core": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", "dev": true, - "requires": { + "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", @@ -621,147 +1073,155 @@ "hast-util-to-html": "^9.0.4" } }, - "@shikijs/engine-javascript": { + "node_modules/@shikijs/engine-javascript": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", "dev": true, - "requires": { + "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^3.1.0" } }, - "@shikijs/engine-oniguruma": { + "node_modules/@shikijs/engine-oniguruma": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", "dev": true, - "requires": { + "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2" } }, - "@shikijs/langs": { + "node_modules/@shikijs/langs": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", "dev": true, - "requires": { + "dependencies": { "@shikijs/types": "2.5.0" } }, - "@shikijs/themes": { + "node_modules/@shikijs/themes": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", "dev": true, - "requires": { + "dependencies": { "@shikijs/types": "2.5.0" } }, - "@shikijs/transformers": { + "node_modules/@shikijs/transformers": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", "dev": true, - "requires": { + "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/types": "2.5.0" } }, - "@shikijs/types": { + "node_modules/@shikijs/types": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", "dev": true, - "requires": { + "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, - "@shikijs/vscode-textmate": { + "node_modules/@shikijs/vscode-textmate": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "dev": true }, - "@types/estree": { + "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, - "@types/hast": { + "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "*" } }, - "@types/linkify-it": { + "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "dev": true }, - "@types/markdown-it": { + "node_modules/@types/markdown-it": { "version": "14.1.2", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, - "requires": { + "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, - "@types/mdast": { + "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "*" } }, - "@types/mdurl": { + "node_modules/@types/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true }, - "@types/unist": { + "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true }, - "@types/web-bluetooth": { + "node_modules/@types/web-bluetooth": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", "dev": true }, - "@ungap/structured-clone": { + "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "deprecated": "Potential CWE-502 - Update to 1.3.1 or higher", "dev": true }, - "@vitejs/plugin-vue": { + "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", - "dev": true + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } }, - "@vue/compiler-core": { + "node_modules/@vue/compiler-core": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", "dev": true, - "requires": { + "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.33", "entities": "^7.0.1", @@ -769,22 +1229,22 @@ "source-map-js": "^1.2.1" } }, - "@vue/compiler-dom": { + "node_modules/@vue/compiler-dom": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", "dev": true, - "requires": { + "dependencies": { "@vue/compiler-core": "3.5.33", "@vue/shared": "3.5.33" } }, - "@vue/compiler-sfc": { + "node_modules/@vue/compiler-sfc": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", "dev": true, - "requires": { + "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.33", "@vue/compiler-dom": "3.5.33", @@ -796,31 +1256,31 @@ "source-map-js": "^1.2.1" } }, - "@vue/compiler-ssr": { + "node_modules/@vue/compiler-ssr": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", "dev": true, - "requires": { + "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/shared": "3.5.33" } }, - "@vue/devtools-api": { + "node_modules/@vue/devtools-api": { "version": "7.7.9", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", "dev": true, - "requires": { + "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, - "@vue/devtools-kit": { + "node_modules/@vue/devtools-kit": { "version": "7.7.9", "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", "dev": true, - "requires": { + "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", @@ -830,106 +1290,173 @@ "superjson": "^2.2.2" } }, - "@vue/devtools-shared": { + "node_modules/@vue/devtools-shared": { "version": "7.7.9", "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", "dev": true, - "requires": { + "dependencies": { "rfdc": "^1.4.1" } }, - "@vue/reactivity": { + "node_modules/@vue/reactivity": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", "dev": true, - "requires": { + "dependencies": { "@vue/shared": "3.5.33" } }, - "@vue/runtime-core": { + "node_modules/@vue/runtime-core": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", "dev": true, - "requires": { + "dependencies": { "@vue/reactivity": "3.5.33", "@vue/shared": "3.5.33" } }, - "@vue/runtime-dom": { + "node_modules/@vue/runtime-dom": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", "dev": true, - "requires": { + "dependencies": { "@vue/reactivity": "3.5.33", "@vue/runtime-core": "3.5.33", "@vue/shared": "3.5.33", "csstype": "^3.2.3" } }, - "@vue/server-renderer": { + "node_modules/@vue/server-renderer": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", "dev": true, - "requires": { + "dependencies": { "@vue/compiler-ssr": "3.5.33", "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "vue": "3.5.33" } }, - "@vue/shared": { + "node_modules/@vue/shared": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", "dev": true }, - "@vueuse/core": { + "node_modules/@vueuse/core": { "version": "12.8.2", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", "dev": true, - "requires": { + "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "@vueuse/integrations": { + "node_modules/@vueuse/integrations": { "version": "12.8.2", "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", "dev": true, - "requires": { + "dependencies": { "@vueuse/core": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" - } - }, - "@vueuse/metadata": { + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { "version": "12.8.2", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", - "dev": true + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } }, - "@vueuse/shared": { + "node_modules/@vueuse/shared": { "version": "12.8.2", "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", "dev": true, - "requires": { + "dependencies": { "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "algoliasearch": { + "node_modules/algoliasearch": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.0.tgz", "integrity": "sha512-0ZzY9mjqV7gop/AH8pIBiAS8giXP7WcSiUfoFYIzYAK9QC5c37E4SIVtJVBMwlURc0/uNt2o4RcNRvdHa4CJ5w==", "dev": true, - "requires": { + "dependencies": { "@algolia/abtesting": "1.18.0", "@algolia/client-abtesting": "5.52.0", "@algolia/client-analytics": "5.52.0", @@ -944,86 +1471,134 @@ "@algolia/requester-browser-xhr": "5.52.0", "@algolia/requester-fetch": "5.52.0", "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "birpc": { + "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", - "dev": true + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } }, - "ccount": { + "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "character-entities-html4": { + "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "character-entities-legacy": { + "node_modules/character-entities-legacy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "comma-separated-tokens": { + "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "copy-anything": { + "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", "dev": true, - "requires": { + "dependencies": { "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" } }, - "csstype": { + "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true }, - "dequal": { + "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "devlop": { + "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "dev": true, - "requires": { + "dependencies": { "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "emoji-regex-xs": { + "node_modules/emoji-regex-xs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", "dev": true }, - "entities": { + "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "esbuild": { + "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, - "requires": { + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", @@ -1049,34 +1624,41 @@ "@esbuild/win32-x64": "0.21.5" } }, - "estree-walker": { + "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, - "focus-trap": { + "node_modules/focus-trap": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, - "requires": { + "dependencies": { "tabbable": "^6.4.0" } }, - "fsevents": { + "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "hast-util-to-html": { + "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", "dev": true, - "requires": { + "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", @@ -1088,56 +1670,74 @@ "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "hast-util-whitespace": { + "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dev": true, - "requires": { + "dependencies": { "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "hookable": { + "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "dev": true }, - "html-void-elements": { + "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "is-what": { + "node_modules/is-what": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", - "dev": true + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } }, - "magic-string": { + "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, - "requires": { + "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "mark.js": { + "node_modules/mark.js": { "version": "8.11.1", "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", "dev": true }, - "mdast-util-to-hast": { + "node_modules/mdast-util-to-hast": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "dev": true, - "requires": { + "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", @@ -1147,147 +1747,248 @@ "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "micromark-util-character": { + "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "dev": true, - "requires": { + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "micromark-util-encode": { + "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "dev": true - }, - "micromark-util-sanitize-uri": { + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "dev": true, - "requires": { + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, - "micromark-util-symbol": { + "node_modules/micromark-util-symbol": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "dev": true - }, - "micromark-util-types": { + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "dev": true - }, - "minisearch": { + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/minisearch": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", "dev": true }, - "mitt": { + "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true }, - "nanoid": { + "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, - "oniguruma-to-es": { + "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", "dev": true, - "requires": { + "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, - "perfect-debounce": { + "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "dev": true }, - "picocolors": { + "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, - "postcss": { + "node_modules/postcss": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", "dev": true, - "requires": { + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "preact": { + "node_modules/preact": { "version": "10.29.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", - "dev": true + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } }, - "property-information": { + "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "regex": { + "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "dev": true, - "requires": { + "dependencies": { "regex-utilities": "^2.3.0" } }, - "regex-recursion": { + "node_modules/regex-recursion": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "dev": true, - "requires": { + "dependencies": { "regex-utilities": "^2.3.0" } }, - "regex-utilities": { + "node_modules/regex-utilities": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "dev": true }, - "rfdc": { + "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, - "rollup": { + "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, - "requires": { + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", @@ -1313,16 +2014,15 @@ "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", - "@types/estree": "1.0.8", "fsevents": "~2.3.2" } }, - "shiki": { + "node_modules/shiki": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", "dev": true, - "requires": { + "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", @@ -1333,141 +2033,237 @@ "@types/hast": "^3.0.4" } }, - "source-map-js": { + "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "space-separated-tokens": { + "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "speakingurl": { + "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "stringify-entities": { + "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "dev": true, - "requires": { + "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "superjson": { + "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", "dev": true, - "requires": { + "dependencies": { "copy-anything": "^4" + }, + "engines": { + "node": ">=16" } }, - "tabbable": { + "node_modules/tabbable": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "dev": true }, - "trim-lines": { + "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "unist-util-is": { + "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "unist-util-position": { + "node_modules/unist-util-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "unist-util-stringify-position": { + "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "unist-util-visit": { + "node_modules/unist-util-visit": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "unist-util-visit-parents": { + "node_modules/unist-util-visit-parents": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "vfile": { + "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "vfile-message": { + "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dev": true, - "requires": { + "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "vite": { + "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, - "requires": { + "dependencies": { "esbuild": "^0.21.3", - "fsevents": "~2.3.3", "postcss": "^8.4.43", "rollup": "^4.20.0" - } - }, - "vitepress": { + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", "dev": true, - "requires": { + "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", @@ -1486,26 +2282,53 @@ "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" - } - }, - "vue": { + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { "version": "3.5.33", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", "dev": true, - "requires": { + "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/compiler-sfc": "3.5.33", "@vue/runtime-dom": "3.5.33", "@vue/server-renderer": "3.5.33", "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "zwitch": { + "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "dev": true + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/docs/providers/dapper.md b/docs/providers/dapper.md index bd29208..c91db6f 100644 --- a/docs/providers/dapper.md +++ b/docs/providers/dapper.md @@ -4,18 +4,19 @@ ## Registration -To use the Dapper extensions, register them in your DI container and specify your target SQL dialect: +Configure the Dapper provider once at startup with the static `FlexQueryDapper.Configure` method: ```csharp using FlexQuery.NET.Dapper; -using FlexQuery.NET.Dapper.Dialects; -builder.Services.AddFlexQueryDapper(opts => { - // Specify the database dialect (SqlServer, Postgres, Sqlite, etc.) - opts.Dialect = new SqlServerDialect(); +FlexQueryDapper.Configure(opts => { + // Configure entity mappings (optional) + opts.Model.Entity().ToTable("app_users"); }); ``` +The SQL dialect is **auto-detected** from the supplied `DbConnection` at runtime. + ## SQL Translation The core of the Dapper package is the `SqlTranslator`. It safely converts expression trees and query options into parameterized SQL strings, completely avoiding SQL injection. diff --git a/docs/providers/dapper/conventions.md b/docs/providers/dapper/conventions.md index f0e1ef5..e44af62 100644 --- a/docs/providers/dapper/conventions.md +++ b/docs/providers/dapper/conventions.md @@ -24,16 +24,16 @@ Dapper, unlike EF Core, has no built-in model metadata. It doesn't know your tab Entity Type (e.g., Customer) │ ▼ -IEntityConvention.Apply() → Resolves table name, column mappings +IEntityConvention.Apply() -> Resolves table name, column mappings │ ▼ -IRelationshipConvention.Apply() → Discovers navigation properties, JOIN metadata +IRelationshipConvention.Apply() -> Discovers navigation properties, JOIN metadata │ │ - │ ├── IPluralizer → "Customer" → "Customers" (table name) - │ └── IForeignKeyConvention → Infers FK column: "CustomerId" + │ ├── IPluralizer -> "Customer" -> "Customers" (table name) + │ └── IForeignKeyConvention -> Infers FK column: "CustomerId" │ ▼ -EntityMapping → Complete mapping ready for SQL generation +EntityMapping -> Complete mapping ready for SQL generation ``` ## Convention Interfaces @@ -61,9 +61,9 @@ public interface IEntityConvention ``` The `DefaultEntityConvention` maps: -- **Table name** → Pluralized class name (e.g., `Customer` → `Customers`) -- **Column names** → Property names (1:1 by default) -- **Primary key** → Property named `Id` or `{TypeName}Id`, or decorated with `[Key]` +- **Table name** -> Pluralized class name (e.g., `Customer` -> `Customers`) +- **Column names** -> Property names (1:1 by default) +- **Primary key** -> Property named `Id` or `{TypeName}Id`, or decorated with `[Key]` ### IPluralizer @@ -76,7 +76,7 @@ public interface IPluralizer } ``` -The `DefaultPluralizer` handles common English pluralization rules (e.g., `Customer` → `Customers`, `Category` → `Categories`, `Person` → `People`). +The `DefaultPluralizer` handles common English pluralization rules (e.g., `Customer` -> `Customers`, `Category` -> `Categories`, `Person` -> `People`). ### IForeignKeyConvention @@ -107,8 +107,8 @@ internal interface IRelationshipConvention ``` The `DefaultRelationshipConvention` inspects navigation properties: -- **Single reference** (e.g., `public Customer Customer { get; set; }`) → One-to-one or many-to-one -- **Collection** (e.g., `public List Orders { get; set; }`) → One-to-many +- **Single reference** (e.g., `public Customer Customer { get; set; }`) -> One-to-one or many-to-one +- **Collection** (e.g., `public List Orders { get; set; }`) -> One-to-many ## Default Convention Behavior @@ -137,7 +137,7 @@ The conventions produce: | Entity | Table Name | Primary Key | FK Column | |--------|-----------|-------------|-----------| | `Customer` | `Customers` | `Id` | — | -| `Order` | `Orders` | `Id` | `CustomerId` (→ `Customers.Id`) | +| `Order` | `Orders` | `Id` | `CustomerId` (references `Customers.Id`) | ## Assembly Scanning @@ -154,7 +154,11 @@ This scans for public, non-abstract classes that have: ## Fluent Builder Overrides -When conventions don't match your schema, use the fluent API: +When conventions don't match your schema, you can use the fluent API to configure entities explicitly. + +### Inline Configuration + +You can configure entities directly on the options builder: ```csharp opts.Entity() @@ -163,6 +167,34 @@ opts.Entity() .HasMany(c => c.Orders, "customer_id"); // Custom FK column ``` +### `IEntityTypeConfiguration` + +For larger projects, configuring all entities inline becomes messy. Dapper now provides an EF Core-style `IEntityTypeConfiguration` interface to encapsulate mapping logic per entity. + +```csharp +public class CustomerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("tbl_customers") + .HasKey(c => c.Id); + + builder.Property(c => c.Name, "customer_name"); + builder.HasMany(c => c.Orders, "customer_id"); + } +} +``` + +You can then apply these configurations individually or scan an entire assembly: + +```csharp +// Apply individually +opts.ApplyConfiguration(new CustomerConfiguration()); + +// Or scan an assembly to apply all IEntityTypeConfiguration classes automatically +opts.ApplyConfigurationsFromAssembly(typeof(CustomerConfiguration).Assembly); +``` + Explicit configuration always takes precedence over conventions. ## Custom Conventions diff --git a/docs/providers/dapper/dialects.md b/docs/providers/dapper/dialects.md index 3d0a60d..3770d21 100644 --- a/docs/providers/dapper/dialects.md +++ b/docs/providers/dapper/dialects.md @@ -4,10 +4,27 @@ FlexQuery's Dapper provider supports six SQL dialects out of the box. Each dialect encapsulates all database-specific SQL generation concerns — identifier quoting, parameter syntax, pagination, boolean literals, and string concatenation — behind the `ISqlDialect` interface. +The SQL dialect is **auto-detected at runtime** from the supplied `DbConnection` via `SqlDialectResolver`. There is no manual dialect configuration — the connection type is the single source of truth. + ### Why Dialects Matter SQL is not truly standard. `SELECT TOP 10` works in SQL Server but fails in PostgreSQL. `[Column]` is valid quoting in SQL Server but a syntax error in MySQL. `LIMIT/OFFSET` is PostgreSQL syntax but SQL Server uses `OFFSET/FETCH`. The dialect system ensures that FlexQuery generates correct SQL for your specific database without any manual intervention. +### How Dialect Resolution Works + +When a query is executed, `SqlDialectResolver.Resolve(connection)` inspects the connection's type name and returns the matching dialect: + +| Connection Type | Resolved Dialect | +|---|---| +| `SqlConnection` | `SqlServerDialect` | +| `NpgsqlConnection` | `PostgreSqlDialect` | +| `SqliteConnection` | `SqliteDialect` | +| `MySqlConnection` | `MySqlDialect` | +| `MariaDbConnection` | `MariaDbDialect` | +| `OracleConnection` | `OracleDialect` | + +An unrecognized connection type throws `NotSupportedException`. + ## Dialect Comparison | Feature | SQL Server | PostgreSQL | MySQL | MariaDB | SQLite | Oracle | @@ -23,9 +40,7 @@ SQL is not truly standard. `SELECT TOP 10` works in SQL Server but fails in Post ## SQL Server -```csharp -opts.Dialect = new SqlServerDialect(); -``` +When using `SqlConnection`, SQL Server syntax is generated. **Identifier quoting:** Square brackets `[Column]` @@ -43,9 +58,7 @@ OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY ## PostgreSQL -```csharp -opts.Dialect = new PostgreSqlDialect(); -``` +When using `NpgsqlConnection`, PostgreSQL syntax is generated. **Identifier quoting:** Double quotes `"Column"` — PostgreSQL is case-sensitive with quoted identifiers @@ -63,9 +76,7 @@ LIMIT :PageSize OFFSET :Offset ## MySQL -```csharp -opts.Dialect = new MySqlDialect(); -``` +When using `MySqlConnection`, MySQL syntax is generated. **Identifier quoting:** Backticks `` `Column` `` @@ -84,9 +95,7 @@ CONCAT(`FirstName`, ' ', `LastName`) ## MariaDB -```csharp -opts.Dialect = new MariaDbDialect(); -``` +When using `MariaDbConnection`, MariaDB syntax is generated. MariaDB is **not** a drop-in replacement for MySQL. While the quoting and parameter syntax are identical, MariaDB has its own versioning, features, and behaviors that can diverge. This dedicated dialect ensures correct SQL generation for MariaDB-specific edge cases. @@ -94,9 +103,7 @@ MariaDB is **not** a drop-in replacement for MySQL. While the quoting and parame ## SQLite -```csharp -opts.Dialect = new SqliteDialect(); -``` +When using `SqliteConnection`, SQLite syntax is generated. **Identifier quoting:** Double quotes (ANSI SQL style) @@ -119,9 +126,7 @@ LIMIT @PageSize OFFSET @Offset ## Oracle -```csharp -opts.Dialect = new OracleDialect(); -``` +When using `OracleConnection`, Oracle syntax is generated. **Identifier quoting:** Double quotes — Oracle uppercases unquoted identifiers by default @@ -138,22 +143,10 @@ OFFSET :Offset ROWS FETCH NEXT :PageSize ROWS ONLY - No native BOOLEAN type in SQL — uses `1`/`0` - The `:` parameter prefix is used by ODP.NET -## Automatic Dialect Resolution - -If you don't want to hardcode a dialect, FlexQuery can auto-detect it from the `DbConnection` type: - -```csharp -// The DefaultSqlDialectResolver inspects the connection type name -var dialect = DapperQueryOptions.GlobalDialectResolver.Resolve(connection); -``` - -The resolution priority is: -1. **Explicit `Dialect` property** on `DapperQueryOptions` -2. **`GlobalDefaultDialect`** static property -3. **`GlobalDialectResolver`** which inspects the connection type - ## Custom Dialects +### Implement a New Dialect + Implement `ISqlDialect` for unsupported databases: ```csharp @@ -181,12 +174,23 @@ public class CockroachDbDialect : ISqlDialect } ``` +### Register the Custom Dialect + +The `SqlDialectResolver` is an internal static class. To add support for a custom connection type, extend the resolution logic by adding a new `if` branch: + +```csharp +// Extend SqlDialectResolver (internal) — or fork the resolver for your application +if (typeName.Contains("CockroachDbConnection", StringComparison.OrdinalIgnoreCase)) + return new CockroachDbDialect(); +``` + +If you need full control over resolution, you can implement your own resolution logic and pass the dialect directly to `SqlTranslator`. + ## Best Practices -1. **Match the dialect to your actual database** — Using `SqlServerDialect` against PostgreSQL will generate invalid SQL -2. **Set the dialect once globally** for single-database applications via `DapperQueryOptions.GlobalDefaultDialect` -3. **Use SQLite dialect for tests** — SQLite in-memory databases are fast and disposable -4. **Test pagination with each dialect** — OFFSET/FETCH vs LIMIT/OFFSET edge cases can surface in production +1. **Use a supported provider** — The resolver supports SQL Server, PostgreSQL, SQLite, MySQL, MariaDB, and Oracle out of the box. Unsupported providers throw `NotSupportedException`. +2. **Use SQLite for tests** — SQLite in-memory databases are fast and disposable; the resolver auto-detects `SqliteConnection` +3. **Test pagination with each dialect** — OFFSET/FETCH vs LIMIT/OFFSET edge cases can surface in production ## Related Features diff --git a/docs/providers/dapper/getting-started.md b/docs/providers/dapper/getting-started.md index 4bebc8d..49946f4 100644 --- a/docs/providers/dapper/getting-started.md +++ b/docs/providers/dapper/getting-started.md @@ -35,46 +35,21 @@ This package depends on `FlexQuery.NET` (core) and `Dapper`. ## Registration -Register the Dapper provider in your DI container: +Configure the Dapper provider once at startup with the static `FlexQueryDapper.Configure` method: ```csharp using FlexQuery.NET.Dapper; -using FlexQuery.NET.Dapper.Dialects; -builder.Services.AddFlexQueryDapper(opts => +FlexQueryDapper.Configure(opts => { - // Required: specify your database dialect - opts.Dialect = new SqlServerDialect(); + // Configure entity mappings (optional — conventions will be used by default) + opts.Model.Entity().ToTable("app_users"); }); ``` -The `AddFlexQueryDapper` method registers `DapperQueryOptions` as a singleton in the service container, making it available for injection into your controllers and services. +The `FlexQueryDapper.Configure` method builds the entity mapping model and stores it internally as the global runtime model. No DI registration is needed. -## Dialect Configuration - -Every database has slightly different SQL syntax. The dialect tells the translator how to quote identifiers, build pagination clauses, and format parameters: - -```csharp -// SQL Server -opts.Dialect = new SqlServerDialect(); - -// PostgreSQL -opts.Dialect = new PostgreSqlDialect(); - -// MySQL -opts.Dialect = new MySqlDialect(); - -// MariaDB (not a MySQL drop-in!) -opts.Dialect = new MariaDbDialect(); - -// SQLite (great for testing) -opts.Dialect = new SqliteDialect(); - -// Oracle (12c+) -opts.Dialect = new OracleDialect(); -``` - -See the [Dialects](/providers/dapper/dialects) page for complete details on each dialect. +The SQL dialect is **auto-detected** from the supplied `DbConnection` at runtime — no manual dialect configuration is required. ## Your First Query @@ -92,7 +67,6 @@ public async Task GetUsers( var result = await connection.FlexQueryAsync(parameters, opts => { - opts.Dialect = new SqlServerDialect(); opts.AllowedFields = new HashSet { "Id", "Name", "Email", "CreatedAt" }; opts.MaxPageSize = 100; }); @@ -109,10 +83,10 @@ When composing with adapters (AG Grid, MiniOData) or building queries programmat // Step 1: Parse from any source var options = AgGridQueryOptionsParser.Parse(agGridRequest); -// Step 2: Execute with Dapper +// Step 2: Execute with Dapper (dialect is auto-detected from the connection) var result = await connection.FlexQueryAsync(options, opts => { - opts.Dialect = new SqlServerDialect(); + opts.AllowedFields = new HashSet { "Id", "Name" }; }); ``` @@ -123,7 +97,6 @@ For full control over all execution options: ```csharp var dapperOptions = new DapperQueryOptions { - Dialect = new PostgreSqlDialect(), CommandTimeoutSeconds = 60, AllowedFields = new HashSet { "Id", "Name" }, StrictFieldValidation = true, @@ -182,9 +155,9 @@ Data.Count = groups returned in the current page ## Best Practices -1. **Always specify a dialect** — The translator cannot generate correct SQL without one +1. **The SQL dialect is auto-detected from the `DbConnection`** — no manual dialect configuration is needed 2. **Use `AllowedFields`** — Dapper generates raw SQL; restricting fields is critical for security -3. **Set `CommandTimeoutSeconds`** for complex queries — The default is 30 seconds +3. **Set `CommandTimeoutSeconds` for complex queries** — The default is 30 seconds 4. **Prefer `FlexQueryParameters` overloads** for API endpoints — They handle parsing and validation automatically 5. **Use `ScanEntitiesFromAssembly`** during startup — Avoid lazy mapping discovery in hot paths 6. **Reuse connections** — FlexQuery does not manage connection lifecycle; follow Dapper's connection pooling best practices @@ -193,7 +166,7 @@ Data.Count = groups returned in the current page | Pitfall | Solution | |---------|----------| -| Forgetting to set a dialect | Throws at runtime. Always configure `opts.Dialect` | +| Using an unsupported database provider | Throws `NotSupportedException`. Supported: SQL Server, PostgreSQL, SQLite, MySQL, MariaDB, Oracle | | Property names don't match column names | Use `Entity().Property()` for explicit mapping | | N+1 queries with includes | Dapper includes generate JOINs — no N+1, but watch for Cartesian explosion with multiple collections | | Connection not opened before query | Call `await connection.OpenAsync()` before `FlexQueryAsync` | diff --git a/docs/providers/dapper/relationship-queries.md b/docs/providers/dapper/relationship-queries.md index 296f1e8..49e56d5 100644 --- a/docs/providers/dapper/relationship-queries.md +++ b/docs/providers/dapper/relationship-queries.md @@ -33,7 +33,7 @@ Multiple includes generate multiple JOINs: ## Filtered Includes → LEFT JOIN + WHERE-like condition -Filtered includes translate the inline JQL filter into a WHERE condition applied inside the JOIN predicate (or as an additional WHERE clause scoped to that join): +Filtered includes translate the inline FQL filter into a WHERE condition applied inside the JOIN predicate (or as an additional WHERE clause scoped to that join): ``` ?include=Orders(Status = 'Active' AND Total > 100) @@ -119,10 +119,8 @@ When using standard (non-flat) includes, Dapper returns dynamic rows which are t ```csharp // This is handled automatically by FlexQueryAsync -var result = await connection.FlexQueryAsync(parameters, opts => -{ - opts.Dialect = new SqlServerDialect(); -}); +// The SQL dialect is auto-detected from the DbConnection type. +var result = await connection.FlexQueryAsync(parameters); // result.Data[0].Orders is populated from the JOIN result ``` diff --git a/docs/providers/dapper/sql-generation.md b/docs/providers/dapper/sql-generation.md index 9c9ee71..38d371d 100644 --- a/docs/providers/dapper/sql-generation.md +++ b/docs/providers/dapper/sql-generation.md @@ -8,6 +8,8 @@ The SQL generation pipeline is the core of `FlexQuery.NET.Dapper`. It transforms The `SqlTranslator` class is an orchestrator that resolves entity mappings, builds a selection tree, and delegates to specialized builders for each SQL clause. The result is a `SqlCommand` object containing the SQL string and a dictionary of named parameters. +> **Dialect auto-detection:** The SQL dialect is resolved at runtime from the supplied `DbConnection` by `SqlDialectResolver`. No manual dialect configuration is needed — the connection type determines whether SQL Server, PostgreSQL, MySQL, MariaDB, SQLite, or Oracle syntax is generated. + ### Why It Exists Manually building SQL WHERE clauses from dynamic user input is tedious, error-prone, and a security risk. The SQL generation pipeline handles operator translation, parameter naming, identifier quoting, JOIN generation, and dialect-specific syntax — all from the same `QueryOptions` model that EF Core uses. @@ -139,11 +141,10 @@ var parameters = new FlexQueryParameters Select = "Id,Name,Email" }; -var result = await connection.FlexQueryAsync(parameters, opts => -{ - opts.Dialect = new SqlServerDialect(); -}); +var result = await connection.FlexQueryAsync(parameters); +// The SQL dialect is auto-detected from the connection's type. +// For a SqlConnection, SQL Server syntax is generated: // Generated SQL: // SELECT [Id], [Name], [Email] // FROM [Users] diff --git a/docs/providers/ef-core.md b/docs/providers/ef-core.md index bef2397..6107543 100644 --- a/docs/providers/ef-core.md +++ b/docs/providers/ef-core.md @@ -95,7 +95,7 @@ query .ThenInclude(o => o.OrderItems.Where(i => i.Quantity > 5)) ``` -### Using ApplyFilteredIncludes +### Using ApplyExpand If you need to apply includes separately from the main query: @@ -134,12 +134,14 @@ For nested projections with includes: FlexQuery supports grand total aggregation via LINQ: ```csharp +using FlexQuery.NET.Models.Aggregates; + var options = parameters.ToQueryOptions(); options.Aggregates = new List { - new() { Function = "sum", Field = "Price", Alias = "priceSum" }, - new() { Function = "avg", Field = "Price", Alias = "priceAvg" }, - new() { Function = "count", Field = "Id", Alias = "idCount" } + new() { Function = AggregateFunction.Sum, Field = "Price", Alias = "priceSum" }, + new() { Function = AggregateFunction.Avg, Field = "Price", Alias = "priceAvg" }, + new() { Function = AggregateFunction.Count, Field = "Id", Alias = "idCount" } }; var result = await _context.Products.FlexQueryAsync(options); @@ -150,18 +152,47 @@ var result = await _context.Products.FlexQueryAsync(options); When `GroupBy` is specified alongside aggregates, the pipeline uses `GroupByBuilder` to generate grouped aggregation queries that EF Core translates into SQL `GROUP BY` with aggregate functions. -## UseNoTracking +## EF Core Query Options -For read-only API endpoints (which is most query endpoints), disable EF Core's change tracker: +The EF Core provider introduces `EfCoreQueryOptions`, which allows you to control query compilation behavior per-request without modifying the underlying `DbContext`. ```csharp var result = await _context.Products.FlexQueryAsync(parameters, opts => { - opts.UseNoTracking = true; // Adds .AsNoTracking() to the query + opts.UseNoTracking = true; // Adds .AsNoTracking() }); ``` -**Performance impact:** Disabling change tracking can improve query performance by 20-40% for large result sets because EF Core skips identity resolution and snapshot creation. +### UseNoTracking +Disabling change tracking can improve query performance by 20-40% for large result sets because EF Core skips identity resolution and snapshot creation. Recommended for all read-only API endpoints. + +### Split Query Optimization +When your query includes multiple collection navigations, EF Core normally generates a single massive SQL query with multiple `LEFT JOIN`s, which can cause a "cartesian explosion" of data transfer. Configure EF Core's native query splitting behavior on the `DbContext` level: + +```csharp +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +{ + optionsBuilder.UseSqlServer(connectionString, opt => + opt.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); +} +``` + +> [!NOTE] +> The `UseSplitQuery` option was removed from FlexQuery options in v4. Use EF Core's native query splitting configuration instead. + +### IgnoreAutoIncludes & IgnoreQueryFilters +These options are not available in FlexQuery v4. To bypass globally configured EF Core behaviors, configure them directly on the `DbContext` before passing the queryable to `FlexQueryAsync`: + +```csharp +var query = _context.Products + .IgnoreAutoIncludes() + .IgnoreQueryFilters(); + +var result = await query.FlexQueryAsync(parameters, opts => +{ + opts.UseNoTracking = true; +}); +``` ## ToSqlPreview diff --git a/docs/shared/operators.md b/docs/shared/operators.md index e8c4412..730df05 100644 --- a/docs/shared/operators.md +++ b/docs/shared/operators.md @@ -177,19 +177,19 @@ GET /api/users?filter=orders:count:gt:5 WHERE (SELECT COUNT(*) FROM Orders o WHERE o.UserId = u.Id) > 5 ``` -### Nested any (JQL) +### Nested any (FQL) ``` -GET /api/users?query=Orders.any(Status = "shipped" AND Amount > 100) +GET /api/users?filter=Orders.any(Status = "shipped" AND Amount > 100) ``` --- -## JQL Operator Syntax +## FQL Operator Syntax -JQL uses natural language symbols: +FQL uses natural language symbols: -| JQL Syntax | Operator | +| FQL Syntax | Operator | | :--- | :--- | | `=` | eq | | `!=` | neq | @@ -201,12 +201,12 @@ JQL uses natural language symbols: | `startsWith(field, "val")` | startswith | | `.any(...)` | any (collection) | -**JQL examples:** +**FQL examples:** ``` -GET /api/users?query=status = "active" AND age >= 18 -GET /api/users?query=(name = "alice" OR name = "bob") AND status = "active" -GET /api/users?query=Orders.any(Status = "shipped") +GET /api/users?filter=status = "active" AND age >= 18 +GET /api/users?filter=(name = "alice" OR name = "bob") AND status = "active" +GET /api/users?filter=Orders.any(Status = "shipped") ``` --- diff --git a/docs/shared/query-language.md b/docs/shared/query-language.md index cbb25cc..a7333f7 100644 --- a/docs/shared/query-language.md +++ b/docs/shared/query-language.md @@ -1,6 +1,6 @@ # Query Language Reference -FlexQuery.NET supports four query input formats. The parser **auto-detects** the format — you do not need to specify which one you are using. +FlexQuery.NET supports two query input formats. The parser is selected explicitly per request via `QuerySyntax` — no auto-detection. --- @@ -37,28 +37,28 @@ filter=status:eq:active,status:eq:pending&logic=or --- -## Format 2: JQL (SQL-like) +## Format 2: FQL (SQL-like) -Natural language SQL-like syntax using the `query` parameter. +Natural language SQL-like syntax. FQL supports all query parameters, not just filters. ``` -query=expression +filter=expression ``` -**Examples:** +**Filter examples:** ``` -query=status = "active" -query=age >= 18 -query=name = "alice" OR name = "bob" -query=(status = "active" OR status = "pending") AND age >= 18 -query=Orders.any(Status = "shipped") -query=Orders.any(Status = "shipped" AND Amount > 100) +filter=status = "active" +filter=age >= 18 +filter=name = "alice" OR name = "bob" +filter=(status = "active" OR status = "pending") AND age >= 18 +filter=Orders.any(Status = "shipped") +filter=Orders.any(Status = "shipped" AND Amount > 100) ``` -**Supported operators:** +**FQL supported operators:** -| JQL | Meaning | +| FQL | Meaning | | :--- | :--- | | `=` | eq | | `!=` | neq | @@ -66,73 +66,58 @@ query=Orders.any(Status = "shipped" AND Amount > 100) | `>=` | gte | | `<` | lt | | `<=` | lte | -| `contains(f, v)` | contains | -| `startsWith(f, v)` | startswith | +| `CONTAINS` | contains | +| `STARTSWITH` | startswith | +| `ENDSWITH` | endswith | +| `IN (...)` | in | +| `LIKE` | like | +| `BETWEEN x AND y` | between | +| `IS NULL` | isnull | +| `IS NOT NULL` | isnotnull | | `.any(...)` | any on collection | | `.all(...)` | all on collection | +| `[...]` | bracket syntax for any | +| `COUNT` | count on collection | ---- - -## Format 3: JSON - -Structured nested filter tree. - +**FQL sort:** ``` -filter={"logic":"and","filters":[...]} +sort=Name ASC +sort=CreatedDate DESC +sort=Customer.Name ASC, CreatedDate DESC ``` -**Examples:** - -Simple: -```json -{"logic":"and","filters":[ - {"field":"status","operator":"eq","value":"active"}, - {"field":"age","operator":"gte","value":"18"} -]} -``` - -Nested OR: -```json -{ - "logic": "and", - "filters": [ - { "field": "status", "operator": "eq", "value": "active" }, - { - "logic": "or", - "filters": [ - { "field": "name", "operator": "contains", "value": "alice" }, - { "field": "name", "operator": "contains", "value": "bob" } - ] - } - ] -} +**FQL aggregates:** +``` +aggregates=SUM(Amount) +aggregates=SUM(Amount) AS TotalSales +aggregates=COUNT(*) +aggregates=AVG(Price), MIN(Date), MAX(Date) ``` ---- - -## Format 4: Indexed (Form-based) - -Array-indexed parameters for form submissions. +**FQL having:** +``` +having=COUNT(*) > 5 +having=SUM(Amount) > 1000 +having=AVG(Price) >= 500 +``` +**FQL groupBy:** ``` -filter[0].field=status -filter[0].operator=eq -filter[0].value=active -filter[1].field=age -filter[1].operator=gte -filter[1].value=18 -logic=and +groupBy=Department +groupBy=Department,Category +groupBy=Customer.Region ``` --- ## Sort Syntax +**DSL (colon-delimited):** ``` sort=field:direction,field:direction ``` -| Example | Meaning | +| DSL Example | Meaning | | :--- | :--- | | `sort=name:asc` | Sort by name ascending | | `sort=createdAt:desc` | Sort by createdAt descending | @@ -141,6 +126,14 @@ sort=field:direction,field:direction | `sort=orders.count():desc` | Sort by collection count | | `sort=orders.sum(amount):desc` | Sort by collection sum | +**FQL (SQL-inspired):** +``` +sort=Name ASC +sort=Name DESC +sort=Customer.Name ASC, CreatedDate DESC +sort=name (defaults to ASC) +``` + --- ## Select Syntax @@ -151,11 +144,45 @@ select=field1,field2,nested.field | Example | Meaning | | :--- | :--- | -| `select=id,name,email` | Top-level fields | -| `select=id,name,profile.bio` | Nested path | +| `select=Id,Name,Email` | Top-level fields | +| `select=Id,Name,Profile.Bio` | Nested path | | `select=*` | All fields (wildcard) | -| `select=status,count()` | Field + aggregate | -| `select=status,sum(amount)` | Field + sum aggregate | + +Note: Aggregates are no longer specified in the `select` parameter. Use the dedicated `aggregates` parameter instead. + +--- + +## Aggregates Syntax + +**DSL:** +``` +aggregates=Field:Function[:Alias] +``` + +| DSL Example | Generated Alias | Meaning | +| :--- | :--- | :--- | +| `Amount:sum` | `AmountSum` | Sum of Amount | +| `Amount:sum:TotalSales` | `TotalSales` | Sum with explicit alias | +| `Price:avg` | `PriceAvg` | Average of Price | +| `*:count` | `Count` | Count all rows | +| `Date:min,Date:max` | `DateMin`, `DateMax` | Min and max | + +**FQL:** +``` +aggregates=FUNCTION(Field) [AS Alias] +``` + +| FQL Example | Generated Alias | Meaning | +| :--- | :--- | :--- | +| `SUM(Amount)` | `AmountSum` | Sum of Amount | +| `SUM(Amount) AS TotalSales` | `TotalSales` | Sum with explicit alias | +| `COUNT(*)` | `Count` | Count all rows | +| `AVG(Price)` | `PriceAvg` | Average of Price | +| `MIN(Date), MAX(Date)` | `DateMin`, `DateMax` | Min and max | + +Both DSL and FQL support: `SUM`, `COUNT`, `AVG`, `MIN`, `MAX`. + +Function names are case-insensitive. Auto-generated aliases follow PascalCase convention. --- @@ -165,20 +192,40 @@ select=field1,field2,nested.field groupBy=field1,field2 ``` +| Example | Meaning | +| :--- | :--- | +| `groupBy=Department` | Single field | +| `groupBy=Department,Category` | Multiple fields | +| `groupBy=Customer.Region` | Nested property | + --- ## Having Syntax +**DSL:** ``` having=function(field):operator:value ``` -| Example | Meaning | +| DSL Example | Meaning | | :--- | :--- | -| `having=count():gt:5` | Groups with count > 5 | -| `having=sum(amount):gte:1000` | Groups with sum >= 1000 | +| `having=count:gt:5` | Groups with count > 5 | +| `having=sum(total):gt:1000` | Groups with sum > 1000 | | `having=avg(amount):between:100,500` | Groups with avg in range | +**FQL:** +``` +having=FUNCTION(Field) OPERATOR value +``` + +| FQL Example | Meaning | +| :--- | :--- | +| `having=COUNT(*) > 5` | Groups with count > 5 | +| `having=SUM(Amount) > 1000` | Groups with sum > 1000 | +| `having=AVG(Price) >= 500` | Groups with avg >= 500 | + +Supported HAVING operators: `=`, `!=`, `>`, `>=`, `<`, `<=` (FQL); `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between` (DSL). + --- ## Include Syntax @@ -214,4 +261,4 @@ include=Navigation(filter).NestedNavigation | :--- | :--- | :--- | | `mode` | `nested`, `flat`, `flat-mixed` | Projection output shape | | `distinct` | `true`, `false` | Apply DISTINCT | -| `logic` | `and`, `or` | Top-level filter logic (Indexed format) | +| `logic` | `and`, `or` | Top-level filter logic | diff --git a/src/FlexQuery.NET.Adapters.AgGrid/README.md b/src/FlexQuery.NET.Adapters.AgGrid/README.md index 8a836ff..9e0b844 100644 --- a/src/FlexQuery.NET.Adapters.AgGrid/README.md +++ b/src/FlexQuery.NET.Adapters.AgGrid/README.md @@ -26,7 +26,7 @@ public async Task GetGridData([FromBody] AgGridRequest request) { var options = request.ToQueryOptions(); - var result = await _context.Products.FlexQueryAsync(options, opts => + var result = await _context.Products.FlexQueryAsync(options, opts => { opts.AllowedFields = new HashSet { "Id", "Name", "Price", "Category" }; }); diff --git a/src/FlexQuery.NET.Adapters.Kendo/README.md b/src/FlexQuery.NET.Adapters.Kendo/README.md index d7eaf94..3dad182 100644 --- a/src/FlexQuery.NET.Adapters.Kendo/README.md +++ b/src/FlexQuery.NET.Adapters.Kendo/README.md @@ -24,7 +24,7 @@ public async Task GetGridData([FromBody] KendoRequest request) { var options = request.ToQueryOptions(); - var result = await _context.Products.FlexQueryAsync(options, opts => + var result = await _context.Products.FlexQueryAsync(options, opts => { opts.AllowedFields = new HashSet { "Id", "Name", "Price", "Category" }; }); diff --git a/src/FlexQuery.NET.AspNetCore/README.md b/src/FlexQuery.NET.AspNetCore/README.md index 2462db2..69ee86f 100644 --- a/src/FlexQuery.NET.AspNetCore/README.md +++ b/src/FlexQuery.NET.AspNetCore/README.md @@ -17,10 +17,6 @@ dotnet add package FlexQuery.NET.AspNetCore ## Registration ```csharp -builder.Services.AddFlexQuery(); -builder.Services.AddFlexQuerySecurity(); - -// Or combine with MVC: builder.Services.AddControllers() .AddFlexQuerySecurity(); ``` @@ -37,8 +33,10 @@ public class UsersController : ControllerBase MaxFieldDepth = 2)] public async Task GetUsers([FromQuery] FlexQueryParameters parameters) { - // FieldAccess settings are automatically resolved from HttpContext. - var result = await _context.Users.FlexQueryAsync(parameters, HttpContext); + var options = HttpContext.GetFlexQueryExecutionOptions(); + options.AllowedFields = new HashSet { "Id", "Name", "Email", "Status" }; + + var result = await _context.Users.FlexQueryAsync(parameters, options); return Ok(result); } } @@ -48,7 +46,7 @@ public class UsersController : ControllerBase - **`[FieldAccess]` Attribute** — Declare Allowed, Blocked, Filterable, Sortable, Selectable, Groupable, Aggregatable fields per-endpoint - **`FieldAccessFilter`** — Action filter that applies attribute settings to `QueryExecutionOptions` -- **`Automatic Security Resolution** — Extension method accepting `HttpContext` for automatic options resolution +- **`GetFlexQueryExecutionOptions`** — Extension method on `HttpContext` to retrieve `QueryExecutionOptions` populated by the `FieldAccessFilter` - **Swagger Integration** — Works with Swagger/Swashbuckle for API documentation ## Related Packages diff --git a/src/FlexQuery.NET.Dapper/README.md b/src/FlexQuery.NET.Dapper/README.md index 938c958..ad6528b 100644 --- a/src/FlexQuery.NET.Dapper/README.md +++ b/src/FlexQuery.NET.Dapper/README.md @@ -18,26 +18,25 @@ dotnet add package FlexQuery.NET.Dapper ```csharp using FlexQuery.NET.Dapper; -using FlexQuery.NET.Dapper.Dialects; -builder.Services.AddFlexQueryDapper(options => +FlexQueryDapper.Configure(options => { - options.UseSqlServer(); + options.CommandTimeout = 60; }); ``` Or configure options per-query: ```csharp -options.Dialect = new PostgreSqlDialect(); -options.CommandTimeoutSeconds = 60; +options.CommandTimeout = 60; ``` ## Quick Start +## Quick Start + ```csharp using FlexQuery.NET.Dapper; -using FlexQuery.NET.Dapper.Dialects; [HttpGet("users")] public async Task GetUsers([FromQuery] FlexQueryParameters parameters) @@ -46,9 +45,6 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame var result = await connection.FlexQueryAsync(parameters, options => { - // Or register the dialect globally using AddFlexQueryDapper() - options.Dialect = new SqlServerDialect(); - options.AllowedFields = new HashSet { "Id", "Name", "Email" }; }); @@ -59,11 +55,10 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame ## Features - **SQL Generation** — `SqlTranslator` produces parameterized, injection-safe SQL -- **Dialect Support** — SQL Server, PostgreSQL, MySQL, SQLite via `ISqlDialect` +- **Dialect Support** — SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, Oracle via `ISqlDialect` +- **Auto-Dialect Detection** — Dialect is resolved automatically from the `DbConnection` type at runtime - **Flat Projection** — Deep select paths (e.g., `Orders.Total`) become `LEFT JOIN` with flattened aliases -- **Optional Auto-Dialect Detection** — `ISqlDialectResolver` can detect dialect from the `DbConnection` - **Entity Mapping** — Fluent entity-to-table mapping via `DapperQueryOptions.Entity()` -- **Diagnostics** — Pass `Action` to observe pipeline stages ## Known Limitations diff --git a/src/FlexQuery.NET.Diagnostics/README.md b/src/FlexQuery.NET.Diagnostics/README.md index b25d1a7..12c2dfd 100644 --- a/src/FlexQuery.NET.Diagnostics/README.md +++ b/src/FlexQuery.NET.Diagnostics/README.md @@ -29,9 +29,7 @@ var collector = new FlexQueryDiagnosticsCollector(); var result = await _context.Users.FlexQueryAsync(parameters, options => { options.AllowedFields = new HashSet { "Id", "Name" }; -}, execution => -{ - execution.Listener = collector; + options.Listener = collector; }); var report = collector.BuildReport(); diff --git a/src/FlexQuery.NET.EntityFrameworkCore/README.md b/src/FlexQuery.NET.EntityFrameworkCore/README.md index c1ae58d..b209a47 100644 --- a/src/FlexQuery.NET.EntityFrameworkCore/README.md +++ b/src/FlexQuery.NET.EntityFrameworkCore/README.md @@ -35,13 +35,12 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame ## Features -- **FlexQueryAsync** — Unified parse-validate-execute pipeline with configurable `QueryExecutionOptions` -- **Filtered Includes** — Include related collections with inline WHERE filters via `ApplyFilteredIncludes()` +- **FlexQueryAsync** — Unified parse-validate-execute pipeline with `EfCoreQueryOptions` +- **Expanded Includes** — Include related collections with inline WHERE filters via `ApplyExpand()` - **Projection** — Nested, Flat, and FlatMixed projection modes -- **Execution Options** — `UseSplitQuery` to avoid cartesian explosion, `UseNoTracking` for read-only queries -- **SQL Preview** — `ToSqlPreview()` to inspect the generated SQL without executing -- **Diagnostics** — Pass `Action` to observe pipeline stages -- **EF Core Operators** — `UseEfCoreOperators()` to register `LIKE` operator handler for EF Core translation +- **Keyset Pagination** — High-performance cursor-based pagination +- **Execution Options** — `UseNoTracking` for read-only queries +- **Static Configuration** — Configure via `FlexQueryEFCore.Configure()` or `FlexQueryEFCore.Setup()` at startup ## Related Packages diff --git a/src/FlexQuery.NET.Parsers.Fql/README.md b/src/FlexQuery.NET.Parsers.Fql/README.md index ee568e2..461c00e 100644 --- a/src/FlexQuery.NET.Parsers.Fql/README.md +++ b/src/FlexQuery.NET.Parsers.Fql/README.md @@ -2,11 +2,11 @@ [![NuGet Version](https://img.shields.io/nuget/v/FlexQuery.NET.Parsers.Fql.svg)](https://www.nuget.org/packages/FlexQuery.NET.Parsers.Fql) -FQL (FlexQuery Language) parser for FlexQuery.NET. +FQL (FlexQuery Query Language) is a SQL-inspired query language for FlexQuery.NET. -## When to Use This Package +It provides an expressive and familiar syntax for filtering, sorting, grouping, aggregation, and projection while integrating seamlessly with the FlexQuery.NET query pipeline. -Install this package when you want to support Fql-style filter syntax in your API. The Fql parser integrates with the `QueryOptionsParser` pipeline so Fql queries are automatically detected and parsed alongside the native DSL format. +--- ## Installation @@ -14,47 +14,246 @@ Install this package when you want to support Fql-style filter syntax in your AP dotnet add package FlexQuery.NET.Parsers.Fql ``` -## Quick Start +## Registration ```csharp -using FlexQuery.NET.Parsers.Fql; +Fql.Register(); +``` + +--- + +# When to Use FQL + +Use FQL when your API consumers prefer SQL-like query expressions instead of the native FlexQuery DSL. + +FQL is ideal for: + +- SQL-inspired filtering +- Complex logical expressions +- Nested property filtering +- Collection navigation (`Any`, `All`, `Count`) +- Aggregation and grouping +- Rich REST API querying + +--- + +# HTTP Request Examples + +## Basic Filtering + +```http +GET /api/users?filter=Status = 'Active' +``` + +--- + +## Multiple Conditions + +```http +GET /api/users?filter=Status = 'Active' AND Age >= 18 +``` + +```http +GET /api/products?filter=Category = 'Books' OR Category = 'Electronics' +``` + +--- + +## Nested Property Filtering + +```http +GET /api/orders?filter=Customer.Country = 'Japan' +``` + +--- + +## String Operations + +```http +GET /api/users?filter=Name CONTAINS 'john' +``` + +```http +GET /api/users?filter=Email STARTSWITH 'admin' +``` + +```http +GET /api/users?filter=Email ENDSWITH '.com' +``` + +```http +GET /api/users?filter=Name LIKE '%john%' +``` + +--- + +## Null Checks + +```http +GET /api/users?filter=DeletedAt IS NULL +``` + +```http +GET /api/users?filter=DeletedAt IS NOT NULL +``` + +--- -var parser = new FqlQueryParser(); -var filterGroup = parser.Parse("Status = 'Active' AND Age >= 18"); +## Collection Operators -// GET /api/users?filter=Status = 'Active' AND Age >= 18 +```http +GET /api/users?filter=Status IN ('Active','Pending') ``` -## Fql Syntax Examples +```http +GET /api/products?filter=Price BETWEEN 1000 AND 5000 +``` + +--- + +## Collection Navigation + +### Any + +```http +GET /api/customers?filter=Orders.Any(Status = 'Completed') +``` + +```http +GET /api/customers?filter=Orders.Any(Status = 'Completed' AND Total > 1000) +``` + +### Nested Collections + +```http +GET /api/customers?filter=Orders.Any( + Status = 'Completed' + AND OrderItems.Any(Quantity > 5) +) +``` + +### Count + +```http +GET /api/customers?filter=Orders.Count > 5 +``` + +--- + +# Complete Example + +Retrieve active orders from Japanese customers, include related entities, sort the results, calculate aggregates, and return paged results. + +```http +GET /api/orders? +filter=((Status = 'Open' OR Status = 'Pending') +AND Amount > 1000 +AND Customer.Country = 'Japan' +AND OrderItems.Any(Product.Price > 500)) +&select=Id,OrderNo,Amount,Customer.Name +&include=Customer,OrderItems +&sort=CreatedAt DESC,Customer.Name ASC +&groupBy=Customer.Country +&aggregate=SUM(Amount) AS TotalSales,COUNT(*) AS TotalOrders +&having=SUM(Amount) > 10000 +&distinct=true +&page=1 +&pageSize=20 +&includeCount=true +``` -```Fql -Status = 'Active' -Age >= 18 AND -Name CONTAINS 'john' -Status = 'Active' AND Age >= 18 -Category = 'Electronics' OR Category = 'Books' -DeletedAt IS NULL -Status IN ('Active', 'Pending') +--- + +# Supported Query Parameters + +| Parameter | Description | +|------------|-------------| +| `filter` | Filter records | +| `select` | Select specific fields | +| `sort` | Sort one or more fields | +| `include` | Include related entities | +| `groupBy` | Group results | +| `aggregates` | Aggregate functions | +| `having` | HAVING clause | +| `page` | Page number | +| `pageSize` | Number of records per page | +| `distinct` | Return distinct records | +| `includeCount` | Include the total record count | + +--- + +# Supported Operators + +## Comparison + +```sql += +!= +> +>= +< +<= ``` -## Features +## Logical + +```sql +AND +OR +``` + +## String + +```sql +CONTAINS +STARTSWITH +ENDSWITH +LIKE +``` + +## Null + +```sql +IS NULL +IS NOT NULL +``` + +## Collections + +```sql +IN +NOT IN +BETWEEN +ANY +ALL +COUNT +``` + +--- + +# Features -- **`FqlQueryParser`** — Parses Fql filter expressions into `FilterGroup` AST -- **Auto-Detection** — Registers as `IQueryParser` so Fql queries are handled seamlessly -- **Supported Operators** — eq, neq, gt, gte, lt, lte, contains, startswith, endswith, like, isnull, isnotnull, in, notin, between, any, all, count -- **Standalone Usage** — Can be used without the full FlexQuery execution pipeline +- SQL-inspired query language +- Strict grammar validation +- Nested property paths +- Collection navigation +- Aggregate functions +- HAVING support +- Projection +- Sorting +- Paging +- Compatible with the FlexQuery.NET execution pipeline -## Known Limitations +--- -- The parser implements a subset of the full FQL specification — complex functions and custom fields are not supported -- Date/time parsing uses .NET conventions +# Related Packages -## Related Packages +- **FlexQuery.NET** – Core query engine +- **FlexQuery.NET.Parsers.Dsl** – Native FlexQuery DSL +- **FlexQuery.NET.Parsers.MiniOData** – OData-compatible query syntax -- [FlexQuery.NET](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET/README.md) — Core query engine -- [FlexQuery.NET.Parsers.MiniOData](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.MiniOData/README.md) — Alternative parser for OData syntax +--- -## Documentation +# Documentation -- [Query Formats Guide](https://flexquery.vercel.app/guide/query-formats) -- [Query Syntax Reference](https://flexquery.vercel.app/guide/query-syntax) +- https://flexquery.vercel.app \ No newline at end of file diff --git a/src/FlexQuery.NET.Parsers.MiniOData/README.md b/src/FlexQuery.NET.Parsers.MiniOData/README.md index 2fb3c1e..3135b5b 100644 --- a/src/FlexQuery.NET.Parsers.MiniOData/README.md +++ b/src/FlexQuery.NET.Parsers.MiniOData/README.md @@ -18,7 +18,9 @@ dotnet add package FlexQuery.NET.Parsers.MiniOData ## Registration ```csharp -builder.Services.AddFlexQueryMiniOData(); +using FlexQuery.NET.Parsers.MiniOData; + +MiniOData.Register(); ``` ## Quick Start @@ -32,45 +34,63 @@ GET /api/products?$filter=Price gt 50 and Status eq 'Active'&$orderby=Name desc& ```csharp [HttpGet("products")] -public async Task GetProducts([FromQuery] FlexQueryParameters parameters) +public async Task GetProducts([FromQuery] MiniODataRequest request) { - var result = await _context.Products.FlexQueryAsync(parameters, opts => + var options = request.ToQueryOptions(); + + var result = await _context.Products.FlexQueryAsync(options, cfg => { - opts.AllowedFields = new HashSet { "Id", "Name", "Price", "Status" }; + cfg.AllowedFields = new HashSet { "Id", "Name", "Price", "Status" }; }); return Ok(result); } ``` -## Supported OData Parameters +## Supported OData Features -| Parameter | Maps To | -|---|---| -| `$filter` | `QueryOptions.Filter` | -| `$orderby` | `QueryOptions.Sort` | -| `$select` | `QueryOptions.Select` | -| `$top` | `Paging.PageSize` | -| `$skip` | Calculated from `Paging.Page` | -| `$expand` | `QueryOptions.Includes` | +| Feature | Status | Maps To / Notes | +| ---------- | ----------- | -------------------------------- | +| `$filter` | ✅ Supported | `QueryOptions.Filter` — strict grammar validation | +| `$orderby` | ✅ Supported | `QueryOptions.Sort` — supports `asc` / `desc` | +| `$select` | ✅ Supported | `QueryOptions.Select` — supports nested property paths | +| `$expand` | ✅ Supported | `QueryOptions.Includes` — flat navigation paths only | +| `$top` | ✅ Supported | `Paging.PageSize` | +| `$skip` | ✅ Supported | Calculated from `Paging.Page` | +| `$count` | ✅ Supported | Maps to `IncludeCount` | ## Features -- **Auto-Detection** — `MiniODataQueryParser` detects OData parameters and parses them automatically +- **OData-Compatible Syntax** — Supports `$filter`, `$orderby`, `$select`, `$expand`, `$top`, `$skip`, `$count` - **No Migration Required** — Both OData and native FlexQuery syntax work on the same endpoints -- **Seamless Integration** — Register once to enable automatic OData parameter parsing -- **Standalone Parsing** — `ODataQueryParameterParser.Parse()` for direct OData-to-`QueryOptions` conversion +- **Seamless Integration** — Register once via `MiniOData.Register()` to enable OData parameter parsing +- **Standalone Parsing** — `MiniODataRequest.ToQueryOptions()` for direct OData-to-`QueryOptions` conversion + +## Deferred / Unsupported Features + +MiniOData is a lightweight OData compatibility layer, not a complete OData implementation. The +following features are intentionally unavailable. Supplying an unsupported or deferred feature +fails with a clear `MiniODataParseException` rather than being silently ignored. + +| Feature | Status | Notes | +| ------------------------------------- | --------------- | -------------------------------------- | +| `$apply` | ⏳ Deferred | May be implemented in a future release | +| `$compute` | ❌ Not Supported | No current FlexQuery equivalent | +| `$search` | ❌ Not Supported | No current FlexQuery equivalent | +| `$levels` | ❌ Not Supported | No current FlexQuery equivalent | +| `$ref` | ❌ Not Supported | No current FlexQuery equivalent | +| Nested query options inside `$expand` | ❌ Not Supported | Use top-level query parameters instead | ## Known Limitations - The parser implements a subset of the full OData specification — not all OData functions and expressions are supported -- `$expand` support is limited to simple navigation property includes +- `$expand` support is limited to flat navigation property includes (nested query options are unsupported) - `$count` aggregation semantics differ from OData's `$inlinecount` ## Related Packages - [FlexQuery.NET](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET/README.md) — Core query engine -- [FlexQuery.NET.Parsers.Jql](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.Jql/README.md) — Alternative parser for JQL syntax +- [FlexQuery.NET.Parsers.FQL](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.FQL/README.md) — Alternative parser for FQL syntax ## Documentation diff --git a/src/FlexQuery.NET/README.md b/src/FlexQuery.NET/README.md index ab34caa..f68aa39 100644 --- a/src/FlexQuery.NET/README.md +++ b/src/FlexQuery.NET/README.md @@ -3,85 +3,163 @@ [![NuGet Version](https://img.shields.io/nuget/v/FlexQuery.NET.svg)](https://www.nuget.org/packages/FlexQuery.NET) [![NuGet Downloads](https://img.shields.io/nuget/dt/FlexQuery.NET.svg)](https://www.nuget.org/packages/FlexQuery.NET) -Dynamic filtering, sorting, paging, and projection for `IQueryable` in .NET. +A flexible query engine for .NET that enables dynamic filtering, sorting, projection, grouping, aggregation, paging, and validation over `IQueryable`. -FlexQuery.NET is the core package. It provides the query parsing engine, expression builders, validation pipeline, and all core models. +FlexQuery.NET includes the native FlexQuery DSL out of the box and serves as the foundation of the FlexQuery ecosystem. -## When to Use This Package +--- -Install this package if you want to parse, validate, and compose dynamic queries independently of any specific data provider. -Use this package directly when building custom integrations or when implementing your own query execution pipeline. - - -## Installation +# Installation ```bash dotnet add package FlexQuery.NET ``` +--- + +# When to Use This Package -## Quick Start +Install the core package when you want to: -```csharp -using FlexQuery.NET; +- Build dynamic query APIs +- Parse and validate client query parameters +- Generate LINQ expression trees +- Support filtering, sorting, projection, grouping, and paging +- Build custom query execution pipelines +- Integrate FlexQuery with your own data provider -var parameters = new FlexQueryParameters -{ - Filter = "status:eq:active", - Sort = "createdAt:desc", - Page = 1, - PageSize = 20 -}; +If you're using Entity Framework Core or Dapper, install the corresponding integration package. -var options = parameters.ToQueryOptions(); +--- -var query = _context.Users.Apply(options); +# Example HTTP Request + +The native FlexQuery DSL is included by default. + +```http +GET /api/products? +filter=category:eq:electronics|price:between:1000,5000 +&select=id,name,price,supplier.name +&sort=price:desc,name:asc +&include=supplier,reviews +&page=1 +&pageSize=20 +&includeCount=true -// Execute using your preferred provider ``` -## Features +--- -- **Query Parsing** — Auto-detects DSL, JSON, and Indexed query formats via `QueryOptionsParser.Parse()` -- **Filtering** — 20+ operators (eq, contains, gt, between, in, etc.), nested AND/OR logic -- **Sorting** — Multi-field sorting with direction and aggregate sort support -- **Paging** — 1-based page indexing with configurable page size limits -- **Security** — Declare allowed/blocked fields per-endpoint via `BaseQueryOptions` -- **Validation** — Built-in field path, operator, and type validation with `ValidateOrThrow()` -- **Diagnostics** — Optional `IFlexQueryExecutionListener` for observability -- **Query Parsing Cache** — Thread-safe parser cache for low-latency repeated queries +# Features -## Supported Query Formats +- Dynamic filtering +- Projection (`select`) +- Multi-column sorting +- Navigation property includes +- Nested property paths +- Collection operators (`any`, `all`, `count`) +- Grouping +- Aggregate functions +- HAVING support +- Offset pagination +- Keyset (cursor) pagination +- Query validation +- Expression tree generation +- Provider-agnostic architecture +- Extensible parser pipeline -All formats are auto-detected — no configuration needed. +--- -```http -DSL -GET /api/users?filter=age:gte:18&sort=name:asc +# Supported Query Parameters + +| Parameter | Description | +|-----------|-------------| +| `filter` | Filter records | +| `select` | Select specific fields | +| `sort` | Sort one or more fields | +| `include` | Include related entities | +| `groupBy` | Group records | +| `aggregate` | Aggregate functions | +| `having` | HAVING clause | +| `page` | Page number | +| `pageSize` | Number of records per page | +| `cursor` | Cursor for keyset pagination | +| `useKeysetPagination` | Enables keyset pagination | +| `distinct` | Return distinct records | +| `includeCount` | Include the total record count | + +--- -JSON -GET /api/users?filter={"logic":"and","filters":[{"field":"age","operator":"gte","value":18}]} +# Query Languages -Indexed -GET /api/users?filter[0].field=age&filter[0].operator=gte&filter[0].value=18 +FlexQuery.NET includes the native FlexQuery DSL by default. + +Additional query languages are available as optional packages. + +| Query Language | Package | +|----------------|---------| +| Native FlexQuery DSL | Included | +| FQL (SQL-inspired) | FlexQuery.NET.Parsers.Fql | +| Mini OData | FlexQuery.NET.Parsers.MiniOData | + +All query languages produce the same internal `QueryOptions` model, allowing them to share the same validation, expression building, and execution pipeline. + +--- + +# Architecture + +``` +HTTP Request + │ + ▼ +Query Parser + │ + ▼ +QueryOptions + │ + ▼ +Validation + │ + ▼ +Expression Builder + │ + ▼ +Execution Provider + ├── Entity Framework Core + ├── Dapper + └── Custom Provider ``` -## Related Packages +--- + +# Related Packages + +| Package | Description | +|---------|-------------| +| FlexQuery.NET.EntityFrameworkCore | Entity Framework Core integration | +| FlexQuery.NET.Dapper | SQL generation for Dapper | +| FlexQuery.NET.AspNetCore | ASP.NET Core integration | +| FlexQuery.NET.Diagnostics | Diagnostics and execution events | +| FlexQuery.NET.Adapters.AgGrid | AG Grid Server-Side Row Model | +| FlexQuery.NET.Adapters.Kendo | Telerik Kendo UI integration | +| FlexQuery.NET.Parsers.Fql | SQL-inspired query language | +| FlexQuery.NET.Parsers.MiniOData | OData-compatible query language | +| FlexQuery.NET.OpenApi | OpenAPI/Swagger documentation and examples | -- [FlexQuery.NET.EntityFrameworkCore](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.EntityFrameworkCore/README.md) — Async execution for EF Core -- [FlexQuery.NET.Dapper](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Dapper/README.md) — SQL generation for Dapper -- [FlexQuery.NET.AspNetCore](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.AspNetCore/README.md) — ASP.NET Core `[FieldAccess]` security -- [FlexQuery.NET.Diagnostics](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Diagnostics/README.md) — Execution diagnostics -- [FlexQuery.NET.Adapters.AgGrid](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Adapters.AgGrid/README.md) — AG Grid SSRM adapter -- [FlexQuery.NET.Adapters.Kendo](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Adapters.Kendo/README.md) — Kendo UI adapter -- [FlexQuery.NET.Parsers.Jql](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.Jql/README.md) — JQL parser -- [FlexQuery.NET.Parsers.MiniOData](https://github.com/peterjohncasasola/FlexQuery.NET/blob/main/src/FlexQuery.NET.Parsers.MiniOData/README.md) — OData-compatible parser +--- -## Full Documentation: +# Documentation https://flexquery.vercel.app -- [Getting Started](https://flexquery.vercel.app/guide/getting-started) -- [Query Syntax Reference](https://flexquery.vercel.app/shared/query-language) -- [Security & Governance](https://flexquery.vercel.app/guide/security-governance) -- [API Reference](https://flexquery.vercel.app/shared/operators) +- Getting Started +- Query Language (DSL) +- FQL +- Mini OData +- Operators +- Pagination +- Validation +- Entity Framework Core +- Dapper +- ASP.NET Core +- Adapters \ No newline at end of file From 3cd45a2a2ac5ff5a7b034a3ba7cfa129470a2cc4 Mon Sep 17 00:00:00 2001 From: Peter John Casasola Date: Mon, 13 Jul 2026 02:57:20 +0800 Subject: [PATCH 3/6] docs: unify example domain and restructure documentation - Move MiniOData from adapters/ to parsers/ to match source package - Add missing FQL parser documentation page - Unify all examples to canonical Customer/Order/OrderItem domain - Restructure sidebar: comparisons and architecture earlier, performance tuning/debugging under execution - Fix 17 documentation correctness issues from Phase 1 audit - Verify all internal links resolve correctly 48 files changed, 1125 insertions(+), 558 deletions(-) --- docs/.vitepress/config.ts | 53 +++- docs/adapters/ag-grid.md | 11 +- docs/adapters/kendo.md | 5 +- docs/architecture/provider-model.md | 1 - docs/comparison/graphql-odata.md | 8 +- docs/examples/advanced.md | 76 ++--- docs/examples/basic.md | 65 ++-- docs/examples/filtering.md | 12 +- docs/examples/formats.md | 210 ++++++++++++- docs/examples/include.md | 12 +- docs/examples/real-world.md | 148 ++++----- docs/guide/aspnet-integration.md | 30 +- docs/guide/basic-usage.md | 12 +- docs/guide/comparison-libraries.md | 16 +- docs/guide/comparison.md | 16 +- docs/guide/core-concepts.md | 60 ++-- docs/guide/debugging.md | 4 +- docs/guide/dotnet-comparison.md | 30 +- docs/guide/execution-pipeline.md | 36 ++- docs/guide/extension-methods.md | 4 +- docs/guide/filtering.md | 42 +-- docs/guide/flattening.md | 18 +- docs/guide/fluent-builder.md | 2 +- docs/guide/getting-started.md | 41 +-- docs/guide/grouping.md | 26 +- docs/guide/include-filtering.md | 42 +-- docs/guide/linq-integration.md | 6 +- docs/guide/paging.md | 18 +- docs/guide/performance-tuning.md | 4 +- .../guide/performance/fairness-disclaimers.md | 2 +- docs/guide/performance/memory-usage.md | 2 +- docs/guide/performance/scalability.md | 4 +- docs/guide/projection.md | 32 +- docs/guide/query-composition.md | 16 +- docs/guide/query-formats.md | 4 +- docs/guide/query-syntax.md | 42 ++- docs/guide/security.md | 24 +- docs/guide/sorting.md | 42 +-- docs/guide/swagger-integration.md | 2 +- docs/guide/validation.md | 26 +- docs/guide/why-flexquery.md | 32 +- docs/index.md | 64 ++-- docs/parsers/fql.md | 297 ++++++++++++++++++ docs/{adapters => parsers}/miniodata.md | 0 docs/providers/dapper/getting-started.md | 14 +- docs/providers/ef-core.md | 24 +- docs/readme.md | 8 +- docs/shared/operators.md | 40 +-- 48 files changed, 1125 insertions(+), 558 deletions(-) create mode 100644 docs/parsers/fql.md rename docs/{adapters => parsers}/miniodata.md (100%) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a3e2bcb..00b695b 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -78,6 +78,20 @@ export default defineConfig({ { text: 'Query Formats', link: '/guide/query-formats' } ] }, + { + text: 'Comparisons', + items: [ + { text: 'vs GraphQL & OData', link: '/guide/comparison' }, + { text: 'vs .NET Libraries', link: '/guide/comparison-libraries' } + ] + }, + { + text: 'Architecture', + items: [ + { text: 'Provider Model', link: '/architecture/provider-model' }, + { text: 'Grouped Query Contract', link: '/architecture/grouped-query-contract' } + ] + }, { text: 'Execution & Security', items: [ @@ -85,7 +99,9 @@ export default defineConfig({ { text: 'Validation', link: '/guide/validation' }, { text: 'Security & Field Access', link: '/guide/security' }, { text: 'Security Governance', link: '/guide/security-governance' }, - { text: 'Field Mapping', link: '/guide/field-mapping' } + { text: 'Field Mapping', link: '/guide/field-mapping' }, + { text: 'Performance Tuning', link: '/guide/performance-tuning' }, + { text: 'Debugging', link: '/guide/debugging' } ] }, { @@ -105,12 +121,18 @@ export default defineConfig({ } ] }, + { + text: 'Parsers', + items: [ + { text: 'MiniOData Parser', link: '/parsers/miniodata' }, + { text: 'FQL Parser', link: '/parsers/fql' } + ] + }, { text: 'Adapters', items: [ { text: 'AG Grid Integration', link: '/adapters/ag-grid' }, - { text: 'MiniOData Parser', link: '/adapters/miniodata' }, - { text: 'Kendo UI Integration', link: '/adapters/kendo' }, + { text: 'Kendo UI Integration', link: '/adapters/kendo' } ] }, { @@ -118,9 +140,7 @@ export default defineConfig({ collapsed: false, items: [ { text: 'ASP.NET Core', link: '/guide/aspnet-integration' }, - { text: 'Swagger Integration', link: '/guide/swagger-integration' }, - { text: 'Performance Tuning', link: '/guide/performance-tuning' }, - { text: 'Debugging', link: '/guide/debugging' } + { text: 'Swagger Integration', link: '/guide/swagger-integration' } ] }, { @@ -141,16 +161,14 @@ export default defineConfig({ ] }, { - text: 'Comparisons', - items: [ - { text: 'vs GraphQL & OData', link: '/guide/comparison' }, - { text: 'vs .NET Libraries', link: '/guide/dotnet-comparison' } - ] - }, - { - text: 'Architecture', + text: 'Examples', items: [ - { text: 'Provider Model', link: '/architecture/provider-model' } + { text: 'Basic Examples', link: '/examples/basic' }, + { text: 'Advanced Examples', link: '/examples/advanced' }, + { text: 'Real-World Scenarios', link: '/examples/real-world' }, + { text: 'Format Examples', link: '/examples/formats' }, + { text: 'Filtering Examples', link: '/examples/filtering' }, + { text: 'Include Examples', link: '/examples/include' } ] }, { @@ -171,7 +189,10 @@ export default defineConfig({ items: [ { text: 'Basic Examples', link: '/examples/basic' }, { text: 'Advanced Examples', link: '/examples/advanced' }, - { text: 'Real-World Scenarios', link: '/examples/real-world' } + { text: 'Real-World Scenarios', link: '/examples/real-world' }, + { text: 'Format Examples', link: '/examples/formats' }, + { text: 'Filtering Examples', link: '/examples/filtering' }, + { text: 'Include Examples', link: '/examples/include' } ] } ], diff --git a/docs/adapters/ag-grid.md b/docs/adapters/ag-grid.md index 20fc56a..3abc6e7 100644 --- a/docs/adapters/ag-grid.md +++ b/docs/adapters/ag-grid.md @@ -39,7 +39,7 @@ AG Grid Enterprise (Browser) │ ASP.NET Controller │ - ▼ AgGridQueryOptionsParser.Parse() + ▼ ToQueryOptions() │ QueryOptions (canonical AST) │ @@ -53,18 +53,21 @@ QueryResult ## Basic Example ```csharp +using FlexQuery.NET.Adapters.AgGrid; +using FlexQuery.NET.Adapters.AgGrid.Models; + [HttpPost("grid-data")] public async Task GetGridData([FromBody] JsonElement agGridPayload) { - var options = AgGridQueryOptionsParser.Parse(agGridPayload); + var options = agGridPayload.ToQueryOptions(); - var result = await _context.Products.FlexQueryAsync(options, opts => + var result = await _context.Products.FlexQueryAsync(options, opts => { opts.AllowedFields = new HashSet { "Id", "Name", "Price", "Category" }; opts.MaxPageSize = 200; }); - return Ok(result); + return Ok(options); } ``` diff --git a/docs/adapters/kendo.md b/docs/adapters/kendo.md index 0f3a305..4d1f96e 100644 --- a/docs/adapters/kendo.md +++ b/docs/adapters/kendo.md @@ -59,7 +59,7 @@ QueryResult [HttpPost("grid-data")] public async Task GetGridData([FromBody] JsonElement kendoPayload) { - var options = KendoQueryOptionsParser.Parse(kendoPayload); + var options = kendoPayload.ToQueryOptions(); var result = await _context.Products.FlexQueryAsync(options, opts => { @@ -76,9 +76,6 @@ Using extension methods: ```csharp // From a KendoRequest object var options = kendoRequest.ToQueryOptions(); - -// From a raw JSON string -var options = jsonString.FromKendoJson(); ``` ## Advanced Example: Full Kendo Payload diff --git a/docs/architecture/provider-model.md b/docs/architecture/provider-model.md index 85729af..d51aa68 100644 --- a/docs/architecture/provider-model.md +++ b/docs/architecture/provider-model.md @@ -164,7 +164,6 @@ public async Task GetProducts([FromQuery] FlexQueryParameters p) public async Task GetSalesReport([FromQuery] FlexQueryParameters p) { await using var conn = new SqlConnection(_connectionString); - await conn.OpenAsync(); return Ok(await conn.FlexQueryAsync(p, ConfigureOptions)); } diff --git a/docs/comparison/graphql-odata.md b/docs/comparison/graphql-odata.md index 3174983..49b9bc9 100644 --- a/docs/comparison/graphql-odata.md +++ b/docs/comparison/graphql-odata.md @@ -21,22 +21,22 @@ A quick side-by-side comparison. For the full analysis with complete request/res ## The Same Query -**Get active users named "alice", sorted by creation date, page 1:** +**Get active customers named "alice", sorted by creation date, page 1:** ### FlexQuery.NET ``` -GET /api/users?filter=name:contains:alice,status:eq:active&sort=createdAt:desc&page=1&pageSize=10&select=id,name,email +GET /api/customers?filter=name:contains:alice,status:eq:active&sort=createdDate:desc&page=1&pageSize=10&select=id,name,email ``` ### GraphQL ```graphql POST /graphql -{ users(where: { and: [{ name: { contains: "alice" } }, { status: { eq: "active" } }] }, order: [{ createdAt: DESC }], skip: 0, take: 10) { totalCount items { id name email } } } +{ customers(where: { and: [{ name: { contains: "alice" } }, { status: { eq: "active" } }] }, order: [{ createdDate: DESC }], skip: 0, take: 10) { totalCount items { id name email } } } ``` ### OData ``` -GET /odata/Users?$filter=contains(Name,'alice') and Status eq 'active'&$orderby=CreatedAt desc&$top=10&$skip=0&$select=Id,Name,Email&$count=true +GET /odata/Customers?$filter=contains(Name,'alice') and Status eq 'active'&$orderby=CreatedDate desc&$top=10&$skip=0&$select=Id,Name,Email&$count=true ``` --- diff --git a/docs/examples/advanced.md b/docs/examples/advanced.md index 73c5aac..8b0355a 100644 --- a/docs/examples/advanced.md +++ b/docs/examples/advanced.md @@ -2,27 +2,29 @@ Advanced patterns including nested filters, aggregates, projection modes, filtered includes, and aliasing. +> **Note:** Examples using `FlexQueryAsync` require the `FlexQuery.NET.EntityFrameworkCore` or `FlexQuery.NET.Dapper` package. The core `FlexQuery.NET` package provides synchronous `FlexQuery` for advanced scenarios. + --- ## Example 1: Nested ANY Filter -**Scenario:** Get all users who have at least one shipped order with amount > 100. +**Scenario:** Get all customers who have at least one shipped order with total > 100. **Request:** ``` -GET /api/users?query=Orders.any(Status = "shipped" AND Amount > 100)&select=id,name&page=1&pageSize=20 +GET /api/customers?filter=Orders.any(Status = "shipped" AND Total > 100)&select=id,name&page=1&pageSize=20 ``` **Controller:** ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { - "id", "name", "email", "orders.status", "orders.amount" + "id", "name", "email", "orders.status", "orders.total" }; exec.MaxFieldDepth = 2; }); @@ -33,13 +35,13 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame **SQL Generated:** ```sql -SELECT u.Id, u.Name -FROM Users u +SELECT c.Id, c.Name +FROM Customers c WHERE EXISTS ( SELECT 1 FROM Orders o - WHERE o.UserId = u.Id + WHERE o.CustomerId = c.Id AND o.Status = 'shipped' - AND o.Amount > 100 + AND o.Total > 100 ) OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY ``` @@ -68,16 +70,16 @@ OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY **Request:** ``` -GET /api/orders?select=status,count(),sum(amount)&groupBy=status&sort=count():desc +GET /api/orders?select=status,count(),sum(total)&groupBy=status&sort=count():desc ``` **Response:** ```json { "data": [ - { "status": "pending", "allCount": 45, "amountSum": 12400.00 }, - { "status": "shipped", "allCount": 38, "amountSum": 9800.50 }, - { "status": "cancelled", "allCount": 12, "amountSum": 3200.00 } + { "status": "pending", "allCount": 45, "totalSum": 12400.00 }, + { "status": "shipped", "allCount": 38, "totalSum": 9800.50 }, + { "status": "cancelled", "allCount": 12, "totalSum": 3200.00 } ], "totalCount": 3 } @@ -91,14 +93,14 @@ GET /api/orders?select=status,count(),sum(amount)&groupBy=status&sort=count():de **Request:** ``` -GET /api/orders?select=status,sum(amount)&groupBy=status&having=sum(amount):gt:10000 +GET /api/orders?select=status,sum(total)&groupBy=status&having=sum(total):gt:10000 ``` **Response:** ```json { "data": [ - { "status": "pending", "amountSum": 12400.00 } + { "status": "pending", "totalSum": 12400.00 } ], "totalCount": 1 } @@ -108,11 +110,11 @@ GET /api/orders?select=status,sum(amount)&groupBy=status&having=sum(amount):gt:1 ## Example 4: Filtered Includes -**Scenario:** Get all users, but only include their orders that are in "shipped" status. +**Scenario:** Get all customers, but only include their orders that are in "shipped" status. **Request:** ``` -GET /api/users?include=Orders(status:eq:shipped)&select=id,name&page=1&pageSize=3 +GET /api/customers?include=Orders(status:eq:shipped)&select=id,name&page=1&pageSize=3 ``` **Response:** @@ -123,8 +125,8 @@ GET /api/users?include=Orders(status:eq:shipped)&select=id,name&page=1&pageSize= "id": 1, "name": "Alice Chen", "orders": [ - { "id": 101, "status": "shipped", "amount": 250.00 }, - { "id": 108, "status": "shipped", "amount": 89.00 } + { "id": 101, "status": "shipped", "total": 250.00 }, + { "id": 108, "status": "shipped", "total": 89.00 } ] }, { @@ -136,7 +138,7 @@ GET /api/users?include=Orders(status:eq:shipped)&select=id,name&page=1&pageSize= "id": 3, "name": "Carol White", "orders": [ - { "id": 202, "status": "shipped", "amount": 450.00 } + { "id": 202, "status": "shipped", "total": 450.00 } ] } ], @@ -156,15 +158,15 @@ Note: Bob has no shipped orders, but he is still included in results — the inc **Request:** ``` -GET /api/users?select=id,name,profile.bio,address.city&mode=flat&page=1&pageSize=5 +GET /api/customers?select=id,name,address.city&mode=flat&page=1&pageSize=5 ``` **Response:** ```json { "data": [ - { "id": 1, "name": "Alice", "profile.bio": "Software Engineer", "address.city": "Singapore" }, - { "id": 2, "name": "Bob", "profile.bio": "Product Manager", "address.city": "London" } + { "id": 1, "name": "Alice", "address.city": "Singapore" }, + { "id": 2, "name": "Bob", "address.city": "London" } ], "totalCount": 48, "page": 1, @@ -176,11 +178,11 @@ GET /api/users?select=id,name,profile.bio,address.city&mode=flat&page=1&pageSize ## Example 6: Sort by Collection Aggregate -**Scenario:** Sort users by their total order count (most active users first). +**Scenario:** Sort customers by their total order count (most active customers first). **Request:** ``` -GET /api/users?sort=orders.count():desc&select=id,name&page=1&pageSize=10 +GET /api/customers?sort=orders.count():desc&select=id,name&page=1&pageSize=10 ``` **Response:** @@ -205,16 +207,16 @@ GET /api/users?sort=orders.count():desc&select=id,name&page=1&pageSize=10 **Request:** ``` -GET /api/users?filter={"logic":"and","filters":[ +GET /api/customers?filter={"logic":"and","filters":[ {"field":"status","operator":"eq","value":"active"}, {"logic":"or","filters":[ - {"field":"age","operator":"gte","value":"30"}, + {"field":"salary","operator":"gte","value":"50000"}, {"field":"name","operator":"contains","value":"senior"} ]} ]} ``` -Equivalent to: `status = "active" AND (age >= 30 OR name contains "senior")` +Equivalent to: `status = "active" AND (salary >= 50000 OR name contains "senior")` --- @@ -225,14 +227,14 @@ Equivalent to: `status = "active" AND (age >= 30 OR name contains "senior")` **Controller:** ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.RoleAllowedFields = new Dictionary> { - ["admin"] = new HashSet { "id", "name", "email", "salary", "status" }, - ["viewer"] = new HashSet { "id", "name", "email", "status" } + ["admin"] = new HashSet { "id", "name", "email", "salary", "status", "city" }, + ["viewer"] = new HashSet { "id", "name", "email", "status", "city" } }; exec.CurrentRole = User.IsInRole("admin") ? "admin" : "viewer"; @@ -245,12 +247,12 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame **Admin request:** ``` -GET /api/users?select=id,name,salary&filter=salary:gt:50000 +GET /api/customers?select=id,name,salary&filter=salary:gt:50000 ``` **Viewer request (same endpoint, different role):** ``` -GET /api/users?select=id,name,salary +GET /api/customers?select=id,name,salary ``` **Viewer response (400):** @@ -282,14 +284,14 @@ public async Task GetOrders([FromQuery] FlexQueryParameters param // Validate var execOptions = new QueryExecutionOptions { - AllowedFields = new HashSet { "id", "amount", "status", "createdAt" }, + AllowedFields = new HashSet { "id", "orderNumber", "total", "status", "orderDate" }, MaxFieldDepth = 1 }; options.ValidateOrThrow(execOptions); // Start with tenant pre-filter (BEFORE FlexQuery filter) var query = _context.Orders - .Where(o => o.TenantId == CurrentTenantId) + .Where(o => o.CustomerId == CurrentTenantId) .AsQueryable(); // Apply FlexQuery pipeline @@ -315,7 +317,7 @@ public async Task GetOrders([FromQuery] FlexQueryParameters param **Request:** ``` -GET /api/users?select=status&distinct=true +GET /api/customers?select=status&distinct=true ``` **Response:** diff --git a/docs/examples/basic.md b/docs/examples/basic.md index dbfed20..d29df56 100644 --- a/docs/examples/basic.md +++ b/docs/examples/basic.md @@ -2,26 +2,29 @@ Real-world examples showing common FlexQuery.NET usage patterns. + +> **Note:** Examples using `FlexQueryAsync` require the `FlexQuery.NET.EntityFrameworkCore` or `FlexQuery.NET.Dapper` package. The core `FlexQuery.NET` package provides synchronous `FlexQuery` for advanced scenarios. + --- ## Example 1: List with Filter and Paging -**Scenario:** An admin panel listing users, searchable by name, filterable by status. +**Scenario:** An admin panel listing customers, searchable by name, filterable by status. **Request:** ``` -GET /api/users?filter=status:eq:active&sort=name:asc&page=1&pageSize=20 +GET /api/customers?filter=status:eq:active&sort=name:asc&page=1&pageSize=20 ``` **Controller:** ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { - exec.AllowedFields = new HashSet { "id", "name", "email", "status", "createdAt" }; - exec.SortableFields = new HashSet { "name", "createdAt" }; + exec.AllowedFields = new HashSet { "id", "name", "email", "status", "createdDate" }; + exec.SortableFields = new HashSet { "name", "createdDate" }; exec.MaxFieldDepth = 1; }); @@ -41,9 +44,9 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame "hasPreviousPage": false, "aggregates": null, "data": [ - { "id": 1, "name": "Alice Chen", "email": "alice@example.com", "status": "active", "createdAt": "2024-03-15T10:00:00Z" }, - { "id": 2, "name": "Bob Smith", "email": "bob@example.com", "status": "active", "createdAt": "2024-04-01T09:30:00Z" }, - { "id": 5, "name": "Carol White", "email": "carol@example.com", "status": "active", "createdAt": "2024-04-20T14:00:00Z" } + { "id": 1, "name": "Alice Chen", "email": "alice@example.com", "status": "active", "createdDate": "2024-03-15T10:00:00Z" }, + { "id": 2, "name": "Bob Smith", "email": "bob@example.com", "status": "active", "createdDate": "2024-04-01T09:30:00Z" }, + { "id": 5, "name": "Carol White", "email": "carol@example.com", "status": "active", "createdDate": "2024-04-20T14:00:00Z" } ], "nextCursorToken": null } @@ -55,13 +58,13 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame **Request:** ``` -GET /api/users?filter=name:contains:ali&sort=name:asc&page=1&pageSize=10 +GET /api/customers?filter=name:contains:ali&sort=name:asc&page=1&pageSize=10 ``` **SQL Generated:** ```sql -SELECT Id, Name, Email, Status, CreatedAt -FROM Users +SELECT Id, Name, Email, Status, CreatedDate +FROM Customers WHERE LOWER(Name) LIKE '%ali%' ORDER BY Name ASC OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY @@ -95,13 +98,13 @@ OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY **Request:** ``` -GET /api/users?filter=status:eq:active&select=id,name,email&page=1&pageSize=50 +GET /api/customers?filter=status:eq:active&select=id,name,email&page=1&pageSize=50 ``` **SQL Generated:** ```sql SELECT Id, Name, Email -FROM Users +FROM Customers WHERE Status = 'active' ORDER BY Id OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY @@ -132,12 +135,12 @@ OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY **Request:** ``` -GET /api/users?sort=status:asc,name:asc,createdAt:desc&page=1&pageSize=20 +GET /api/customers?sort=status:asc,name:asc,createdDate:desc&page=1&pageSize=20 ``` **SQL Generated:** ```sql -ORDER BY Status ASC, Name ASC, CreatedAt DESC +ORDER BY Status ASC, Name ASC, CreatedDate DESC ``` --- @@ -146,7 +149,7 @@ ORDER BY Status ASC, Name ASC, CreatedAt DESC **Request:** ``` -GET /api/users?query=(name = "alice" OR name = "bob") AND status = "active"&page=1&pageSize=10 +GET /api/customers?filter=((name = "alice" OR name = "bob") AND status = "active")&page=1&pageSize=10 ``` **Response:** @@ -174,13 +177,13 @@ GET /api/users?query=(name = "alice" OR name = "bob") AND status = "active"&page **Request:** ``` -GET /api/users?filter=status:in:active,trial,pending&sort=createdAt:desc&page=1&pageSize=20 +GET /api/orders?filter=status:in:pending,processing,shipped&sort=orderDate:desc&page=1&pageSize=20 ``` **SQL Generated:** ```sql -WHERE Status IN ('active', 'trial', 'pending') -ORDER BY CreatedAt DESC +WHERE Status IN ('pending', 'processing', 'shipped') +ORDER BY OrderDate DESC ``` --- @@ -189,13 +192,13 @@ ORDER BY CreatedAt DESC **Request:** ``` -GET /api/orders?filter=createdAt:between:2024-01-01,2024-12-31&sort=createdAt:asc +GET /api/orders?filter=orderDate:between:2024-01-01,2024-12-31&sort=orderDate:asc ``` **SQL Generated:** ```sql -WHERE CreatedAt BETWEEN '2024-01-01' AND '2024-12-31' -ORDER BY CreatedAt ASC +WHERE OrderDate BETWEEN '2024-01-01' AND '2024-12-31' +ORDER BY OrderDate ASC ``` --- @@ -204,14 +207,14 @@ ORDER BY CreatedAt ASC **Request:** ``` -GET /api/users?filter=deletedAt:isnull&sort=name:asc +GET /api/customers?filter=address:isnull&sort=name:asc ``` -Returns only users that have not been soft-deleted. +Returns only customers that have no address on file. **SQL Generated:** ```sql -WHERE DeletedAt IS NULL +WHERE AddressId IS NULL ORDER BY Name ASC ``` @@ -221,7 +224,7 @@ ORDER BY Name ASC **Request:** ``` -GET /api/users?filter=status:eq:active&page=1&pageSize=20&includeCount=false +GET /api/customers?filter=status:eq:active&page=1&pageSize=20&includeCount=false ``` **Response:** @@ -245,7 +248,7 @@ No `COUNT(*)` query is issued — faster for high-frequency endpoints where coun **Request:** ``` -GET /api/users?filter=passwordHash:isnotnull +GET /api/customers?filter=salary:isnotnull ``` **Response (400):** @@ -254,10 +257,10 @@ GET /api/users?filter=passwordHash:isnotnull "title": "Query validation failed", "errors": [ { - "field": "passwordHash", + "field": "salary", "code": "FIELD_ACCESS_DENIED", - "message": "Field 'passwordHash' is explicitly blocked." + "message": "Field 'salary' is explicitly blocked." } ] } -``` +``` \ No newline at end of file diff --git a/docs/examples/filtering.md b/docs/examples/filtering.md index e843405..f5134d3 100644 --- a/docs/examples/filtering.md +++ b/docs/examples/filtering.md @@ -8,7 +8,7 @@ Customers from Berlin or Paris, sorted by name. ### Request ```http -GET /api/customers?query=(city = "Berlin" OR city = "Paris") AND status = "Active"&sort=name:asc +GET /api/customers?filter=(city = "Berlin" OR city = "Paris") AND status = "Active"&sort=name asc ``` ### Response @@ -59,11 +59,11 @@ query.Where(c => c.Orders.Any(o => o.Status == "Cancelled" && o.Total > 500)) ## Multi-Level Nested `ANY` -Customers with a cancelled order that contains a specific product. +Customers with a cancelled order that contains a specific order item. ### Request ```http -GET /api/customers?filter=orders.any(status = "Cancelled" AND orderItems.any(productName CONTAINS "Laptop")) +GET /api/customers?filter=orders.any(status = "Cancelled" AND items.any(sku CONTAINS "Laptop")) ``` --- @@ -88,12 +88,12 @@ query.Where(c => c.Orders == null || c.Orders.All(o => o.Status == "Shipped" || ## IS NULL / IS NOT NULL -Customers without an assigned profile, or with a specific nested null check. +Customers without an assigned address, or with a specific nested null check. ### Request ```http -GET /api/customers?filter=profile isnull -GET /api/customers?filter=profile.bio notnull +GET /api/customers?filter=address isnull +GET /api/customers?filter=address.city notnull ``` --- diff --git a/docs/examples/formats.md b/docs/examples/formats.md index 2025759..dda9185 100644 --- a/docs/examples/formats.md +++ b/docs/examples/formats.md @@ -1,18 +1,216 @@ # Query Format Examples -FlexQuery.NET natively supports multiple formats to handle both simple URL requests and complex JSON objects sent from data-grid UI builders. +FlexQuery.NET supports multiple query formats to handle both simple URL requests and complex JSON objects sent from data-grid UI builders. -## DSL Format with Range + Set +--- + +## DSL Format (Default) + +The native FlexQuery DSL uses colon-separated values. It is compact and URL-friendly. + +### Simple Filter + +**Request:** +```http +GET /api/customers?filter=status:eq:active&sort=name:asc +``` + +### Compound Filter (AND) + +Multiple conditions are separated by commas (AND logic). URL-encode `&` as `%26`. + +**Request:** +```http +GET /api/customers?filter=status:eq:active%26city:eq:New York +``` + +### OR Logic + +**Request:** +```http +GET /api/customers?filter=status:eq:active|status:eq:pending +``` -Customers aged 25–40 in specific statuses using the compact Domain Specific Language. +### Range and Set Filter -### Request +**Request:** ```http GET /api/customers - ?filter=(age:between:25,40)%26(status:in:Active,Review) - &sort=age:asc + ?filter=(salary:between:50000,100000)%26(status:in:Active,Review) + &sort=salary:asc +``` + +### Collection Predicate (ANY) + +Filter customers who have at least one shipped order. + +**Request:** +```http +GET /api/customers?filter=orders:any:status:eq:shipped +``` + +### Response + +```json +{ + "totalCount": 12, + "resultCount": 12, + "page": 1, + "pageSize": 20, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "aggregates": null, + "data": [ + { "id": 1, "name": "Alice Chen", "status": "active", "salary": 75000 }, + { "id": 2, "name": "Bob Smith", "status": "active", "salary": 82000 } + ], + "nextCursorToken": null +} +``` + +--- + +## FQL Format (SQL-Like) + +FQL uses natural language expressions. It requires the `FlexQuery.NET.Parsers.Fql` package and `Fql.Register()` at startup. + +### Simple Filter + +**Request:** +```http +GET /api/customers?filter=status = "active" AND salary >= 50000 +``` + +### Nested Logic + +**Request:** +```http +GET /api/customers?filter=(name = "alice" OR name = "bob") AND status = "active" +``` + +### Collection Predicate + +**Request:** +```http +GET /api/customers?filter=Orders.any(Status = "shipped" AND Total > 100) +``` + +### Sort and Project + +**Request:** +```http +GET /api/customers?filter=status = "active" + &sort=name asc + &select=id,name,email +``` + +--- + +## JSON Format + +For complex nested filters, send a JSON object. This is useful when URL encoding becomes unwieldy. + +### Nested AND/OR + +**Request:** +```http +GET /api/customers?filter={"logic":"and","filters":[ + {"field":"status","operator":"eq","value":"active"}, + {"logic":"or","filters":[ + {"field":"salary","operator":"gte","value":"50000"}, + {"field":"name","operator":"contains","value":"senior"} + ]} +]} +``` + +Equivalent to: `status = "active" AND (salary >= 50000 OR name contains "senior")` + +### Response + +```json +{ + "totalCount": 8, + "resultCount": 8, + "page": 1, + "pageSize": 20, + "totalPages": 1, + "hasNextPage": false, + "hasPreviousPage": false, + "aggregates": null, + "data": [ + { "id": 1, "name": "Alice Chen", "status": "active", "salary": 75000 }, + { "id": 3, "name": "Senior Advisor", "status": "active", "salary": 90000 } + ], + "nextCursorToken": null +} ``` --- +## MiniOData Format + +MiniOData uses standard OData `$filter`, `$orderby`, `$select`, and `$expand` parameters. It requires the `FlexQuery.NET.Parsers.MiniOData` package and `MiniOData.Register()` at startup. + +### Filter and Sort + +**Request:** +```http +GET /api/customers?$filter=salary ge 50000 and status eq 'active' + &$orderby=name asc +``` + +### Select and Expand + +**Request:** +```http +GET /api/customers?$select=id,name,email + &$expand=orders +``` + +### Top and Skip + +**Request:** +```http +GET /api/customers?$top=20&$skip=40 +``` + +### Count + +**Request:** +```http +GET /api/customers?$filter=status eq 'active'&$count=true +``` + +--- + +## Format Comparison + +| Feature | DSL | FQL | JSON | MiniOData | +| :--- | :--- | :--- | :--- | :--- | +| **URL-friendly** | ✅ Very | ⚠️ Needs encoding | ⚠️ Needs encoding | ✅ Good | +| **Human-readable** | Medium | ✅ High | Medium | ✅ High | +| **Nested logic** | Limited | ✅ Full | ✅ Full | ✅ Full | +| **Package required** | Core (default) | `FlexQuery.NET.Parsers.Fql` | Core (default) | `FlexQuery.NET.Parsers.MiniOData` | +| **Registration** | None | `Fql.Register()` | None | `MiniOData.Register()` | +| **Best for** | Internal tools | Developer tools | Complex conditions | OData migration | + +--- + +## Auto-Detection + +When using the JSON or DSL format, FlexQuery.NET auto-detects the input format: + +1. **`$` prefix on parameter keys** — If any raw query parameter starts with `$` (e.g., `$filter`, `$orderby`), the MiniOData parser claims the request. +2. **JSON object** — If the `filter` parameter is a JSON object (starts with `{`), the JSON parser is used. +3. **DSL/FQL** — Otherwise, the configured default parser (DSL or FQL) handles the request. + +This means your API can seamlessly accept multiple formats on the **same endpoint** without structural changes. + +--- + +## Related Topics +- [Query Syntax](/guide/query-syntax) — How to configure and register parsers +- [Filtering](/guide/filtering) — All supported operators and filter patterns +- [Query Language Reference](/shared/query-language) — Complete syntax reference diff --git a/docs/examples/include.md b/docs/examples/include.md index 8b087db..c47ce83 100644 --- a/docs/examples/include.md +++ b/docs/examples/include.md @@ -42,8 +42,8 @@ Only customers named "Connelly", showing only their cancelled orders with a spec ### Request ```http GET /api/customers - ?query=name CONTAINS "Connelly" - &include=orders(status = "cancelled").orderItems(productName = "Tasty Metal Pants") + ?filter=name CONTAINS "Connelly" + &include=orders(status = "cancelled").items(sku = "Tasty Metal Pants") ``` --- @@ -82,12 +82,12 @@ GET /api/customers Customers who have shipped orders containing expensive items — shape the response to show only those child records. -> 💡 The `query` filters root entities (customers). The `include` pipeline independently shapes which child records appear. +> 💡 The `filter` filters root entities (customers). The `include` pipeline independently shapes which child records appear. ### Request ```http GET /api/customers - ?query=orders.any(status = "Shipped" AND orderItems.any(price > 100)) - &include=orders(status = "Shipped").orderItems(price > 100) - &select=id,name,orders.id,orders.total,orders.orderItems.productName,orders.orderItems.price + ?filter=orders.any(status = "Shipped" AND items.any(total > 100)) + &include=orders(status = "Shipped").items(total > 100) + &select=id,name,orders.id,orders.total,orders.items.sku,orders.items.total ``` diff --git a/docs/examples/real-world.md b/docs/examples/real-world.md index 6636f04..7c1c853 100644 --- a/docs/examples/real-world.md +++ b/docs/examples/real-world.md @@ -2,6 +2,8 @@ Complete, production-ready examples showing FlexQuery.NET in real application contexts. +> **Note:** Examples using `FlexQueryAsync` require the `FlexQuery.NET.EntityFrameworkCore` or `FlexQuery.NET.Dapper` package. The core `FlexQuery.NET` package provides synchronous `FlexQuery` for advanced scenarios. + --- ## Scenario 1: E-Commerce Order Dashboard @@ -12,13 +14,13 @@ An admin dashboard needs to filter orders by status, date range, and customer ```csharp public class Order { - public int Id { get; set; } - public int CustomerId { get; set; } - public string Status { get; set; } = ""; - public decimal Amount { get; set; } - public DateTime CreatedAt { get; set; } - public Customer Customer { get; set; } = null!; - public List Items { get; set; } = new(); + public int Id { get; set; } + public int CustomerId { get; set; } + public string OrderNumber { get; set; } = ""; + public decimal Total { get; set; } + public DateTime OrderDate { get; set; } + public string Status { get; set; } = ""; + public Customer Customer { get; set; } = null!; } ``` @@ -27,17 +29,17 @@ public class Order [HttpGet("orders")] public async Task GetOrders([FromQuery] FlexQueryParameters parameters, CancellationToken ct) { - var result = await _context.Orders.FlexQueryAsync(parameters, exec => + var result = await _context.Orders.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { - "id", "customerId", "status", "amount", "createdAt", + "id", "orderNumber", "total", "status", "orderDate", "customer.name", "customer.email" }; - exec.FilterableFields = new HashSet { "status", "amount", "createdAt", "customerId" }; - exec.SortableFields = new HashSet { "amount", "createdAt", "status" }; - exec.SelectableFields = new HashSet { "id", "status", "amount", "createdAt", "customer.name" }; - exec.BlockedFields = new HashSet { "customer.passwordHash" }; + exec.FilterableFields = new HashSet { "status", "total", "orderDate", "customerId" }; + exec.SortableFields = new HashSet { "total", "orderDate", "status" }; + exec.SelectableFields = new HashSet { "id", "status", "total", "orderDate", "customer.name" }; + exec.BlockedFields = new HashSet { "customer.email" }; exec.MaxFieldDepth = 2; }, ct); @@ -45,12 +47,12 @@ public async Task GetOrders([FromQuery] FlexQueryParameters param } ``` -**Sample Request — Orders in date range, sorted by amount:** +**Sample Request — Orders in date range, sorted by total:** ``` -GET /api/orders?filter=createdAt:between:2024-01-01,2024-12-31,status:in:pending,processing - &sort=amount:desc +GET /api/orders?filter=orderDate:between:2024-01-01,2024-12-31,status:in:pending,processing + &sort=total:desc &page=1&pageSize=25 - &select=id,status,amount,createdAt,customer.name + &select=id,status,total,orderDate,customer.name ``` **Response:** @@ -68,15 +70,15 @@ GET /api/orders?filter=createdAt:between:2024-01-01,2024-12-31,status:in:pending { "id": 1001, "status": "processing", - "amount": 1250.00, - "createdAt": "2024-11-15T08:00:00Z", + "total": 1250.00, + "orderDate": "2024-11-15T08:00:00Z", "customer": { "name": "Alice Chen" } }, { "id": 998, "status": "pending", - "amount": 870.50, - "createdAt": "2024-11-14T16:30:00Z", + "total": 870.50, + "orderDate": "2024-11-14T16:30:00Z", "customer": { "name": "Bob Smith" } } ], @@ -86,23 +88,23 @@ GET /api/orders?filter=createdAt:between:2024-01-01,2024-12-31,status:in:pending --- -## Scenario 2: Multi-Tenant SaaS User Management +## Scenario 2: Multi-Tenant SaaS Customer Management A SaaS platform with admin and viewer roles — each role sees different fields. **Controller:** ```csharp -[HttpGet("users")] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters, CancellationToken ct) +[HttpGet("customers")] +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters, CancellationToken ct) { var isAdmin = User.IsInRole("admin"); - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { // Base fields available to all roles exec.AllowedFields = new HashSet { - "id", "name", "email", "status", "createdAt" + "id", "name", "email", "status", "city" }; // Role-specific additional fields @@ -110,8 +112,8 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame { ["admin"] = new HashSet { - "id", "name", "email", "status", "createdAt", - "salary", "internalRating", "lastLoginAt", "tenantId" + "id", "name", "email", "status", "city", + "salary", "createdDate" }, ["viewer"] = new HashSet { @@ -120,7 +122,7 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame }; exec.CurrentRole = isAdmin ? "admin" : "viewer"; - exec.BlockedFields = new HashSet { "passwordHash", "twoFactorSecret" }; + exec.BlockedFields = new HashSet { "email" }; exec.MaxFieldDepth = 2; // Scope to current tenant automatically @@ -133,12 +135,12 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame **Admin Request:** ``` -GET /api/users?filter=salary:gt:80000&select=id,name,salary,internalRating&sort=salary:desc +GET /api/customers?filter=salary:gt:80000&select=id,name,salary,city&sort=salary:desc ``` **Viewer Request (same endpoint):** ``` -GET /api/users?filter=salary:gt:80000 +GET /api/customers?filter=salary:gt:80000 ``` **Viewer Response (400):** @@ -156,58 +158,60 @@ GET /api/users?filter=salary:gt:80000 --- -## Scenario 3: Product Catalog with Filtered Relations +## Scenario 3: Customer Catalog with Filtered Relations **Entity:** ```csharp -public class Product +public class Customer { - public int Id { get; set; } - public string Name { get; set; } = ""; - public string Category { get; set; } = ""; - public decimal Price { get; set; } - public List Reviews { get; set; } = new(); - public List Tags { get; set; } = new(); + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Email { get; set; } = ""; + public string City { get; set; } = ""; + public string Status { get; set; } = ""; + public decimal Salary { get; set; } + public DateTime CreatedDate { get; set; } + public List Orders { get; set; } = new(); } ``` -**Request — Products in electronics, with 5-star reviews only:** +**Request — Customers in New York, with recent orders only:** ``` -GET /api/products?filter=category:eq:electronics,price:lte:500 - &include=Reviews(rating:eq:5) - &sort=price:asc +GET /api/customers?filter=city:eq:New York,salary:gte:50000 + &include=Orders(status:eq:shipped) + &sort=salary:desc &page=1&pageSize=10 - &select=id,name,price,category + &select=id,name,email,city,orders.orderNumber,orders.total ``` **Response:** ```json { - "totalCount": 28, - "resultCount": 28, + "totalCount": 12, + "resultCount": 12, "page": 1, "pageSize": 10, - "totalPages": 3, + "totalPages": 2, "hasNextPage": true, "hasPreviousPage": false, "aggregates": null, "data": [ { "id": 5, - "name": "Wireless Earbuds", - "price": 89.99, - "category": "electronics", - "reviews": [ - { "id": 201, "rating": 5, "comment": "Amazing sound quality!" }, - { "id": 208, "rating": 5, "comment": "Best earbuds I've owned." } + "name": "Carol White", + "email": "carol@example.com", + "city": "New York", + "orders": [ + { "id": 501, "orderNumber": "ORD-501", "total": 299.99 }, + { "id": 508, "orderNumber": "ORD-508", "total": 149.50 } ] }, { "id": 12, - "name": "USB-C Hub", - "price": 45.00, - "category": "electronics", - "reviews": [] + "name": "Dave Brown", + "email": "dave@example.com", + "city": "New York", + "orders": [] } ], "nextCursorToken": null @@ -218,7 +222,7 @@ GET /api/products?filter=category:eq:electronics,price:lte:500 ## Scenario 4: Analytics / Reporting Endpoint -**Scenario:** Monthly revenue report grouped by region and product category. +**Scenario:** Monthly revenue report grouped by city and customer status. **Controller:** ```csharp @@ -226,11 +230,11 @@ GET /api/products?filter=category:eq:electronics,price:lte:500 [Authorize(Roles = "admin,analyst")] public async Task GetRevenueReport([FromQuery] FlexQueryParameters parameters, CancellationToken ct) { - var result = await _context.Orders.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { - "status", "region", "category", "amount", "createdAt" + "city", "status", "salary", "createdDate" }; exec.MaxFieldDepth = 1; }, ct); @@ -241,11 +245,11 @@ public async Task GetRevenueReport([FromQuery] FlexQueryParameter **Request:** ``` -GET /api/reports/revenue?filter=createdAt:between:2024-01-01,2024-12-31,status:eq:completed - &select=region,count(),sum(amount),avg(amount) - &groupBy=region - &having=sum(amount):gt:10000 - &sort=sum(amount):desc +GET /api/reports/revenue?filter=createdDate:between:2024-01-01,2024-12-31,status:eq:active + &select=city,count(),sum(salary),avg(salary) + &groupBy=city + &having=sum(salary):gt:50000 + &sort=sum(salary):desc ``` **Response:** @@ -257,9 +261,9 @@ GET /api/reports/revenue?filter=createdAt:between:2024-01-01,2024-12-31,status:e "pageSize": 20, "aggregates": null, "data": [ - { "region": "North America", "allCount": 512, "amountSum": 128000.00, "amountAvg": 250.00 }, - { "region": "Europe", "allCount": 380, "amountSum": 95000.00, "amountAvg": 250.00 }, - { "region": "Asia Pacific", "allCount": 210, "amountSum": 52500.00, "amountAvg": 250.00 } + { "city": "New York", "allCount": 24, "salarySum": 180000.00, "salaryAvg": 7500.00 }, + { "city": "London", "allCount": 18, "salarySum": 126000.00, "salaryAvg": 7000.00 }, + { "city": "Singapore", "allCount": 12, "salarySum": 72000.00, "salaryAvg": 6000.00 } ], "nextCursorToken": null } @@ -270,16 +274,16 @@ GET /api/reports/revenue?filter=createdAt:between:2024-01-01,2024-12-31,status:e ## Scenario 5: Minimal API (No Controller) ```csharp -app.MapGet("/api/products", async ( +app.MapGet("/api/customers", async ( [AsParameters] FlexQueryParameters parameters, AppDbContext db, CancellationToken ct) => { try { - var result = await db.Products.FlexQueryAsync(parameters, exec => + var result = await db.Customers.FlexQueryAsync(parameters, exec => { - exec.AllowedFields = new HashSet { "id", "name", "price", "category", "inStock" }; + exec.AllowedFields = new HashSet { "id", "name", "email", "city", "status" }; exec.MaxFieldDepth = 1; }, ct); @@ -294,5 +298,5 @@ app.MapGet("/api/products", async ( **Request:** ``` -GET /api/products?filter=inStock:eq:true,price:between:10,100&sort=price:asc&page=1&pageSize=20 +GET /api/customers?filter=city:eq:New York,status:eq:active&sort=salary:desc&page=1&pageSize=20 ``` diff --git a/docs/guide/aspnet-integration.md b/docs/guide/aspnet-integration.md index dec116c..a7359c7 100644 --- a/docs/guide/aspnet-integration.md +++ b/docs/guide/aspnet-integration.md @@ -28,7 +28,7 @@ Unlike v3, FlexQuery.NET v4 uses a static configuration model. To enable the dec ```csharp using FlexQuery.NET; -using FlexQuery.NET.AspNetCore.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); @@ -44,6 +44,9 @@ builder.Services.AddControllers() .AddFlexQuerySecurity(); var app = builder.Build(); + +app.MapControllers(); +app.Run(); ``` > [!NOTE] @@ -58,21 +61,23 @@ The `[FieldAccess]` attribute allows you to define field security rules directly ```csharp [HttpGet] [FieldAccess( - AllowedFields = new[] { "Id", "Name", "Email", "Status" }, - FilterableFields = new[] { "Name", "Status" }, - SortableFields = new[] { "Name", "CreatedAt" }, - MaxFieldDepth = 2 + Allowed = new[] { "Id", "Name", "Email", "Status" }, + Filterable = new[] { "Name", "Status" }, + Sortable = new[] { "Name", "CreatedAt" }, + Selectable = new[] { "Id", "Name", "Email" }, + MaxDepth = 2 )] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { // Retrieve the security rules populated by the [FieldAccess] filter var execOptions = HttpContext.GetFlexQueryExecutionOptions(); - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = execOptions.AllowedFields; exec.FilterableFields = execOptions.FilterableFields; exec.SortableFields = execOptions.SortableFields; + exec.SelectableFields = execOptions.SelectableFields; exec.MaxFieldDepth = execOptions.MaxFieldDepth; }); @@ -128,10 +133,13 @@ builder.Services.AddControllers(o => ```csharp [HttpGet] -public async Task>> GetUsers( +public async Task>> GetCustomers( [FromQuery] FlexQueryParameters parameters) { - return await _context.Users.FlexQueryAsync(parameters, HttpContext); + return await _context.Customers.FlexQueryAsync(parameters, exec => + { + exec.AllowedFields = new HashSet { "Id", "Name", "Email" }; + }); } ``` @@ -151,11 +159,11 @@ Swagger UI will automatically display the query string parameters bound to the o The `[FieldAccess]` attribute and `AddFlexQuerySecurity()` pipeline are designed specifically for the MVC/Web API Controller pipeline. For Minimal APIs, we strongly recommend manual configuration using the inline lambda, which is highly performant and explicit: ```csharp -app.MapGet("/api/users", async ( +app.MapGet("/api/customers", async ( [AsParameters] FlexQueryParameters parameters, AppDbContext db) => { - var result = await db.Users.FlexQueryAsync(parameters, exec => + var result = await db.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = ["Id", "Name", "Email"]; exec.MaxFieldDepth = 2; diff --git a/docs/guide/basic-usage.md b/docs/guide/basic-usage.md index 05bb809..e74d6ac 100644 --- a/docs/guide/basic-usage.md +++ b/docs/guide/basic-usage.md @@ -4,6 +4,8 @@ FlexQuery.NET uses a consistent, human-readable DSL (Domain Specific Language) for dynamic querying. This guide provides a rapid introduction to the standard formats for filtering, sorting, paging, and projection. +> **Note:** Examples using `FlexQueryAsync` require the `FlexQuery.NET.EntityFrameworkCore` or `FlexQuery.NET.Dapper` package. The core `FlexQuery.NET` package provides synchronous `FlexQuery` for advanced scenarios. + ## Why this feature exists When building APIs, there is a constant tension between backend rigidity and frontend flexibility. The FlexQuery DSL exists to provide a standardized, secure, and easily-parsable syntax that frontends can use to request exact data shapes without forcing backend developers to write custom SQL or LINQ for every view. @@ -98,10 +100,10 @@ In FlexQuery v4, all these features are applied in a single unified pipeline. Yo ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { // Execute everything in one pass - var result = await _context.Users.FlexQueryAsync(parameters, options => + var result = await _context.Customers.FlexQueryAsync(parameters, options => { // Enforce your security rules options.AllowedFields = ["Id", "Name", "Price", "Category.Name", "Status"]; @@ -112,15 +114,17 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame ``` ### HTTP POST Requests + If your query is too large for a URL query string, you can use the `FlexQueryRequest` model to accept the query via a JSON POST body. The properties are exactly the same as `FlexQueryParameters`. ```csharp [HttpPost("query")] public async Task QueryUsers([FromBody] FlexQueryRequest request) { - var result = await _context.Users.FlexQueryAsync(request, options => + var options = request.ToQueryOptions(); + var result = await _context.Customers.FlexQueryAsync(options, exec => { - options.AllowedFields = ["Id", "Name", "Price", "Status"]; + exec.AllowedFields = new HashSet { "Id", "Name", "Price", "Status" }; }); return Ok(result); diff --git a/docs/guide/comparison-libraries.md b/docs/guide/comparison-libraries.md index a46ecf9..3905824 100644 --- a/docs/guide/comparison-libraries.md +++ b/docs/guide/comparison-libraries.md @@ -82,7 +82,7 @@ It supports: All in a **single method call**. ```http -GET /api/users?filter=Name:contains:John&select=Name,Orders.Status&include=Orders +GET /api/customers?filter=Name:contains:John&select=Name,Orders.Status&include=Orders ``` --- @@ -152,11 +152,11 @@ Parse → Validate → Execute (single pipeline) ### 🟦 FlexQuery.NET ```http -GET /api/users?filter=Name:contains:John&include=Orders&select=Name,Orders.Status&sort=CreatedAt:desc +GET /api/customers?filter=Name:contains:John&include=Orders&select=Name,Orders.Status&sort=CreatedDate:desc ``` ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, options => +var result = await _context.Customers.FlexQueryAsync(parameters, options => { // Security policy enforced inline options.AllowedFields = ["Name", "Orders.Status", "CreatedAt"]; @@ -170,7 +170,7 @@ var result = await _context.Users.FlexQueryAsync(parameters, options => ```csharp var mapper = new GridifyMapper().GenerateDefaultMap(); -var query = _context.Users +var query = _context.Customers .Include(u => u.Orders) .Gridify(queryObj, mapper); @@ -186,15 +186,15 @@ var result = query.Data.Select(u => new ### 🟥 Sieve ```csharp -var query = _context.Users.Include(u => u.Orders); +var query = _context.Customers.Include(c => c.Orders); // Sieve applies the filters/sorts based on attributes query = _sieveProcessor.Apply(sieveModel, query); -var result = await query.Select(u => new +var result = await query.Select(c => new { - u.Name, - Orders = u.Orders.Select(o => new { o.Status }) + c.Name, + Orders = c.Orders.Select(o => new { o.Status }) }).ToListAsync(); ``` diff --git a/docs/guide/comparison.md b/docs/guide/comparison.md index defbc53..c11f128 100644 --- a/docs/guide/comparison.md +++ b/docs/guide/comparison.md @@ -208,13 +208,13 @@ OData emphasizes standardized metadata-driven REST interoperability, but brings ### FlexQuery.NET (DSL) ```http -GET /api/users?filter=orders:any:status:eq:shipped +GET /api/customers?filter=orders.any(status:eq:'shipped') ``` ### FlexQuery.NET (FQL) ```http -GET /api/users?query=Orders.any(Status = "shipped") +GET /api/customers?filter=Orders.any(Status = "shipped") ``` ### GraphQL @@ -243,7 +243,7 @@ query { ### OData ```http -GET /api/users?$filter=orders/any(o: o/Status eq 'shipped') +GET /api/customers?$filter=orders/any(o: o/Status eq 'shipped') ``` All three approaches support nested collection filtering, but with different query styles and ecosystem expectations. @@ -267,9 +267,9 @@ All three approaches support nested collection filtering, but with different que ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { // Security policy enforced immediately inline exec.AllowedFields = ["id", "name", "email", "status"]; @@ -300,7 +300,7 @@ public class Query [UseProjection] [UseFiltering] [UseSorting] - public IQueryable GetUsers([ScopedService] AppDbContext db) => db.Users; + public IQueryable GetCustomers([ScopedService] AppDbContext db) => db.Customers; } ``` @@ -323,9 +323,9 @@ builder.Services [EnableQuery] [HttpGet] -public IQueryable GetUsers() +public IQueryable GetCustomers() { - return _context.Users; + return _context.Customers; } ``` diff --git a/docs/guide/core-concepts.md b/docs/guide/core-concepts.md index eba402c..bb14813 100644 --- a/docs/guide/core-concepts.md +++ b/docs/guide/core-concepts.md @@ -20,8 +20,8 @@ FlexQuery is a pipeline engine. To effectively debug complex projection queries, FlexQuery.NET exposes two complementary API layers: | API Level | Recommended For | Entry Point | -|---|---|---| -| **High-Level API** | Controllers, APIs, frontend-driven filtering | `FlexQueryParameters` + `FlexQueryAsync()` | +| :--- | :--- | :--- | +| **High-Level API** | Controllers, APIs, frontend-driven filtering | `FlexQueryParameters` + `FlexQueryAsync()` (EF Core/Dapper) or `FlexQuery()` (Core) | | **Advanced API** | Server-side query composition, strongly-typed filters | `QueryOptions` + `ApplyFilter()`, `ApplySort()` | Most applications should use the high-level API. @@ -50,7 +50,7 @@ HTTP Query String ▼ QueryOptions ← The internal parsed model │ - ├── ValidateOrThrow() ← Enforces Server Policy (Field access, depth) + │ (FlexQueryAsync includes validation automatically) │ ├── ApplyFilter() ← AST → SQL WHERE / Expression Tree ├── ApplySort() ← AST → SQL ORDER BY @@ -124,7 +124,7 @@ While `QueryOptions` represents what the *client wants*, `BaseQueryOptions` (and You configure this via the lambda in `FlexQueryAsync`: ```csharp -var result = await _db.Users.FlexQueryAsync(parameters, exec => +var result = await _db.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "Id", "Name", "Email" }; exec.MaxFieldDepth = 2; @@ -136,9 +136,26 @@ var result = await _db.Users.FlexQueryAsync(parameters, exec => | :--- | :--- | | `AllowedFields` | Global allow-list. If a client requests a field not on this list, it is rejected. | | `BlockedFields` | Explicitly blocked fields (e.g. `PasswordHash`). Overrides `AllowedFields`. | +| `FilterableFields` | Fields allowed specifically in filter expressions. | +| `SortableFields` | Fields allowed specifically in sort expressions. | +| `SelectableFields` | Fields allowed specifically in projection/select expressions. | +| `GroupableFields` | Fields allowed specifically in group-by expressions. | +| `AggregatableFields` | Fields allowed specifically in aggregate expressions. | +| `AllowedIncludes` | Navigation properties allowed for include/expand. | +| `AllowedOperators` | Per-field operator restrictions. | | `MaxFieldDepth` | Maximum dot-notation path depth. `2` allows `Category.Name` but blocks `Category.Company.Name`. | | `MaxPageSize` | Hard cap on `pageSize` to prevent memory exhaustion. | | `StrictFieldValidation` | `true` = Throw exception on violation. `false` = Silently strip unauthorized fields from the query. | +| `DefaultSortField` | Default sort field when client doesn't specify. | +| `DefaultSortDescending` | Default sort direction. | +| `CaseInsensitive` | Whether field name matching is case-insensitive. | +| `IncludeTotalCount` | Whether to include total count by default. | +| `ExpressionMappings` | Maps DTO field aliases to entity expressions. | +| `FieldMappings` | Maps external field aliases to internal property names. | +| `RoleAllowedFields` | Per-role field allow-lists. | +| `CurrentRole` | The active role for evaluating `RoleAllowedFields`. | +| `AllowedFieldsResolver` | Dynamic resolver for allowed fields based on entity type. | +| `Listener` | Optional execution event listener. | --- @@ -148,13 +165,13 @@ FlexQuery supports multiple query syntaxes. The active syntax is configured glob ### DSL Format (Default) ```http -GET /api/users?filter=Status:eq:Active%26LastName:contains:Smi&sort=LastName:asc +GET /api/customers?filter=Status:eq:Active%26LastName:contains:Smi&sort=LastName:asc ``` ### FQL Format (SQL-like) Requires `Fql.Register()` at startup and `QuerySyntax.Fql`: ```http -GET /api/users?filter=Status = "Active" AND LastName CONTAINS "Smi"&sort=LastName:asc +GET /api/customers?filter=Status = "Active" AND LastName CONTAINS "Smi"&sort=LastName:asc ``` @@ -185,8 +202,8 @@ Preserves the object hierarchy: { "id": 1, "name": "Alice", - "profile": { - "bio": "Developer" + "address": { + "city": "Singapore" } } ``` @@ -199,7 +216,7 @@ Flattens all properties to top-level with dot-notation keys: { "id": 1, "name": "Alice", - "profile.bio": "Developer" + "address.bio": "Developer" } ``` @@ -211,7 +228,7 @@ Scalars at the top level, collections remain nested: { "id": 1, "name": "Alice", - "profile_bio": "Developer" + "address_city": "Singapore" } ``` @@ -239,15 +256,20 @@ Every high-level method returns a standardized envelope `QueryResult`: } ``` -| Property | Type | Description | -| :--- | :--- | :--- | -| `data` | `List` | The current page of results. | -| `totalCount` | `int?` | Total records before paging (null if `IncludeCount=false` or keyset pagination is used). | -| `resultCount` | `int?` | Count of records after grouping (often equals `totalCount`). | -| `page` | `int` | Current page number. | -| `pageSize` | `int` | Items per page. | -| `totalPages` | `int` | Computed `Ceiling(resultCount / pageSize)`. | -| `nextCursorToken` | `string?` | Keyset cursor for the next page, used in high-performance paging. | +> **Note:** The JSON properties above use camelCase due to default .NET JSON serialization. The actual C# properties on `QueryResult` use PascalCase. + +| C# Property | JSON Name | Type | Description | +| :--- | :--- | :--- | :--- | +| `TotalCount` | `totalCount` | `int?` | Total records before paging (null if `IncludeCount=false` or keyset pagination is used). | +| `ResultCount` | `resultCount` | `int?` | Count of records after grouping (often equals `TotalCount`). | +| `Page` | `page` | `int` | Current page number. | +| `PageSize` | `pageSize` | `int` | Items per page. | +| `TotalPages` | `totalPages` | `int` | Computed `Ceiling(ResultCount / pageSize)`. | +| `HasNextPage` | `hasNextPage` | `bool` | Whether a next page exists. | +| `HasPreviousPage` | `hasPreviousPage` | `bool` | Whether a previous page exists. | +| `Aggregates` | `aggregates` | `Dictionary>?` | Grand total aggregate results. | +| `Data` | `data` | `IReadOnlyList` | The current page of results. | +| `NextCursorToken` | `nextCursorToken` | `string?` | Keyset cursor for the next page, used in high-performance paging. | ## Best Practices - **Never skip validation:** Whether you use `FlexQueryAsync` or the manual pipeline, never trust client query definitions blindly. Always configure an execution policy. diff --git a/docs/guide/debugging.md b/docs/guide/debugging.md index 9e17b14..e2c4f43 100644 --- a/docs/guide/debugging.md +++ b/docs/guide/debugging.md @@ -155,7 +155,7 @@ Or you can use `ToQueryString()` in the manual pipeline to inspect the SQL witho ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); query = query.ApplyFilter(options); query = query.ApplySort(options); @@ -186,7 +186,7 @@ ORDER BY [u].[Name] ### "Results are empty but shouldn't be" 1. Verify case sensitivity: `options.CaseInsensitive` is `true` by default, but if you turned it off, "Alice" and "alice" will not match. -2. Check if a server-side pre-filter is excluding results before FlexQuery runs (e.g., `_context.Users.Where(u => u.TenantId == 1).FlexQueryAsync(...)`). +2. Check if a server-side pre-filter is excluding results before FlexQuery runs (e.g., `_context.Customers.Where(c => c.CustomerId == 1).FlexQueryAsync(...)`). 3. Use `query.ToQueryString()` or SQL Profiler to see the exact SQL generated. ### "Validation is rejecting a valid field" diff --git a/docs/guide/dotnet-comparison.md b/docs/guide/dotnet-comparison.md index 24fc9ce..8fceb8a 100644 --- a/docs/guide/dotnet-comparison.md +++ b/docs/guide/dotnet-comparison.md @@ -93,14 +93,14 @@ Because expressions are string-based and parsed blindly, applications typically ### FlexQuery.NET ```http -GET /api/users?filter=status:eq:active%26age:gte:18&sort=name:asc&page=2&pageSize=10 +GET /api/customers?filter=status:eq:active%26salary:gte:50000&sort=name:asc&page=2&pageSize=10 ``` ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { // Enforce server policy exec.AllowedFields = new HashSet @@ -120,14 +120,14 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame ### Gridify ```http -GET /api/users?filter=status=active,age>=18&orderBy=name&page=2&pageSize=10 +GET /api/customers?filter=status=active,salary>=50000&orderBy=name&page=2&pageSize=10 ``` ```csharp [HttpGet] -public async Task GetUsers([FromQuery] GridifyQuery query) +public async Task GetCustomers([FromQuery] GridifyQuery query) { - var result = await _context.Users.GridifyAsync(query); + var result = await _context.Customers.GridifyAsync(query); return Ok(result); } ``` @@ -139,16 +139,16 @@ Gridify intentionally keeps configuration lightweight and focused purely on filt ### Sieve ```http -GET /api/users?filters=Status==active,Age>=18&sorts=Name&page=2&pageSize=10 +GET /api/customers?filters=Status==active,Salary>=50000&sorts=Name&page=2&pageSize=10 ``` ```csharp -public class UserSieveProcessor : SieveProcessor +public class CustomerSieveProcessor : SieveProcessor { - public UserSieveProcessor(IOptions options) : base(options) { } + public CustomerSieveProcessor(IOptions options) : base(options) { } } -public class User +public class Customer { [Sieve(CanFilter = true, CanSort = true)] public string Name { get; set; } @@ -157,13 +157,13 @@ public class User public string Status { get; set; } [Sieve(CanFilter = true)] - public int Age { get; set; } + public decimal Salary { get; set; } } [HttpGet] -public async Task GetUsers([FromQuery] SieveModel model) +public async Task GetCustomers([FromQuery] SieveModel model) { - var query = _sieveProcessor.Apply(model, _context.Users); + var query = _sieveProcessor.Apply(model, _context.Customers); return Ok(await query.ToListAsync()); } ``` @@ -176,13 +176,13 @@ Sieve emphasizes declarative configuration through attributes. ```csharp [HttpGet] -public async Task GetUsers( +public async Task GetCustomers( string? filter, string? sort, int page = 1, int pageSize = 10) { - var query = _context.Users.AsQueryable(); + var query = _context.Customers.AsQueryable(); if (!string.IsNullOrEmpty(filter)) query = query.Where(filter); diff --git a/docs/guide/execution-pipeline.md b/docs/guide/execution-pipeline.md index db1bf64..ffea4bc 100644 --- a/docs/guide/execution-pipeline.md +++ b/docs/guide/execution-pipeline.md @@ -41,15 +41,17 @@ These extension methods provide: ## High-Level: FlexQueryAsync ⭐ Recommended -`FlexQueryAsync` is the **unified pipeline method**. It parses, validates, and executes in a single call. +`FlexQueryAsync` (available in `FlexQuery.NET.EntityFrameworkCore` and `FlexQuery.NET.Dapper`) is the **unified pipeline method**. It parses, validates, and executes in a single call. **When to use:** Any standard public API endpoint. ```csharp +using FlexQuery.NET.EntityFrameworkCore; + [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = ["Id", "Name", "Email", "Status"]; exec.MaxFieldDepth = 2; @@ -63,7 +65,7 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame ```text ToQueryOptions(parameters) - → ValidateOrThrow(execOptions) + → Validate (field access, operators, depth against Server Policy) → ApplyFilter → ApplySort → CountAsync (if IncludeCount = true and not keyset) @@ -93,9 +95,9 @@ var filtered = query.ApplyFilter(options); - Builds an expression tree; EF Core translates it to SQL. **Example:** -`GET /api/users?filter=Status:eq:active` +`GET /api/customers?filter=Status:eq:active` ```sql -SELECT * FROM Users WHERE Status = 'active' +SELECT * FROM Customers WHERE Status = 'active' ``` --- @@ -112,9 +114,9 @@ var sorted = query.ApplySort(options); - No-op if `options.Sort` is empty. **Example:** -`GET /api/users?sort=Name:asc,CreatedAt:desc` +`GET /api/customers?sort=Name:asc,CreatedDate:desc` ```sql -ORDER BY Name ASC, CreatedAt DESC +ORDER BY Name ASC, CreatedDate DESC ``` --- @@ -143,10 +145,10 @@ var withIncludes = query.ApplyExpand(options); - No-op if `options.Expand` is null or empty. **Example:** -`GET /api/users?include=Orders(Status:eq:shipped)` +`GET /api/customers?include=Orders(Status:eq:shipped)` ```csharp // Translates internally to: -query.Include(u => u.Orders.Where(o => o.Status == "shipped")) +query.Include(c => c.Orders.Where(o => o.Status == "shipped")) ``` --- @@ -177,7 +179,7 @@ var data = await projected.ToListAsync(); // ❌ DO NOT DO THIS var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); // 1st Filter: Applied here manually query = query.ApplyFilter(options); @@ -191,7 +193,7 @@ var result = await query.FlexQueryAsync(options); ```csharp // ✅ CORRECT: Everything in one call, filter applied once -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = ["Id", "Name", "Email"]; }); @@ -205,7 +207,7 @@ For when you need full control — e.g., injecting custom tenant filter between ```csharp [HttpGet] -public async Task GetUsersManual([FromQuery] FlexQueryParameters parameters, CancellationToken ct) +public async Task GetCustomersManual([FromQuery] FlexQueryParameters parameters, CancellationToken ct) { // 1. Parse var options = parameters.ToQueryOptions(); @@ -213,14 +215,14 @@ public async Task GetUsersManual([FromQuery] FlexQueryParameters // 2. Validate against Server Policy var execOptions = new EfCoreQueryOptions { - AllowedFields = ["Id", "Name", "Email", "Status", "CreatedAt"], + AllowedFields = ["Id", "Name", "Email", "Status", "City", "CreatedDate"], MaxFieldDepth = 2 }; - options.ValidateOrThrow(execOptions); + options.ValidateOrThrow(execOptions); // 3. Start query with strict Tenancy limits - var query = _context.Users - .Where(u => u.TenantId == CurrentTenantId) + var query = _context.Customers + .Where(u => u.CustomerId == CurrentTenantId) .AsQueryable(); // 4. Apply FlexQuery filter and sort diff --git a/docs/guide/extension-methods.md b/docs/guide/extension-methods.md index 7a800db..f402bd3 100644 --- a/docs/guide/extension-methods.md +++ b/docs/guide/extension-methods.md @@ -20,7 +20,7 @@ The all-in-one unified pipeline method. Handles parsing, validation, filtering, [HttpGet] public async Task Get([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = ["Id", "Name", "Email"]; exec.MaxFieldDepth = 2; @@ -125,7 +125,7 @@ If you need to project or map the results inside a `QueryResult` *after* exec Casts the underlying `Data` collection from one type to another. ```csharp -var entityResult = await _context.Users.FlexQueryAsync(options); +var entityResult = await _context.Customers.FlexQueryAsync(options); var dtoResult = entityResult.ToProjectedQueryResult(); ``` diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index 674f02f..ec272c0 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -39,15 +39,15 @@ FlexQuery.NET supports multiple filter syntaxes. The active syntax is configured Simple colon-delimited expressions. ``` -GET /api/users?filter=status:eq:active -GET /api/users?filter=age:gte:18 -GET /api/users?filter=name:contains:alice +GET /api/customers?filter=status:eq:active +GET /api/customers?filter=salary:gte:50000 +GET /api/customers?filter=name:contains:alice ``` **Compound (AND — URL-encode `&` as `%26`):** ```http -GET /api/users?filter=status:eq:active%26age:gte:18 +GET /api/customers?filter=status:eq:active%26city:eq:'New York' ``` ### FQL Format (SQL-like) @@ -60,8 +60,8 @@ Fql.Register(); ``` ``` -GET /api/users?filter=status = "active" AND age >= 18 -GET /api/users?filter=(name = "alice" OR name = "bob") AND status = "active" +GET /api/customers?filter=status = "active" AND salary >= 50000 +GET /api/customers?filter=(name = "alice" OR name = "bob") AND status = "active" ``` @@ -73,10 +73,10 @@ GET /api/users?filter=(name = "alice" OR name = "bob") AND status = "active" | :--- | :--- | :--- | | `eq` | Equals | `status:eq:active` | | `neq` | Not equals | `status:neq:deleted` | -| `gt` | Greater than | `age:gt:18` | -| `gte` | Greater than or equal | `age:gte:18` | -| `lt` | Less than | `price:lt:100` | -| `lte` | Less than or equal | `price:lte:100` | +| `gt` | Greater than | `salary:gt:50000` | +| `gte` | Greater than or equal | `salary:gte:50000` | +| `lt` | Less than | `salary:lt:100000` | +| `lte` | Less than or equal | `salary:lte:100000` | | `contains` | String contains | `name:contains:alice` | | `startswith` | String starts with | `email:startswith:admin` | | `endswith` | String ends with | `email:endswith:.com` | @@ -101,7 +101,7 @@ GET /api/users?filter=(name = "alice" OR name = "bob") AND status = "active" ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var filtered = query.ApplyFilter(options); var users = await filtered.ToListAsync(); ``` @@ -110,12 +110,12 @@ var users = await filtered.ToListAsync(); Request: ``` -GET /api/users?filter=orders:any:status:eq:shipped +GET /api/customers?filter=orders:any:status:eq:shipped ``` This translates to: ```csharp -_context.Users.Where(u => u.Orders.Any(o => o.Status == "shipped")) +_context.Customers.Where(u => u.Orders.Any(o => o.Status == "shipped")) ``` SQL: @@ -129,14 +129,14 @@ SELECT * FROM Users u WHERE EXISTS ( Request (FQL syntax): ``` -GET /api/users?filter=Orders.any(Status = "shipped" AND Amount > 100) +GET /api/customers?filter=Orders.any(Status = "shipped" AND Total > 100) ``` ### Range Filter Request: ``` -GET /api/users?filter=age:between:18,65 +GET /api/customers?filter=salary:between:50000,150000 ``` SQL: @@ -148,7 +148,7 @@ SELECT * FROM Users WHERE Age BETWEEN 18 AND 65 Request: ``` -GET /api/users?filter=status:in:active,pending,trial +GET /api/customers?filter=status:in:active,pending,trial ``` SQL: @@ -162,7 +162,7 @@ SELECT * FROM Users WHERE Status IN ('active', 'pending', 'trial') **Request:** ``` -GET /api/users?filter=status:eq:active&page=1&pageSize=3 +GET /api/customers?filter=status:eq:active&page=1&pageSize=3 ``` **Response:** @@ -193,12 +193,12 @@ GET /api/users?filter=status:eq:active&page=1&pageSize=3 ```csharp // WRONG — client can filter on any field, including sensitive ones -var result = await _context.Users.FlexQueryAsync(parameters); +var result = await _context.Customers.FlexQueryAsync(parameters); ``` ```csharp // CORRECT -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "name", "email", "status" }; }); @@ -208,14 +208,14 @@ var result = await _context.Users.FlexQueryAsync(parameters, exec => ```csharp // WRONG — tenant filter applied after FlexQuery, may expose cross-tenant data -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); query = query.ApplyFilter(options); query = query.Where(u => u.TenantId == tenantId); // too late ``` ```csharp // CORRECT — always apply server-side constraints FIRST -var query = _context.Users.Where(u => u.TenantId == tenantId); +var query = _context.Customers.Where(u => u.TenantId == tenantId); query = query.ApplyFilter(options); ``` diff --git a/docs/guide/flattening.md b/docs/guide/flattening.md index c2e0b2d..71f536f 100644 --- a/docs/guide/flattening.md +++ b/docs/guide/flattening.md @@ -42,7 +42,7 @@ The projection mode controls the **shape** of the JSON response: ### Default (Nested) ``` -GET /api/users?select=id,name,profile.bio,address.city +GET /api/customers?select=id,name,address.city ``` **Response:** @@ -62,7 +62,7 @@ GET /api/users?select=id,name,profile.bio,address.city ### Flat Mode ``` -GET /api/users?select=id,name,profile.bio,address.city&mode=flat +GET /api/customers?select=id,name,address.city&mode=flat ``` **Response:** @@ -70,15 +70,15 @@ GET /api/users?select=id,name,profile.bio,address.city&mode=flat { "id": 1, "name": "Alice", - "profile.bio": "Software Engineer", - "address.city": "Singapore" + "address.city": "Singapore", + "address.city": "London" } ``` ### FlatMixed Mode ``` -GET /api/users?select=id,name,profile.bio,orders&mode=flat-mixed +GET /api/customers?select=id,name,address.city,orders&mode=flat-mixed ``` **Response:** @@ -86,7 +86,7 @@ GET /api/users?select=id,name,profile.bio,orders&mode=flat-mixed { "id": 1, "name": "Alice", - "profile_bio": "Software Engineer", + "address_city": "Singapore", "orders": [ { "id": 101, "status": "shipped" } ] @@ -106,7 +106,7 @@ var options = new QueryOptions ProjectionMode = ProjectionMode.Flat }; -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var projected = query.ApplySelect(options); var data = await projected.ToListAsync(); ``` @@ -116,7 +116,7 @@ var data = await projected.ToListAsync(); The `mode` query parameter is automatically parsed: ``` -GET /api/users?select=id,name&mode=flat +GET /api/customers?select=id,name&mode=flat ``` Valid values: `Nested`, `Flat`, `FlatMixed` (case-insensitive when parsing from URL). @@ -128,7 +128,7 @@ Valid values: `Nested`, `Flat`, `FlatMixed` (case-insensitive when parsing from ### ❌ Using flat mode with deeply nested paths ``` -GET /api/users?select=a.b.c.d.e&mode=flat +GET /api/customers?select=a.b.c.d.e&mode=flat ``` The key `a.b.c.d.e` becomes the flat key — hard to consume. diff --git a/docs/guide/fluent-builder.md b/docs/guide/fluent-builder.md index f9cdedc..bc6bc33 100644 --- a/docs/guide/fluent-builder.md +++ b/docs/guide/fluent-builder.md @@ -27,7 +27,7 @@ var options = Query.Create() .Build(); // options is now a fully formed QueryOptions object -var result = await _context.Users.FlexQueryAsync(options); +var result = await _context.Customers.FlexQueryAsync(options); ``` ## Filtering diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index d85a76f..62c7286 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -80,20 +80,21 @@ This optional integration automatically registers the `FieldAccessFilter` into t ```csharp [FieldAccess(Allowed = new[] { "Id", "Name", "Email" })] [HttpGet] -public async Task GetUsers() { ... } +public async Task GetCustomers() { ... } ``` ### Step 2: Define Your Entity ```csharp -public class User +public class Customer { public int Id { get; set; } public string Name { get; set; } = ""; public string Email { get; set; } = ""; + public string City { get; set; } = ""; public string Status { get; set; } = "active"; - public int Age { get; set; } - public DateTime CreatedAt { get; set; } + public decimal Salary { get; set; } + public DateTime CreatedDate { get; set; } public List Orders { get; set; } = new(); } ``` @@ -104,6 +105,8 @@ public class User This is the recommended production pattern using `FlexQueryAsync`, which automatically handles parsing, validation, and execution in a single line. +> **Note:** `FlexQueryAsync` requires the `FlexQuery.NET.EntityFrameworkCore` or `FlexQuery.NET.Dapper` package. The core `FlexQuery.NET` package provides synchronous `FlexQuery` for advanced scenarios. + ```csharp using FlexQuery.NET.EntityFrameworkCore; using FlexQuery.NET.Models; @@ -114,27 +117,27 @@ using System.Collections.Generic; [ApiController] [Route("api/[controller]")] -public class UsersController : ControllerBase +public class CustomersController : ControllerBase { private readonly AppDbContext _context; - public UsersController(AppDbContext context) => _context = context; + public CustomersController(AppDbContext context) => _context = context; [HttpGet] - public async Task GetUsers([FromQuery] FlexQueryParameters parameters) + public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { try { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { // Security: Declare which fields clients are allowed to view/filter/sort exec.AllowedFields = new HashSet { - "Id", "Name", "Email", "Status", "Age", "CreatedAt" + "Id", "Name", "Email", "Status", "City", "Salary", "CreatedDate" }; // Block highly sensitive fields absolutely - exec.BlockedFields = new HashSet { "PasswordHash" }; + exec.BlockedFields = new HashSet { "Email" }; // Limit nesting depth (prevents infinite traversal via includes) exec.MaxFieldDepth = 2; @@ -154,7 +157,7 @@ public class UsersController : ControllerBase ### Sample Request ```http -GET /api/users?filter=Status:eq:active&sort=Name:asc&page=1&pageSize=10&select=Id,Name,Email +GET /api/customers?filter=Status:eq:active&sort=Name:asc&page=1&pageSize=10&select=Id,Name,Email ``` ### Sample Response @@ -216,7 +219,7 @@ public class FlexQueryParameters : FlexQueryBase ## How FlexQueryAsync Works Under the Hood -`FlexQueryAsync` is the unified high-level method. It internally manages the entire query lifecycle: +`FlexQueryAsync` (available in `FlexQuery.NET.EntityFrameworkCore` and `FlexQuery.NET.Dapper`) is the unified high-level method. It internally manages the entire query lifecycle: ```text FlexQueryParameters @@ -243,7 +246,7 @@ using FlexQuery.NET; using FlexQuery.NET.EntityFrameworkCore; [HttpGet("manual")] -public async Task GetUsersManual([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomersManual([FromQuery] FlexQueryParameters parameters) { // 1. Parse var options = parameters.ToQueryOptions(); @@ -253,10 +256,10 @@ public async Task GetUsersManual([FromQuery] FlexQueryParameters { AllowedFields = new HashSet { "Id", "Name", "Email", "Status" } }; - options.ValidateOrThrow(execOptions); + options.ValidateOrThrow(execOptions); // 3. Start composing the IQueryable - var query = _context.Users.AsQueryable(); + var query = _context.Customers.AsQueryable(); query = query.ApplyFilter(options); query = query.ApplySort(options); @@ -284,20 +287,20 @@ public async Task GetUsersManual([FromQuery] FlexQueryParameters var execOptions = new QueryExecutionOptions { AllowedFields = new HashSet { "Name", "Email", "Status" }, - BlockedFields = new HashSet { "PasswordHash", "InternalNotes" }, + BlockedFields = new HashSet { "Email", "InternalNotes" }, FilterableFields = new HashSet { "Name", "Status" }, - SortableFields = new HashSet { "Name", "CreatedAt" }, + SortableFields = new HashSet { "Name", "CreatedDate" }, SelectableFields = new HashSet { "Id", "Name", "Email" }, MaxFieldDepth = 2 }; -options.ValidateOrThrow(execOptions); +options.ValidateOrThrow(execOptions); ``` If you prefer to avoid exceptions for control flow, you can use `ValidateSafe` to return structured errors: ```csharp -var result = options.ValidateSafe(execOptions); +var result = options.ValidateSafe(execOptions); if (!result.IsValid) { diff --git a/docs/guide/grouping.md b/docs/guide/grouping.md index f1f32fc..d518f26 100644 --- a/docs/guide/grouping.md +++ b/docs/guide/grouping.md @@ -44,12 +44,12 @@ Use grouping for: **DSL:** ``` -GET /api/users?select=status&aggregates=*:count&groupBy=status +GET /api/customers?select=status&aggregates=*:count&groupBy=status ``` **FQL:** ``` -GET /api/users?select=status&aggregates=COUNT(*)&groupBy=status +GET /api/customers?select=status&aggregates=COUNT(*)&groupBy=status ``` **Response:** @@ -68,12 +68,12 @@ GET /api/users?select=status&aggregates=COUNT(*)&groupBy=status **DSL:** ``` -GET /api/orders?select=userId&aggregates=Amount:sum&groupBy=userId +GET /api/orders?select=customerId&aggregates=Total:sum&groupBy=customerId ``` **FQL:** ``` -GET /api/orders?select=UserId&aggregates=SUM(Amount)&groupBy=UserId +GET /api/orders?select=CustomerId&aggregates=SUM(Total)&groupBy=CustomerId ``` **Response:** @@ -90,12 +90,12 @@ GET /api/orders?select=UserId&aggregates=SUM(Amount)&groupBy=UserId **DSL:** ``` -GET /api/orders?select=customerId&aggregates=Amount:avg&groupBy=customerId&having=avg(Amount):gt:500 +GET /api/orders?select=customerId&aggregates=Total:avg&groupBy=customerId&having=avg(Total):gt:500 ``` **FQL:** ``` -GET /api/orders?select=CustomerId&aggregates=AVG(Amount)&groupBy=CustomerId&having=AVG(Amount) > 500 +GET /api/orders?select=CustomerId&aggregates=AVG(Total)&groupBy=CustomerId&having=AVG(Total) > 500 ``` Only returns groups where the average order amount exceeds 500. @@ -114,12 +114,12 @@ Only returns groups where the average order amount exceeds 500. **DSL:** ``` -GET /api/users?select=status&aggregates=*:count&groupBy=status&sort=Count:desc +GET /api/customers?select=status&aggregates=*:count&groupBy=status&sort=Count:desc ``` **FQL:** ``` -GET /api/users?select=status&aggregates=COUNT(*)&groupBy=status&sort=Count DESC +GET /api/customers?select=status&aggregates=COUNT(*)&groupBy=status&sort=Count DESC ``` --- @@ -206,7 +206,7 @@ var options = new QueryOptions } }; -var result = await _context.Users.ApplySelect(options).ToListAsync(); +var result = await _context.Customers.ApplySelect(options).ToListAsync(); ``` ### With HAVING @@ -241,12 +241,12 @@ Aggregates must use the dedicated `aggregates` parameter, not `select`. ``` # WRONG (v3 style) -GET /api/users?select=status,count()&groupBy=status +GET /api/customers?select=status,count()&groupBy=status ``` ``` # CORRECT (v4) -GET /api/users?select=status&aggregates=*:count&groupBy=status +GET /api/customers?select=status&aggregates=*:count&groupBy=status ``` ### ❌ Selecting non-grouped fields @@ -255,12 +255,12 @@ When using GROUP BY, you can only select grouped fields and aggregates. Selectin ``` # WRONG -GET /api/users?select=status,name&aggregates=*:count&groupBy=status +GET /api/customers?select=status,name&aggregates=*:count&groupBy=status ``` ``` # CORRECT — only grouped fields + aggregates -GET /api/users?select=status&aggregates=*:count&groupBy=status +GET /api/customers?select=status&aggregates=*:count&groupBy=status ``` ### ❌ Using HAVING without GROUP BY diff --git a/docs/guide/include-filtering.md b/docs/guide/include-filtering.md index c2ea222..0c87a6e 100644 --- a/docs/guide/include-filtering.md +++ b/docs/guide/include-filtering.md @@ -46,31 +46,31 @@ Use filtered includes when: ### Load Orders for Each User (No Filter) ``` -GET /api/users?include=Orders +GET /api/customers?include=Orders ``` ### Load Only Shipped Orders ``` -GET /api/users?include=Orders(status:eq:shipped) +GET /api/customers?include=Orders(status:eq:shipped) ``` ### Load Orders with Amount Above 100 ``` -GET /api/users?include=Orders(amount:gt:100) +GET /api/customers?include=Orders(total:gt:100) ``` ### Multiple Includes ``` -GET /api/users?include=Orders(status:eq:active),Profile +GET /api/customers?include=Orders(status:eq:active),Address ``` ### Nested Include ``` -GET /api/users?include=Orders(status:eq:active).Items +GET /api/customers?include=Orders(status:eq:active).Items ``` --- @@ -82,7 +82,7 @@ GET /api/users?include=Orders(status:eq:active).Items ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); query = query.ApplyFilter(options); query = query.ApplySort(options); @@ -101,9 +101,9 @@ var data = await query.ToListAsync(); `FlexQueryAsync` applies the include pipeline automatically: ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, exec => -{ - exec.AllowedFields = new HashSet { "id", "name", "orders.*" }; +var result = await _context.Customers.FlexQueryAsync(parameters, exec => + { + exec.AllowedFields = new HashSet { "id", "name", "orders.*" }; }); ``` @@ -113,7 +113,7 @@ var result = await _context.Users.FlexQueryAsync(parameters, exec => **Request:** ``` -GET /api/users?include=Orders(status:eq:shipped)&select=id,name&page=1&pageSize=2 +GET /api/customers?include=Orders(status:eq:shipped)&select=id,name&page=1&pageSize=2 ``` **Response:** @@ -132,8 +132,8 @@ GET /api/users?include=Orders(status:eq:shipped)&select=id,name&page=1&pageSize= "id": 1, "name": "Alice Chen", "orders": [ - { "id": 101, "status": "shipped", "amount": 250.00 }, - { "id": 105, "status": "shipped", "amount": 89.99 } + { "id": 101, "status": "shipped", "total": 250.00 }, + { "id": 105, "status": "shipped", "total": 89.99 } ] }, { @@ -157,16 +157,16 @@ Note: Bob Smith has no shipped orders, so his `orders` array is empty — but he | **Affects root query** | ✅ Yes | ❌ No | | **Affects result count** | ✅ Yes | ❌ No | | **Where condition applied** | Root entity | Related collection | -| **Use case** | Find users where any order is shipped | Get all users, include only their shipped orders | +| **Use case** | Find customers where any order is shipped | Get all customers, include only their shipped orders | **Example showing the difference:** ``` -# Returns only users who HAVE a shipped order -GET /api/users?filter=orders:any:status:eq:shipped +# Returns only customers who HAVE a shipped order +GET /api/customers?filter=orders:any:status:eq:shipped -# Returns ALL users, each with their shipped orders only -GET /api/users?include=Orders(status:eq:shipped) +# Returns ALL customers, each with their shipped orders only +GET /api/customers?include=Orders(status:eq:shipped) ``` --- @@ -176,11 +176,11 @@ GET /api/users?include=Orders(status:eq:shipped) ### ❌ Confusing Include filter with root filter ``` -# This returns all users, with only their shipped orders included -GET /api/users?include=Orders(status:eq:shipped) +# This returns all customers, with only their shipped orders included +GET /api/customers?include=Orders(status:eq:shipped) -# This returns ONLY users who have at least one shipped order -GET /api/users?filter=orders:any:status:eq:shipped +# This returns ONLY customers who have at least one shipped order +GET /api/customers?filter=orders:any:status:eq:shipped ``` Both are valid — just different use cases. Know which one you need. diff --git a/docs/guide/linq-integration.md b/docs/guide/linq-integration.md index f4b45ab..b74eb4a 100644 --- a/docs/guide/linq-integration.md +++ b/docs/guide/linq-integration.md @@ -43,7 +43,7 @@ If you need more control over when specific parts of the query options are appli You can use `ApplyFilter` and `ApplySort` to control exactly where in your pipeline the dynamic logic is injected. ```csharp -var query = _context.Users +var query = _context.Customers .ApplyFilter(options) // Only apply dynamic WHERE .Where(u => u.Email.EndsWith("@company.com")) // Hard-coded filter after .ApplySort(options); // Only apply dynamic ORDER BY @@ -113,12 +113,12 @@ WHERE ... When you use a query string like `?filter=age:gt:25&sort=createdDate:desc`, FlexQuery translates these into additional LINQ expressions. ### Example Request -`GET /api/users?filter=age:gt:25&sort=createdDate:desc` +`GET /api/customers?filter=salary:gt:50000&sort=createdDate:desc` If your code looks like this: ```csharp -var query = _context.Users +var query = _context.Customers .Where(u => u.IsActive); // Base condition var results = await query diff --git a/docs/guide/paging.md b/docs/guide/paging.md index 4552b1a..24b0a03 100644 --- a/docs/guide/paging.md +++ b/docs/guide/paging.md @@ -93,14 +93,14 @@ Use paging on **any list endpoint** to prevent full-table scans and large networ ### Basic Paging ``` -GET /api/users?page=1&pageSize=20 -GET /api/users?page=2&pageSize=10 +GET /api/customers?page=1&pageSize=20 +GET /api/customers?page=2&pageSize=10 ``` ### Disable Total Count (faster) ``` -GET /api/users?page=1&pageSize=20&includeCount=false +GET /api/customers?page=1&pageSize=20&includeCount=false ``` --- @@ -111,7 +111,7 @@ GET /api/users?page=1&pageSize=20&includeCount=false ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var paged = query.ApplyPaging(options); var data = await paged.ToListAsync(); ``` @@ -125,7 +125,7 @@ var options = parameters.ToQueryOptions(); if (options.Paging.PageSize > 100) options.Paging.PageSize = 100; -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var paged = query.ApplyPaging(options); ``` @@ -133,7 +133,7 @@ var paged = query.ApplyPaging(options); ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var filtered = query.ApplyFilter(options); var filtered2 = filtered.ApplySort(options); @@ -163,7 +163,7 @@ return Ok(options.BuildQueryResult(data, total)); **Request:** ``` -GET /api/users?page=3&pageSize=5 +GET /api/customers?page=3&pageSize=5 ``` **Response:** @@ -239,7 +239,7 @@ FlexQuery.NET natively supports keyset pagination. 1. **Initial Request:** Set `useKeysetPagination=true` (or `options.UseKeysetPagination = true`) along with your `sort` criteria. ```http -GET /api/users?sort=createdAt:desc,id:desc&pageSize=50&useKeysetPagination=true +GET /api/customers?sort=createdDate:desc,id:desc&pageSize=50&useKeysetPagination=true ``` 2. **Retrieve the Token:** The response `QueryResult` will include a `nextCursorToken` if there are more records. @@ -254,7 +254,7 @@ GET /api/users?sort=createdAt:desc,id:desc&pageSize=50&useKeysetPagination=true 3. **Subsequent Requests:** Pass that token back in the `cursor` parameter to fetch the next page. ```http -GET /api/users?sort=createdAt:desc,id:desc&pageSize=50&useKeysetPagination=true&cursor=eyJ0eXAiOiJKV1QiLCJ... +GET /api/customers?sort=createdDate:desc,id:desc&pageSize=50&useKeysetPagination=true&cursor=eyJ0eXAiOiJKV1QiLCJ... ``` ### Requirements for Keyset Pagination diff --git a/docs/guide/performance-tuning.md b/docs/guide/performance-tuning.md index 13c74d5..1ccca21 100644 --- a/docs/guide/performance-tuning.md +++ b/docs/guide/performance-tuning.md @@ -168,7 +168,7 @@ Projection reduces the number of selected columns returned by the database. Example: ```http -GET /api/users?select=id,name,email +GET /api/customers?select=id,name,email ``` Typical SQL behavior: @@ -201,7 +201,7 @@ By default, FlexQuery.NET may execute a separate count query when `IncludeCount` Example: ```http -GET /api/users?includeCount=false +GET /api/customers?includeCount=false ``` Disabling count queries may reduce overhead for: diff --git a/docs/guide/performance/fairness-disclaimers.md b/docs/guide/performance/fairness-disclaimers.md index bc66f09..b4c9035 100644 --- a/docs/guide/performance/fairness-disclaimers.md +++ b/docs/guide/performance/fairness-disclaimers.md @@ -292,7 +292,7 @@ The “manual LINQ” baseline represents: Example: ```csharp -_context.Users +_context.Customers .Where(u => u.Status == "active") ``` diff --git a/docs/guide/performance/memory-usage.md b/docs/guide/performance/memory-usage.md index 8a8497e..4244c41 100644 --- a/docs/guide/performance/memory-usage.md +++ b/docs/guide/performance/memory-usage.md @@ -189,7 +189,7 @@ For endpoints exporting all matching records: [HttpGet("export")] public async IAsyncEnumerable Export([FromQuery] FlexQueryParameters parameters) { - await foreach (var item in _context.Users + await foreach (var item in _context.Customers .FlexQueryStreaming(parameters)) { yield return item; diff --git a/docs/guide/performance/scalability.md b/docs/guide/performance/scalability.md index aa797d5..edb193f 100644 --- a/docs/guide/performance/scalability.md +++ b/docs/guide/performance/scalability.md @@ -33,7 +33,7 @@ How does FlexQuery.NET perform as your dataset grows? These benchmarks vary reco The 10,000-record scenario (2.8 ms materialization) is still faster than a typical network round-trip. However, if you allow clients to request arbitrary large pages: ``` -GET /api/users?filter=status:eq:active&pageSize=50000 +GET /api/customers?filter=status:eq:active&pageSize=50000 ``` A single request could: @@ -61,7 +61,7 @@ If your use case requires exporting all matching records (e.g., CSV download for [HttpGet("export")] public async IAsyncEnumerable Export([FromQuery] FlexQueryParameters parameters) { - await foreach (var item in _context.Users + await foreach (var item in _context.Customers .FlexQueryStreaming(parameters)) // hypothetical streaming API { yield return item; diff --git a/docs/guide/projection.md b/docs/guide/projection.md index 3e14427..f9e8c65 100644 --- a/docs/guide/projection.md +++ b/docs/guide/projection.md @@ -45,31 +45,31 @@ Use projection when: ### Basic Field Selection ``` -GET /api/users?select=id,name,email +GET /api/customers?select=id,name,email ``` ### Nested Path Selection ``` -GET /api/users?select=id,name,profile.bio,address.city +GET /api/customers?select=id,name,address.city ``` ### Wildcard (All Fields) ``` -GET /api/users?select=* +GET /api/customers?select=* ``` ### Projection Mode: Flat ``` -GET /api/users?select=id,name,profile.bio&mode=flat +GET /api/customers?select=id,name,address.city&mode=flat ``` ### Projection Mode: FlatMixed ``` -GET /api/users?select=id,name,profile.bio&mode=flat-mixed +GET /api/customers?select=id,name,address.city&mode=flat-mixed ``` --- @@ -126,7 +126,7 @@ Scalar navigation is flattened; collections remain nested: ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); query = query.ApplyFilter(options); query = query.ApplySort(options); query = query.ApplyPaging(options); @@ -139,19 +139,19 @@ var data = await projected.ToListAsync(); ### Restricting Selectable Fields ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, exec => -{ - exec.SelectableFields = new HashSet +var result = await _context.Customers.FlexQueryAsync(parameters, exec => { - "id", "name", "email", "profile.bio" - }; + exec.SelectableFields = new HashSet + { + "id", "name", "email", "address.city" + }; }); ``` ### Projection with Aggregates ``` -GET /api/users?select=status.count()&groupBy=status +GET /api/customers?select=status.count()&groupBy=status ``` ```json @@ -169,7 +169,7 @@ GET /api/users?select=status.count()&groupBy=status **Request:** ``` -GET /api/users?select=id,name,email&page=1&pageSize=3 +GET /api/customers?select=id,name,email&page=1&pageSize=3 ``` **Response:** @@ -194,7 +194,7 @@ GET /api/users?select=id,name,email&page=1&pageSize=3 **Nested path request:** ``` -GET /api/users?select=id,name,address.city&mode=nested +GET /api/customers?select=id,name,address.city&mode=nested ``` ```json @@ -214,13 +214,13 @@ GET /api/users?select=id,name,address.city&mode=nested ```csharp // WRONG — client could select passwordHash, internalNotes, etc. -var result = await _context.Users.FlexQueryAsync(parameters); +var result = await _context.Customers.FlexQueryAsync(parameters); ``` ### ❌ Requesting non-existent fields ``` -GET /api/users?select=id,nonExistentField +GET /api/customers?select=id,nonExistentField ``` Non-existent fields are silently skipped during projection. Validate fields first with `ValidateOrThrow`. diff --git a/docs/guide/query-composition.md b/docs/guide/query-composition.md index d056b83..05e780a 100644 --- a/docs/guide/query-composition.md +++ b/docs/guide/query-composition.md @@ -35,7 +35,7 @@ Real-world applications often require more than just passing a user's filter to String-based queries are the standard for external API interaction. They are lightweight, URL-friendly, and easy to consume from frontend frameworks. ```http -GET /api/products?filter=status:eq:active&sort=createdAt:desc&page=1&pageSize=20 +GET /api/customers?filter=status:eq:active&sort=createdDate:desc&page=1&pageSize=20 ``` FlexQuery.NET parses this into a structured `QueryOptions` object, which is then validated against your server-side security policies before execution. @@ -66,9 +66,9 @@ var options = new QueryOptions }, new FilterCondition { - Field = "Price", + Field = "Salary", Operator = FilterOperators.GreaterThan, - Value = "100" + Value = "50000" } } }, @@ -112,23 +112,23 @@ var options = new QueryOptions Logic = LogicOperator.And, Groups = new List { - // Group 1: (Category == 'Electronics' AND Status == 'Active') + // Group 1: (City == 'New York' AND Status == 'Active') new FilterGroup { Logic = LogicOperator.And, Filters = new List { - new FilterCondition { Field = "Category", Operator = "eq", Value = "Electronics" }, + new FilterCondition { Field = "City", Operator = "eq", Value = "New York" }, new FilterCondition { Field = "Status", Operator = "eq", Value = "Active" } } }, - // Group 2: (Price > 1000) + // Group 2: (Salary > 50000) new FilterGroup { Logic = LogicOperator.And, Filters = new List { - new FilterCondition { Field = "Price", Operator = "gt", Value = "1000" } + new FilterCondition { Field = "Salary", Operator = "gt", Value = "50000" } } } } @@ -228,7 +228,7 @@ FlexQuery.NET includes a strongly-typed fluent builder for developers who prefer ```csharp using FlexQuery.NET; -var results = await _context.Users +var results = await _context.Customers .Filter(f => f .And(x => x.IsActive).Eq(true) .AndGroup(g => g diff --git a/docs/guide/query-formats.md b/docs/guide/query-formats.md index 4a25e31..8b77f90 100644 --- a/docs/guide/query-formats.md +++ b/docs/guide/query-formats.md @@ -89,7 +89,7 @@ Combined with aggregates (always expressed as query parameters, not inside the f ### Filtered includes (FQL inside parentheses) ``` ?include=Orders(Status = 'Active') -?include=Orders(Status = 'Active').OrderItems(Quantity > 5) +?include=Orders(Status = 'Active').Items(Quantity > 5) ``` ### MiniOData @@ -129,4 +129,4 @@ MiniOData does not support filtered includes — use the standard `include` para - [Query Syntax](/guide/query-syntax) — Auto-detection and parser architecture - [Filtering](/guide/filtering) — Full operator reference -- [MiniOData Adapter](/adapters/miniodata) — OData compatibility details +- [MiniOData Parser](/parsers/miniodata) — OData compatibility details diff --git a/docs/guide/query-syntax.md b/docs/guide/query-syntax.md index c59202a..5321dbf 100644 --- a/docs/guide/query-syntax.md +++ b/docs/guide/query-syntax.md @@ -76,17 +76,11 @@ FlexQueryCore.Configure(options => The most compact format. Fields, operators, and values are separated by colons: ``` -GET /api/products?filter=ListPrice:gte:1000&sort=Name:asc -GET /api/products?filter=ProductCategory.Name:eq:Bikes +GET /api/customers?filter=City:eq:New York&sort=Name:asc +GET /api/customers?filter=City:eq:New York%26Status:eq:Active GET /api/customers?filter=LastName:startswith:Smi&select=Id,LastName,Email ``` -**Compound (AND — URL-encode `&` as `%26`):** - -``` -GET /api/products?filter=ListPrice:gte:1000%26ProductCategory.Name:eq:Bikes -``` - **Best for:** Internal tools, compact query strings, URL-friendly APIs. ### FQL — SQL-Like Expression Strings @@ -94,8 +88,8 @@ GET /api/products?filter=ListPrice:gte:1000%26ProductCategory.Name:eq:Bikes Requires the `FlexQuery.NET.Parsers.Fql` package and `Fql.Register()` at startup. Human-readable expressions using standard operators: ``` -GET /api/products?filter=ListPrice >= 1000 AND Category.Name = 'Bikes' -GET /api/customers?filter=LastName CONTAINS 'Smi' OR Email ENDSWITH '@adventure-works.com' +GET /api/customers?filter=Salary >= 50000 AND City = 'New York' +GET /api/customers?filter=LastName CONTAINS 'Smi' OR Email ENDSWITH '@example.com' ``` **Best for:** Developer tools, debugging, admin panels, human-written queries. @@ -105,8 +99,8 @@ GET /api/customers?filter=LastName CONTAINS 'Smi' OR Email ENDSWITH '@adventure- Requires the `FlexQuery.NET.Parsers.MiniOData` package and `MiniOData.Register()` at startup. Standard OData `$filter` and `$orderby` syntax: ``` -GET /api/products?$filter=ListPrice ge 1000 and Category/Name eq 'Bikes' -GET /api/products?$orderby=Name desc&$select=Id,Name,ListPrice&$expand=ProductCategory +GET /api/customers?$filter=Salary ge 50000 and City eq 'New York' +GET /api/customers?$orderby=Name desc&$select=Id,Name,Salary&$expand=Orders ``` **Best for:** Migrating from OData, enterprise clients that already speak OData. @@ -129,15 +123,15 @@ GET /api/products?$orderby=Name desc&$select=Id,Name,ListPrice&$expand=ProductCa ## Example: Single Endpoint with Explicit Syntax ```csharp -[HttpGet("products")] -public async Task GetProducts( +[HttpGet("customers")] +public async Task GetCustomers( [FromQuery] FlexQueryParameters parameters) { - var result = await _context.Products.FlexQueryAsync(parameters, opts => + var result = await _context.Customers.FlexQueryAsync(parameters, opts => { opts.AllowedFields = new HashSet { - "Id", "Name", "ListPrice", "ProductCategory" + "Id", "Name", "Email", "City", "Salary" }; opts.MaxPageSize = 100; }); @@ -150,13 +144,13 @@ This endpoint handles the syntax configured at startup: ``` # NativeDsl (default) -GET /products?filter=ListPrice:gte:1000&sort=Name:asc +GET /customers?filter=Salary:gte:50000&sort=Name:asc # FQL (if registered) -GET /products?filter=ListPrice >= 1000 AND ProductCategory.Name = 'Bikes' +GET /customers?filter=Salary >= 50000 AND City = 'New York' # MiniOData (if registered) -GET /products?$filter=ListPrice ge 1000 and ProductCategory/Name eq 'Bikes' +GET /customers?$filter=Salary ge 50000 and City eq 'New York' ``` --- @@ -166,15 +160,15 @@ GET /products?$filter=ListPrice ge 1000 and ProductCategory/Name eq 'Bikes' To override the global syntax for a single request, pass `QueryOptions` directly instead of `FlexQueryParameters`: ```csharp -[HttpPost("products/query")] -public async Task QueryProducts( +[HttpPost("customers/query")] +public async Task QueryCustomers( [FromBody] MiniODataRequest request) { var options = request.ToQueryOptions(); // options is now parsed using MiniOData syntax - var result = await _context.Products.FlexQueryAsync(options, opts => + var result = await _context.Customers.FlexQueryAsync(options, opts => { - opts.AllowedFields = new HashSet { "Id", "Name", "ListPrice" }; + opts.AllowedFields = new HashSet { "Id", "Name", "Salary" }; }); return Ok(result); @@ -204,5 +198,5 @@ public async Task QueryProducts( ## Related Features - [Filtering](/guide/filtering) — How parsed filters become LINQ expressions -- [MiniOData Adapter](/adapters/miniodata) — OData compatibility details +- [MiniOData Parser](/parsers/miniodata) — OData compatibility details - [AG Grid Adapter](/adapters/ag-grid) — Parsing AG Grid JSON payloads diff --git a/docs/guide/security.md b/docs/guide/security.md index 34cc1e1..c63bdcf 100644 --- a/docs/guide/security.md +++ b/docs/guide/security.md @@ -72,13 +72,21 @@ These are SERVER policies and should **never** be bound directly from HTTP reque | `FilterableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in filter expressions only. | | `SortableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in sort expressions only. | | `SelectableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in select/projection only. | +| `GroupableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in group-by expressions only. | +| `AggregatableFields` | `HashSet?` | BaseQueryOptions | Fields allowed in aggregate expressions only. | +| `ExpressionMappings` | `Dictionary?` | BaseQueryOptions | Maps DTO field aliases to entity expressions. | +| `FieldMappings` | `Dictionary?` | BaseQueryOptions | Maps external field aliases to internal property names. | +| `DefaultSortField` | `string?` | BaseQueryOptions | Default sort field when client doesn't specify. | +| `DefaultSortDescending` | `bool` | BaseQueryOptions | Default sort direction when `DefaultSortField` is used. | | `MaxFieldDepth` | `int?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Maximum dot-notation path depth. | -| `StrictFieldValidation` | `bool?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Fail-fast on first violation. | +| `StrictFieldValidation` | `bool` | FlexQueryOptions (global) or BaseQueryOptions (override) | Fail-fast on first violation. | | `MaxPageSize` | `int?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Maximum allowed page size. | -| `IncludeTotalCount` | `bool?` | FlexQueryOptions (global) or BaseQueryOptions (override) | Include total count by default. | +| `IncludeTotalCount` | `bool` | FlexQueryOptions (global) or BaseQueryOptions (override) | Include total count by default. | +| `CaseInsensitive` | `bool` | FlexQueryOptions (global) or BaseQueryOptions (override) | Whether field name matching is case-insensitive. | | `RoleAllowedFields` | `Dictionary>?` | BaseQueryOptions | Per-role field allow-lists. | | `CurrentRole` | `string?` | BaseQueryOptions | The current user's role name. | -| `FieldMappings` | `Dictionary?` | BaseQueryOptions | Alias → real field name mappings. | +| `AllowedFieldsResolver` | `Func>?` | BaseQueryOptions | Dynamic resolver for allowed fields based on entity type. | +| `Listener` | `IFlexQueryExecutionListener?` | BaseQueryOptions | Optional execution event listener. | ## AllowedFields @@ -241,9 +249,9 @@ Best for simple, single-endpoint rules. ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { "id", "name", "email" }; }); @@ -270,13 +278,13 @@ Best for standardizing rules across a controller. MaxDepth = 2 )] [HttpGet] -public async Task GetUsers( +public async Task GetCustomers( [FromQuery] FlexQueryParameters parameters) { // Retrieve execution options populated by the [FieldAccess] filter var execOptions = HttpContext.GetFlexQueryExecutionOptions(); - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = execOptions.AllowedFields; exec.BlockedFields = execOptions.BlockedFields; @@ -351,7 +359,7 @@ Handle it in your controller: ```csharp try { - var result = await _context.Users.FlexQueryAsync(parameters, exec => { ... }); + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { ... }); return Ok(result); } catch (QueryValidationException ex) diff --git a/docs/guide/sorting.md b/docs/guide/sorting.md index 93abb9c..88a2340 100644 --- a/docs/guide/sorting.md +++ b/docs/guide/sorting.md @@ -39,8 +39,8 @@ Use sorting any time the client should control the order of results — lists, t ### Single Field ``` -GET /api/users?sort=name:asc -GET /api/users?sort=createdAt:desc +GET /api/customers?sort=name:asc +GET /api/customers?sort=createdDate:desc ``` ### Multiple Fields @@ -48,7 +48,7 @@ GET /api/users?sort=createdAt:desc Fields are applied in order (first is primary, subsequent are `ThenBy`): ``` -GET /api/users?sort=status:asc,name:asc,createdAt:desc +GET /api/customers?sort=status:asc,name:asc,createdDate:desc ``` SQL: @@ -61,7 +61,7 @@ ORDER BY Status ASC, Name ASC, CreatedAt DESC Omitting the direction defaults to ascending: ``` -GET /api/users?sort=name +GET /api/customers?sort=name ``` ### Aggregate Sort @@ -69,11 +69,11 @@ GET /api/users?sort=name Sort by a computed aggregate over a related collection: ``` -GET /api/users?sort=orders.count():desc -GET /api/users?sort=orders.sum(amount):desc -GET /api/users?sort=orders.avg(amount):asc -GET /api/users?sort=orders.max(amount):desc -GET /api/users?sort=orders.min(amount):asc +GET /api/customers?sort=orders.count():desc +GET /api/customers?sort=orders.sum(total):desc +GET /api/customers?sort=orders.avg(total):asc +GET /api/customers?sort=orders.max(total):desc +GET /api/customers?sort=orders.min(total):asc ``` --- @@ -84,7 +84,7 @@ GET /api/users?sort=orders.min(amount):asc ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var sorted = query.ApplySort(options); var data = await sorted.ToListAsync(); ``` @@ -96,9 +96,9 @@ var execOptions = new QueryExecutionOptions { SortableFields = new HashSet { "name", "createdAt", "age" } }; -options.ValidateOrThrow(execOptions); +options.ValidateOrThrow(execOptions); -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var sorted = query.ApplySort(options); ``` @@ -115,7 +115,7 @@ var options = new QueryOptions } }; -var query = _context.Users.AsQueryable(); +var query = _context.Customers.AsQueryable(); var sorted = query.ApplySort(options); ``` @@ -125,7 +125,7 @@ var sorted = query.ApplySort(options); **Request:** ``` -GET /api/users?sort=createdAt:desc&page=1&pageSize=3 +GET /api/customers?sort=createdDate:desc&page=1&pageSize=3 ``` **Response:** @@ -140,9 +140,9 @@ GET /api/users?sort=createdAt:desc&page=1&pageSize=3 "hasPreviousPage": false, "aggregates": null, "data": [ - { "id": 10, "name": "Zara Khan", "createdAt": "2025-11-20T09:00:00Z" }, - { "id": 9, "name": "Yuki Tanaka", "createdAt": "2025-10-15T14:30:00Z" }, - { "id": 8, "name": "Xan Torres", "createdAt": "2025-09-01T08:00:00Z" } + { "id": 10, "name": "Zara Khan", "createdDate": "2025-11-20T09:00:00Z" }, + { "id": 9, "name": "Yuki Tanaka", "createdDate": "2025-10-15T14:30:00Z" }, + { "id": 8, "name": "Xan Torres", "createdDate": "2025-09-01T08:00:00Z" } ], "nextCursorToken": null } @@ -156,26 +156,26 @@ GET /api/users?sort=createdAt:desc&page=1&pageSize=3 ``` # WRONG — cannot sort by a collection property -GET /api/users?sort=orders:desc +GET /api/customers?sort=orders:desc ``` Use aggregate sort instead: ``` # CORRECT -GET /api/users?sort=orders.count():desc +GET /api/customers?sort=orders.count():desc ``` ### ❌ Unrestricted sort fields on public API ```csharp // WRONG — client could sort by passwordHash or internal fields -var result = await _context.Users.FlexQueryAsync(parameters); +var result = await _context.Customers.FlexQueryAsync(parameters); ``` ```csharp // CORRECT -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.SortableFields = new HashSet { "name", "email", "createdAt" }; }); diff --git a/docs/guide/swagger-integration.md b/docs/guide/swagger-integration.md index 60ee79f..35154ba 100644 --- a/docs/guide/swagger-integration.md +++ b/docs/guide/swagger-integration.md @@ -10,7 +10,7 @@ When you use `FlexQueryParameters` in your controller actions, ASP.NET Core and [HttpGet] public async Task Get([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, options => + var result = await _context.Customers.FlexQueryAsync(parameters, options => { options.AllowedFields = ["Id", "Name", "Email"]; }); diff --git a/docs/guide/validation.md b/docs/guide/validation.md index c4ac818..cdc5b19 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -26,14 +26,14 @@ Client-provided query strings are inherently untrusted input. The validation lay ### ValidateOrThrow (Throw on Failure) ```csharp -options.ValidateOrThrow(execOptions); +options.ValidateOrThrow(execOptions); // Throws QueryValidationException if invalid ``` ### ValidateSafe (Return Result) ```csharp -var result = options.ValidateSafe(execOptions); +var result = options.ValidateSafe(execOptions); if (!result.IsValid) { @@ -88,7 +88,7 @@ public class ValidationError **Request:** ``` -GET /api/users?filter=passwordHash:isnotnull +GET /api/customers?filter=email:isnotnull ``` **Response (400):** @@ -108,7 +108,7 @@ GET /api/users?filter=passwordHash:isnotnull **Request:** ``` -GET /api/users?filter=salary:gt:50000 +GET /api/customers?filter=salary:gt:50000 ``` **Response (400):** @@ -128,7 +128,7 @@ GET /api/users?filter=salary:gt:50000 **Request:** ``` -GET /api/users?filter=profile.address.city.postcode:eq:12345 +GET /api/customers?filter=address.city:eq:New York ``` **Response (400):** @@ -148,7 +148,7 @@ GET /api/users?filter=profile.address.city.postcode:eq:12345 **Request:** ``` -GET /api/users?filter=createdAt:gt:2024-01-01 +GET /api/customers?filter=createdDate:gt:2024-01-01 ``` With `exec.FilterableFields = { "name", "status" }` (createdAt not included): @@ -186,7 +186,7 @@ If a client attempts to use a restricted operator: **Request:** ``` -GET /api/users?filter=Email:contains:gmail.com +GET /api/customers?filter=email:contains:gmail.com ``` **Response (400):** @@ -232,7 +232,7 @@ var result = options.ValidateSafe(exec); ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { var execOptions = new QueryExecutionOptions { @@ -241,7 +241,7 @@ public async Task GetUsers([FromQuery] FlexQueryParameters parame MaxFieldDepth = 2 }; - var result = await _context.Users.FlexQueryAsync(parameters, execOptions); + var result = await _context.Customers.FlexQueryAsync(parameters, execOptions); return Ok(result); } ``` @@ -250,12 +250,12 @@ Or with inline configuration: ```csharp [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { - exec.AllowedFields = new HashSet { "id", "name", "email", "status" }; - exec.BlockedFields = new HashSet { "passwordHash" }; + exec.AllowedFields = new HashSet { "id", "name", "email", "status", "city" }; + exec.BlockedFields = new HashSet { "email" }; exec.MaxFieldDepth = 2; }); diff --git a/docs/guide/why-flexquery.md b/docs/guide/why-flexquery.md index d7a3670..bedf826 100644 --- a/docs/guide/why-flexquery.md +++ b/docs/guide/why-flexquery.md @@ -13,30 +13,30 @@ Without a query abstraction layer, you end up writing this: ```csharp // ❌ Before FlexQuery.NET — manual, brittle, insecure -[HttpGet("users")] -public async Task GetUsers( +[HttpGet("customers")] +public async Task GetCustomers( string? name, string? status, - int? minAge, + decimal? minSalary, string? sortBy, bool sortDesc = false, int page = 1, int pageSize = 20) { - var query = _context.Users.AsQueryable(); + var query = _context.Customers.AsQueryable(); if (!string.IsNullOrEmpty(name)) - query = query.Where(u => u.Name.Contains(name)); + query = query.Where(c => c.Name.Contains(name)); if (!string.IsNullOrEmpty(status)) - query = query.Where(u => u.Status == status); + query = query.Where(c => c.Status == status); - if (minAge.HasValue) - query = query.Where(u => u.Age >= minAge.Value); + if (minSalary.HasValue) + query = query.Where(c => c.Salary >= minSalary.Value); if (sortBy == "name") query = sortDesc - ? query.OrderByDescending(u => u.Name) - : query.OrderBy(u => u.Name); + ? query.OrderByDescending(c => c.Name) + : query.OrderBy(c => c.Name); // ... repeated for every field @@ -65,9 +65,9 @@ FlexQuery.NET replaces all of that with a **query abstraction layer**: // ✅ After FlexQuery.NET — declarative, safe, composable [HttpGet("users")] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.AllowedFields = new HashSet { @@ -91,7 +91,7 @@ The client can now filter, sort, page, and project **any allowed field** without It accepts query parameters in multiple formats and converts them into a structured, validated AST — `QueryOptions`. ``` -GET /api/users?filter=status:eq:active&sort=name:asc&page=1&pageSize=20&select=id,name,email +GET /api/customers?filter=status:eq:active&sort=name:asc&page=1&pageSize=20&select=id,name,email ``` ### 2. Validates Before Execution @@ -125,7 +125,7 @@ if (!string.IsNullOrEmpty(name)) **After:** ``` -GET /api/users?filter=name:contains:alice +GET /api/customers?filter=name:contains:alice ``` ### Sorting @@ -138,7 +138,7 @@ if (sortBy == "name") **After:** ``` -GET /api/users?sort=name:asc,createdAt:desc +GET /api/customers?sort=name:asc,city:desc ``` ### Projection @@ -151,7 +151,7 @@ return Ok(users); **After:** ``` -GET /api/users?select=id,name,email +GET /api/customers?select=id,name,email ``` The response only contains the 3 requested fields. Smaller payload. Faster response. diff --git a/docs/index.md b/docs/index.md index 2e9a2b2..f334c4d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,10 +37,10 @@ features: details: Validates field paths, operators, types, and access rules before execution. Returns structured errors or throws — your choice. - icon: ⚡ title: EF Core Ready - details: Expression-tree based. Every operation translates to SQL — no client-side evaluation. Full async support via FlexQueryAsync. + details: Expression-tree based. Every operation translates to SQL — no client-side evaluation. Full async support via FlexQueryAsync (requires FlexQuery.NET.EntityFrameworkCore). - icon: 🧩 title: Composable Pipeline - details: Call individual steps (ApplyFilter, ApplySort, ApplyPaging, ApplySelect) or use the unified FlexQueryAsync for the complete pipeline. + details: Call individual steps (ApplyFilter, ApplySort, ApplyPaging, ApplySelect) or use the unified FlexQueryAsync for the complete pipeline (requires EF Core or Dapper provider). ---
@@ -64,24 +64,28 @@ HTTP Request → Parse → Validate → Filter → Sort → Page → Project → **The client sends this HTTP request:** ``` -GET /api/users?filter=status:eq:active&sort=createdAt:desc&page=1&pageSize=10&select=id,name,email +GET /api/customers?filter=status:eq:active&sort=createdDate:desc&page=1&pageSize=10&select=id,name,email ``` **Your controller handles it in 3 lines:** ```csharp +using FlexQuery.NET.EntityFrameworkCore; + [HttpGet] -public async Task GetUsers([FromQuery] FlexQueryParameters parameters) +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) { - var result = await _context.Users.FlexQueryAsync(parameters, exec => + var result = await _context.Customers.FlexQueryAsync(parameters, exec => { - exec.AllowedFields = new HashSet { "id", "name", "email", "status", "createdAt" }; + exec.AllowedFields = new HashSet { "id", "name", "email", "status", "city" }; }); return Ok(result); } ``` +> **Note:** `FlexQueryAsync` requires the `FlexQuery.NET.EntityFrameworkCore` or `FlexQuery.NET.Dapper` package. The core `FlexQuery.NET` package provides synchronous `FlexQuery` for advanced scenarios. + **The response:** ```json @@ -107,8 +111,8 @@ FlexQuery.NET is designed to work at the level of abstraction that suits your us | Level | Method | Use When | | :--- | :--- | :--- | -| **High-level (recommended)** | `FlexQueryAsync` | You want parse + validate + execute in one call | -| **Mid-level** | `QueryOptionsParser.Parse` + manual pipeline | You need custom steps between parse and execute | +| **High-level (recommended)** | `FlexQueryAsync` (EF Core/Dapper) or `FlexQuery` (Core) | You want parse + validate + execute in one call | +| **Mid-level** | `ToQueryOptions()` + manual pipeline | You need custom steps between parse and execute | | **Low-level** | `ApplyFilter`, `ApplySort`, `ApplyPaging`, `ApplySelect` | You need full control over each pipeline stage | --- @@ -117,24 +121,24 @@ FlexQuery.NET is designed to work at the level of abstraction that suits your us ``` FlexQueryParameters (HTTP DTO) - │ - ▼ - QueryOptionsParser.Parse() - │ - ▼ - QueryOptions (AST) - │ - ├── ValidateOrThrow() ← Field access, operator, type checks - │ - ├── ApplyFilter() ← WHERE clause (expression tree) - ├── ApplySort() ← ORDER BY - ├── ApplyPaging() ← SKIP / TAKE + │ + ▼ + ToQueryOptions() ← Parses to AST + │ + ▼ + QueryOptions ← The parsed model + │ + │ (FlexQueryAsync includes validation automatically) + │ + ├── ApplyFilter() ← WHERE clause (expression tree) + ├── ApplySort() ← ORDER BY + ├── ApplyPaging() ← SKIP / TAKE ├── ApplyExpand() ← Include pipeline (independent) - └── ApplySelect() ← Dynamic projection - │ - ▼ - QueryResult - { data, totalCount, resultCount, page, pageSize } + └── ApplySelect() ← Dynamic projection + │ + ▼ + QueryResult + { data, totalCount, resultCount, page, pageSize } ``` `totalCount` is the number of filtered source records. `resultCount` is the number of shaped rows before paging, which differs for grouping, distinct projection, pivoting, or similar cardinality-changing operations. @@ -145,12 +149,10 @@ All expression trees are translated to SQL by EF Core. No client-side evaluation ## Supported Query Formats -FlexQuery.NET auto-detects the input format: - | Format | Example | | :--- | :--- | -| **DSL** | `filter=status:eq:active` | -| **FQL** | `query=status = "active" AND age >= 18` | +| **DSL** | `filter=status:eq:active & salary:gte:50000` | +| **FQL** | `filter=status = "active" AND salary >= 50000` | --- @@ -158,8 +160,8 @@ FlexQuery.NET auto-detects the input format: | Package | Purpose | | :--- | :--- | -| `FlexQuery.NET` | Core library — filtering, sorting, paging, projection, validation | -| `FlexQuery.NET.EntityFrameworkCore` | EF Core async execution — `FlexQueryAsync`, `ApplyExpand` | +| `FlexQuery.NET` | Core library — filtering, sorting, paging, projection, validation. Provides synchronous `FlexQuery` method. | +| `FlexQuery.NET.EntityFrameworkCore` | EF Core async execution — `FlexQueryAsync`, `ApplyExpand`, `UseNoTracking` | | `FlexQuery.NET.Dapper` | Dapper async execution — `FlexQueryAsync` for raw ADO.NET connections | | `FlexQuery.NET.AspNetCore` | ASP.NET Core integration — `FieldAccessFilter`, `[FieldAccess]` attribute | diff --git a/docs/parsers/fql.md b/docs/parsers/fql.md new file mode 100644 index 0000000..48221a3 --- /dev/null +++ b/docs/parsers/fql.md @@ -0,0 +1,297 @@ +# FQL Parser + +## Overview + +`FlexQuery.NET.Parsers.Fql` is an optional parser package that enables FlexQuery to understand and parse SQL-like query expressions. FQL (FlexQuery Query Language) provides an expressive, familiar syntax for filtering, sorting, grouping, aggregation, and projection while integrating seamlessly with the FlexQuery.NET query pipeline. + +### What It Is + +FQL is a SQL-inspired query language parser package. It translates human-readable query expressions into FlexQuery's canonical `QueryOptions` AST. It does **not** execute SQL directly — it generates the same `QueryOptions` that all FlexQuery parsers produce, which are then executed by your chosen provider (EF Core or Dapper). + +### Why It Exists + +Many developers prefer SQL-like syntax for ad-hoc queries, admin tools, and developer-facing APIs. FQL lets you accept natural language expressions (`Status = 'Active' AND Salary >= 50000`) instead of colon-separated DSL (`status:eq:active & salary:gte:50000`), making APIs more accessible to users familiar with SQL. + +### When to Use It + +- You want SQL-like query syntax for your API consumers +- You are building developer tools, admin panels, or internal APIs +- Your users are more comfortable with SQL expressions than DSL syntax +- You want readable query strings in logs and debugging tools + +### When NOT to Use It + +- You are building a public API where URL compactness is critical — use native DSL instead +- All your clients are under your control and can use the native DSL format +- You need OData compatibility — use MiniOData instead + +## Installation + +```bash +dotnet add package FlexQuery.NET.Parsers.Fql +``` + +## Registration + +```csharp +using FlexQuery.NET.Parsers.Fql; + +Fql.Register(); +``` + +This registers the `FqlQueryParser` with the `QueryOptionsParser` and enables the FQL syntax. Must be called **once** at startup. + +**Important:** In v4, parser packages are framework-agnostic and require explicit static registration. The old `AddFqlParser()` DI extension was removed. + +## Supported Operators + +### Comparison Operators + +| FQL Operator | Meaning | Example | +|:---|:---|:---| +| `=` | Equal | `Status = 'Active'` | +| `!=` | Not equal | `Status != 'Inactive'` | +| `>` | Greater than | `Salary > 50000` | +| `>=` | Greater than or equal | `Salary >= 50000` | +| `<` | Less than | `Salary < 100000` | +| `<=` | Less than or equal | `Salary <= 100000` | + +### Logical Operators + +| FQL Operator | Meaning | Example | +|:---|:---|:---| +| `AND` | Logical AND | `Status = 'Active' AND Salary > 50000` | +| `OR` | Logical OR | `City = 'New York' OR City = 'London'` | + +### String Operators + +| FQL Operator | Meaning | Example | +|:---|:---|:---| +| `CONTAINS` | String contains | `Name CONTAINS 'john'` | +| `STARTSWITH` | String starts with | `Email STARTSWITH 'admin'` | +| `ENDSWITH` | String ends with | `Email ENDSWITH '.com'` | +| `LIKE` | SQL LIKE pattern | `Name LIKE '%john%'` | + +### Null Operators + +| FQL Operator | Meaning | Example | +|:---|:---|:---| +| `IS NULL` | Is null | `Address IS NULL` | +| `IS NOT NULL` | Is not null | `Address IS NOT NULL` | + +### Collection Operators + +| FQL Operator | Meaning | Example | +|:---|:---|:---| +| `IN` | Value in set | `Status IN ('Active', 'Pending', 'Review')` | +| `NOT IN` | Value not in set | `Status NOT IN ('Deleted', 'Banned')` | +| `BETWEEN` | Value in range | `Salary BETWEEN 50000 AND 100000` | +| `ANY` | Collection contains matching element | `Orders.Any(Status = 'Shipped')` | +| `ALL` | All collection elements match | `Orders.All(Status = 'Shipped')` | +| `COUNT` | Collection count comparison | `Orders.Count > 5` | + +## Supported Query Parameters + +| Parameter | Description | Example | +|:---|:---|:---| +| `filter` | Filter records using FQL syntax | `filter=Status = 'Active' AND Salary > 50000` | +| `select` | Select specific fields | `select=Id,Name,Email` | +| `sort` | Sort results | `sort=Name asc,CreatedDate desc` | +| `include` | Include related entities | `include=Orders` | +| `groupBy` | Group results | `groupBy=Status` | +| `aggregates` | Aggregate functions | `aggregates=COUNT(*),SUM(Salary)` | +| `having` | HAVING condition | `having=SUM(Salary) > 10000` | +| `page` | Page number | `page=1` | +| `pageSize` | Items per page | `pageSize=20` | +| `distinct` | Return distinct records | `distinct=true` | +| `includeCount` | Include total count | `includeCount=true` | + +## Basic Example + +```csharp +using FlexQuery.NET.Parsers.Fql; + +// Register at startup +Fql.Register(); + +FlexQueryCore.Configure(options => +{ + options.QuerySyntax = QuerySyntax.Fql; +}); +``` + +```http +GET /api/customers?filter=Status = 'Active' AND Salary >= 50000&sort=Name asc&page=1&pageSize=20 +``` + +```csharp +using FlexQuery.NET.Parsers.Fql; +using FlexQuery.NET; + +[HttpGet("customers")] +public async Task GetCustomers([FromQuery] FlexQueryParameters parameters) +{ + var result = await _context.Customers.FlexQueryAsync(parameters, exec => + { + exec.AllowedFields = new HashSet + { + "Id", "Name", "Email", "Status", "City", "Salary", "CreatedDate" + }; + exec.MaxFieldDepth = 2; + }); + + return Ok(result); +} +``` + +## Nested Logic + +FQL supports deeply nested AND/OR groups: + +``` +GET /api/customers?filter=(City = 'New York' OR City = 'London') AND Status = 'Active' AND Salary > 50000 +``` + +Equivalent to: + +```sql +WHERE (City = 'New York' OR City = 'London') + AND Status = 'Active' + AND Salary > 50000 +``` + +## Collection Navigation + +### ANY + +Filter customers who have at least one shipped order: + +``` +GET /api/customers?filter=Orders.Any(Status = 'Shipped') +``` + +With additional conditions: + +``` +GET /api/customers?filter=Orders.Any(Status = 'Shipped' AND Total > 100) +``` + +### Nested Collections + +Filter customers with shipped orders containing expensive items: + +``` +GET /api/customers?filter=Orders.Any( + Status = 'Shipped' + AND OrderItems.Any(Total > 100) +) +``` + +### COUNT + +Filter customers with more than 5 orders: + +``` +GET /api/customers?filter=Orders.Count > 5 +``` + +### ALL + +Filter customers where all orders are shipped: + +``` +GET /api/customers?filter=Orders.All(Status = 'Shipped') +``` + +## String Operations + +### CONTAINS + +``` +GET /api/customers?filter=Name CONTAINS 'alice' +``` + +### STARTSWITH / ENDSWITH + +``` +GET /api/customers?filter=Email STARTSWITH 'admin' +GET /api/customers?filter=Email ENDSWITH '.com' +``` + +### LIKE + +``` +GET /api/customers?filter=Name LIKE '%john%' +``` + +## Real-World Example: Complex Query + +Retrieve active customers from New York or London with salary above 50000, including their recent shipped orders: + +``` +GET /api/customers? + filter=(City = 'New York' OR City = 'London') + AND Status = 'Active' + AND Salary > 50000 + &include=Orders(Status = 'Shipped') + &select=Id,Name,Email,City,Salary,Orders.OrderNumber,Orders.Total + &sort=Salary desc,Name asc + &page=1&pageSize=20 + &includeCount=true +``` + +## Comparison with Other Formats + +| Feature | FQL | NativeDsl | MiniOData | +|:---|:---|:---|:---| +| **Syntax style** | SQL-like | Colon-separated | OData `$` parameters | +| **Nested logic** | ✅ Full AND/OR | ✅ Full AND/OR | ✅ Full AND/OR | +| **Collection operators** | ✅ Any, All, Count | ✅ any, all, count | ❌ Not supported | +| **URL-friendly** | ⚠️ Needs encoding | ✅ Very | ✅ Good | +| **Human-readable** | ✅ High | ⚠️ Medium | ✅ High | +| **Package required** | `FlexQuery.NET.Parsers.Fql` | Core (default) | `FlexQuery.NET.Parsers.MiniOData` | +| **Registration** | `Fql.Register()` | None | `MiniOData.Register()` | +| **Best for** | Developer tools, admin panels | Internal tools, compact URLs | OData migration | + + +To explicitly use FQL, either: +- Configure globally: `FlexQueryCore.Configure(options => options.QuerySyntax = QuerySyntax.Fql)` +- Or send FQL-style expressions with the `query` parameter instead of `filter` + +## Performance Considerations + +- FQL parsing is ~1.8× slower than DSL due to more complex tokenization (handling quotes, whitespace, operator precedence) +- Still sub-microsecond per request — negligible overhead for most applications +- Parser results are cached by `QueryOptionsParser` using a composite cache key that includes the syntax type + +## Security Considerations + +- FQL expressions can reference any field name — **always** configure `AllowedFields` and `StrictFieldValidation` to prevent field enumeration +- FQL supports nested property access (`Customer.Address.City`) — use `MaxFieldDepth` to limit traversal depth +- Collection operators (`ANY`, `ALL`) can generate expensive queries — validate and test with realistic data volumes + +## Best Practices + +1. **Pick one syntax per API** — If all clients are under your control, standardize on DSL or FQL +2. **Use FQL for developer-facing tools** — The SQL-like syntax is more accessible for ad-hoc queries +3. **Validate aggressively** — FQL's expressiveness makes it important to enforce `AllowedFields` and `MaxFieldDepth` +4. **Document supported operators** — Not all FQL operators may be enabled in your security policy +5. **Consider URL encoding** — FQL expressions with spaces and quotes need proper URL encoding in query strings + +## Common Pitfalls + +| Pitfall | Solution | +|:---|:---| +| Forgetting to call `Fql.Register()` | FQL queries throw `ParserNotRegisteredException` at runtime | +| Mixing DSL and FQL syntax | DSL uses colons (`field:op:value`), FQL uses spaces and operators (`field OP value`) | +| Not URL-encoding FQL expressions | Spaces, quotes, and parentheses must be URL-encoded in query strings | +| Assuming all operators are enabled | `AllowedOperators` restricts which FQL operators are accepted | +| Nested property depth exceeded | `MaxFieldDepth` limits how deep FQL can traverse object graphs | + +## Related Features + +- [Query Syntax](/guide/query-syntax) — How to configure and register parsers +- [Query Formats](/guide/query-formats) — Comparing DSL, FQL, JSON, and MiniOData +- [Filtering](/guide/filtering) — FlexQuery's full filter operator reference +- [Operators Reference](/shared/operators) — Complete operator documentation +- [MiniOData Parser](/parsers/miniodata) — OData compatibility alternative \ No newline at end of file diff --git a/docs/adapters/miniodata.md b/docs/parsers/miniodata.md similarity index 100% rename from docs/adapters/miniodata.md rename to docs/parsers/miniodata.md diff --git a/docs/providers/dapper/getting-started.md b/docs/providers/dapper/getting-started.md index 49946f4..f5d2d01 100644 --- a/docs/providers/dapper/getting-started.md +++ b/docs/providers/dapper/getting-started.md @@ -58,16 +58,16 @@ The SQL dialect is **auto-detected** from the supplied `DbConnection` at runtime The simplest approach — pass raw parameters and let FlexQuery parse, validate, and execute: ```csharp -[HttpGet("users")] -public async Task GetUsers( +[HttpGet("customers")] +public async Task GetCustomers( [FromQuery] FlexQueryParameters parameters) { await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(); - var result = await connection.FlexQueryAsync(parameters, opts => + var result = await connection.FlexQueryAsync(parameters, opts => { - opts.AllowedFields = new HashSet { "Id", "Name", "Email", "CreatedAt" }; + opts.AllowedFields = new HashSet { "Id", "Name", "Email", "City", "CreatedDate" }; opts.MaxPageSize = 100; }); @@ -97,7 +97,7 @@ For full control over all execution options: ```csharp var dapperOptions = new DapperQueryOptions { - CommandTimeoutSeconds = 60, + CommandTimeout = 60, AllowedFields = new HashSet { "Id", "Name" }, StrictFieldValidation = true, MaxPageSize = 50 @@ -157,9 +157,9 @@ Data.Count = groups returned in the current page 1. **The SQL dialect is auto-detected from the `DbConnection`** — no manual dialect configuration is needed 2. **Use `AllowedFields`** — Dapper generates raw SQL; restricting fields is critical for security -3. **Set `CommandTimeoutSeconds` for complex queries** — The default is 30 seconds +3. **Set `CommandTimeout` for complex queries** — The default is 30 seconds 4. **Prefer `FlexQueryParameters` overloads** for API endpoints — They handle parsing and validation automatically -5. **Use `ScanEntitiesFromAssembly`** during startup — Avoid lazy mapping discovery in hot paths +5. **Configure entity mappings during startup** — Use `FlexQueryDapper.Configure` to set up mappings once 6. **Reuse connections** — FlexQuery does not manage connection lifecycle; follow Dapper's connection pooling best practices ## Common Pitfalls diff --git a/docs/providers/ef-core.md b/docs/providers/ef-core.md index 6107543..6962431 100644 --- a/docs/providers/ef-core.md +++ b/docs/providers/ef-core.md @@ -85,14 +85,14 @@ QueryResult FlexQuery supports **filtered navigation property loading** — loading related data with inline conditions. This maps to EF Core's filtered `Include()`: ``` -?include=Orders(Status = 'Active').OrderItems(Quantity > 5) +?include=Orders(Status = 'Active').Items(Quantity > 5) ``` This generates: ```csharp query .Include(c => c.Orders.Where(o => o.Status == "Active")) - .ThenInclude(o => o.OrderItems.Where(i => i.Quantity > 5)) + .ThenInclude(o => o.Items.Where(i => i.Quantity > 5)) ``` ### Using ApplyExpand @@ -144,7 +144,7 @@ options.Aggregates = new List new() { Function = AggregateFunction.Count, Field = "Id", Alias = "idCount" } }; -var result = await _context.Products.FlexQueryAsync(options); +var result = await _context.Customers.FlexQueryAsync(options); // result.Aggregates: // { "Price": { "sum": 15000, "avg": 250 }, "Id": { "count": 60 } } @@ -157,7 +157,7 @@ When `GroupBy` is specified alongside aggregates, the pipeline uses `GroupByBuil The EF Core provider introduces `EfCoreQueryOptions`, which allows you to control query compilation behavior per-request without modifying the underlying `DbContext`. ```csharp -var result = await _context.Products.FlexQueryAsync(parameters, opts => +var result = await _context.Customers.FlexQueryAsync(parameters, opts => { opts.UseNoTracking = true; // Adds .AsNoTracking() }); @@ -184,7 +184,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) These options are not available in FlexQuery v4. To bypass globally configured EF Core behaviors, configure them directly on the `DbContext` before passing the queryable to `FlexQueryAsync`: ```csharp -var query = _context.Products +var query = _context.Customers .IgnoreAutoIncludes() .IgnoreQueryFilters(); @@ -200,7 +200,7 @@ Inspect the SQL that EF Core will generate without executing the query: ```csharp var options = parameters.ToQueryOptions(); -var query = _context.Products.AsQueryable().ApplyFilter(options); +var query = _context.Customers.AsQueryable().ApplyFilter(options); string sql = query.ToQueryString(); // EF Core built-in method // Returns the generated SQL or a diagnostic message @@ -212,21 +212,21 @@ Console.WriteLine(sql); ## Real-World Example: Multi-Tenant API ```csharp -[HttpGet("products")] -public async Task GetProducts( +[HttpGet("customers")] +public async Task GetCustomers( [FromQuery] FlexQueryParameters parameters) { var tenantId = User.GetTenantId(); - var result = await _context.Products - .Where(p => p.TenantId == tenantId) // Tenant isolation FIRST + var result = await _context.Customers + .Where(p => p.CustomerId == tenantId) // Customer isolation FIRST .FlexQueryAsync(parameters, opts => { opts.AllowedFields = new HashSet { - "Id", "Name", "Price", "Category", "CreatedAt" + "Id", "Name", "Email", "City", "Status", "Salary", "CreatedDate" }; - opts.BlockedFields = new HashSet { "TenantId", "InternalCost" }; + opts.BlockedFields = new HashSet { "Email", "InternalNotes" }; opts.StrictFieldValidation = true; opts.UseNoTracking = true; opts.MaxPageSize = 200; diff --git a/docs/readme.md b/docs/readme.md index 256b702..61621d4 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -6,7 +6,7 @@ FlexQuery.NET is a lightweight library for `IQueryable` that enables dynamic fil ```csharp // Single unified call: Parse -> Validate -> Execute -var result = await _context.Users.FlexQueryAsync(parameters, options => +var result = await _context.Customers.FlexQueryAsync(parameters, options => { options.AllowedFields = ["Id", "Name", "Orders.Status"]; options.MaxFieldDepth = 2; @@ -30,7 +30,7 @@ var result = await _context.Users.FlexQueryAsync(parameters, options => FlexQuery.NET v2.1 introduces server-side **Execution Strategies**, including support for EF Core **Split Queries** and automatic **No-Tracking** execution to ensure maximum performance for complex data graphs. ```csharp -var result = await _context.Users.FlexQueryAsync(parameters, exec => +var result = await _context.Customers.FlexQueryAsync(parameters, exec => { exec.UseSplitQuery = true; // Avoid cartesian explosion exec.UseNoTracking = true; // Enabled by default @@ -54,12 +54,12 @@ var result = await _context.Users.FlexQueryAsync(parameters, exec => [HttpGet] public async Task Get([FromQuery] FlexQueryParameters parameters) { - return Ok(await _context.Users.FlexQueryAsync(parameters)); + return Ok(await _context.Customers.FlexQueryAsync(parameters)); } ``` 3. **Query** - `GET /api/users?filter=Name:contains:John&sort=CreatedAt:desc&select=Id,Name` + `GET /api/customers?filter=Name:contains:John&sort=CreatedDate:desc&select=Id,Name` --- diff --git a/docs/shared/operators.md b/docs/shared/operators.md index 730df05..bea3230 100644 --- a/docs/shared/operators.md +++ b/docs/shared/operators.md @@ -18,10 +18,10 @@ All filter operators supported by FlexQuery.NET, with examples, SQL translations ### Examples ``` -GET /api/users?filter=age:eq:30 -GET /api/users?filter=age:gte:18 -GET /api/users?filter=createdAt:gt:2024-01-01 -GET /api/users?filter=price:lte:99.99 +GET /api/customers?filter=salary:eq:50000 +GET /api/customers?filter=salary:gte:50000 +GET /api/customers?filter=createdDate:gt:2024-01-01 +GET /api/customers?filter=total:lte:99.99 ``` **SQL:** @@ -46,10 +46,10 @@ WHERE Price <= 99.99 ### Examples ``` -GET /api/users?filter=name:contains:alice -GET /api/users?filter=email:startswith:admin -GET /api/users?filter=email:endswith:.com -GET /api/users?filter=name:like:%ali% +GET /api/customers?filter=name:contains:alice +GET /api/customers?filter=email:startswith:admin +GET /api/customers?filter=email:endswith:.com +GET /api/customers?filter=name:like:%ali% ``` **SQL:** @@ -75,8 +75,8 @@ WHERE Name LIKE '%ali%' ### Examples ``` -GET /api/users?filter=deletedAt:isnull -GET /api/users?filter=profilePicture:isnotnull +GET /api/customers?filter=address:isnull +GET /api/customers?filter=address:isnotnull ``` **SQL:** @@ -97,8 +97,8 @@ WHERE ProfilePicture IS NOT NULL ### Examples ``` -GET /api/users?filter=status:in:active,pending,trial -GET /api/users?filter=status:notin:deleted,banned +GET /api/customers?filter=status:in:active,pending,trial +GET /api/customers?filter=status:notin:deleted,banned ``` **SQL:** @@ -118,7 +118,7 @@ WHERE Status NOT IN ('deleted', 'banned') ### Example ``` -GET /api/users?filter=age:between:18,65 +GET /api/customers?filter=salary:between:50000,150000 GET /api/orders?filter=amount:between:100,500 ``` @@ -143,7 +143,7 @@ These operators work on navigation collection properties. ### any ``` -GET /api/users?filter=orders:any:status:eq:shipped +GET /api/customers?filter=orders:any:status:eq:shipped ``` **SQL:** @@ -156,7 +156,7 @@ WHERE EXISTS ( ### all ``` -GET /api/users?filter=orders:all:status:eq:confirmed +GET /api/customers?filter=orders:all:status:eq:confirmed ``` **SQL:** @@ -169,7 +169,7 @@ WHERE NOT EXISTS ( ### count ``` -GET /api/users?filter=orders:count:gt:5 +GET /api/customers?filter=orders:count:gt:5 ``` **SQL:** @@ -180,7 +180,7 @@ WHERE (SELECT COUNT(*) FROM Orders o WHERE o.UserId = u.Id) > 5 ### Nested any (FQL) ``` -GET /api/users?filter=Orders.any(Status = "shipped" AND Amount > 100) +GET /api/customers?filter=Orders.any(Status = "shipped" AND Total > 100) ``` --- @@ -204,9 +204,9 @@ FQL uses natural language symbols: **FQL examples:** ``` -GET /api/users?filter=status = "active" AND age >= 18 -GET /api/users?filter=(name = "alice" OR name = "bob") AND status = "active" -GET /api/users?filter=Orders.any(Status = "shipped") +GET /api/customers?filter=status = "active" AND salary >= 50000 +GET /api/customers?filter=(name = "alice" OR name = "bob") AND status = "active" +GET /api/customers?filter=Orders.any(Status = "shipped") ``` --- From 696ca3b3257ccf30a566433f48ab7b56899ceb03 Mon Sep 17 00:00:00 2001 From: Peter John Casasola Date: Mon, 13 Jul 2026 04:10:57 +0800 Subject: [PATCH 4/6] refactor: replace DSL symbolic logical operators with AND/OR keywords - DslTokenizer: remove & and | tokenization; add StartsWithLogicalKeyword helper so AND/OR are recognized as identifiers and act as logical operator delimiters - DslAstParser: add MatchAnd/MatchOr helpers; parser now accepts AND/OR identifier tokens as logical operators - DslSafetyValidator: remove & and | from AllowedChars regex - DslTokenKind: update XML docs to reference AND/OR instead of &/| - ExampleProvider: update filter example string to use AND/OR - Tests: update DslQueryParserTests, FilterTests, FieldSecurityTests, and IncludeParserTests to use AND/OR syntax --- .../Documentation/ExampleProvider.cs | 2 +- .../Parsers/Dsl/Ast/DslAstParser.cs | 20 +++++++- .../Parsers/Dsl/Ast/DslSafetyValidator.cs | 2 +- .../Parsers/Dsl/Ast/DslTokenKind.cs | 4 +- .../Parsers/Dsl/Ast/DslTokenizer.cs | 50 ++++++++++++------- .../Parsers/DslQueryParserTests.cs | 14 +++--- .../Tests/FieldSecurityTests.cs | 4 +- .../FlexQuery.NET.Tests/Tests/FilterTests.cs | 2 +- .../Tests/IncludeParserTests.cs | 6 +-- 9 files changed, 67 insertions(+), 37 deletions(-) diff --git a/src/FlexQuery.NET.OpenApi/Documentation/ExampleProvider.cs b/src/FlexQuery.NET.OpenApi/Documentation/ExampleProvider.cs index 848d105..466caf0 100644 --- a/src/FlexQuery.NET.OpenApi/Documentation/ExampleProvider.cs +++ b/src/FlexQuery.NET.OpenApi/Documentation/ExampleProvider.cs @@ -75,7 +75,7 @@ internal static class ExampleProvider internal static FlexQueryParameters CreateParametersExample() => new() { - Filter = "Status:eq:Active,Age:gte:18&(Role:eq:Admin|Role:eq:Manager)", + Filter = "Status:eq:Active,Age:gte:18 AND (Role:eq:Admin OR Role:eq:Manager)", Sort = "LastName:asc,FirstName:asc", Select = "Id,FirstName,LastName,Email,Profile.AvatarUrl", Include = "Orders,Profile", diff --git a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslAstParser.cs b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslAstParser.cs index 034a5d2..fc6d871 100644 --- a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslAstParser.cs +++ b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslAstParser.cs @@ -31,7 +31,7 @@ private DslAstNode ParseOr() { var children = new List { ParseAnd() }; - while (Match(DslTokenKind.Or)) + while (MatchOr()) { children.Add(ParseAnd()); } @@ -45,7 +45,7 @@ private DslAstNode ParseAnd() { var children = new List { ParsePrimary() }; - while (Match(DslTokenKind.And)) + while (MatchAnd()) { children.Add(ParsePrimary()); } @@ -55,6 +55,22 @@ private DslAstNode ParseAnd() : new LogicalNode("and", Flatten("and", children)); } + private bool MatchOr() + { + if (Current.Kind == DslTokenKind.Or) { _position++; return true; } + if (Current.Kind == DslTokenKind.Identifier && Current.Value.Equals("OR", StringComparison.OrdinalIgnoreCase)) + { _position++; return true; } + return false; + } + + private bool MatchAnd() + { + if (Current.Kind == DslTokenKind.And) { _position++; return true; } + if (Current.Kind == DslTokenKind.Identifier && Current.Value.Equals("AND", StringComparison.OrdinalIgnoreCase)) + { _position++; return true; } + return false; + } + private DslAstNode ParsePrimary() { if (Match(DslTokenKind.Not)) diff --git a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslSafetyValidator.cs b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslSafetyValidator.cs index 3bc050c..b90633e 100644 --- a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslSafetyValidator.cs +++ b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslSafetyValidator.cs @@ -7,7 +7,7 @@ namespace FlexQuery.NET.Parsers.Dsl; internal static class DslSafetyValidator { private static readonly Regex AllowedChars = new( - @"^[A-Za-z0-9_\.\:\&\|\(\)\,\-'" + "\"" + @"\\\s@\/\?\=\%\+\#\~!]+$", + @"^[A-Za-z0-9_\.\:\(\)\,\-'" + "\"" + @"\\\s@\/\?\=\%\+\#\~!]+$", RegexOptions.Compiled); private static readonly Regex FieldPattern = new( diff --git a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenKind.cs b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenKind.cs index eef46a1..2679834 100644 --- a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenKind.cs +++ b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenKind.cs @@ -7,9 +7,9 @@ internal enum DslTokenKind Identifier, /// The ':' separator between condition parts. Colon, - /// The '&' logical AND operator. + /// The 'AND' logical AND operator. And, - /// The '|' logical OR operator. + /// The 'OR' logical OR operator. Or, /// The unary '!' NOT operator. Not, diff --git a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenizer.cs b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenizer.cs index f569cf1..e2e4626 100644 --- a/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenizer.cs +++ b/src/FlexQuery.NET/Parsers/Dsl/Ast/DslTokenizer.cs @@ -36,6 +36,15 @@ public IReadOnlyList Tokenize() continue; } + if (StartsWithLogicalKeyword(_position, "AND") || StartsWithLogicalKeyword(_position, "OR")) + { + var keyword = StartsWithLogicalKeyword(_position, "AND") ? "AND" : "OR"; + tokens.Add(new DslToken(DslTokenKind.Identifier, keyword, _position)); + _position += keyword.Length; + _colonCount = 0; + continue; + } + var start = _position; switch (current) { @@ -48,13 +57,13 @@ public IReadOnlyList Tokenize() _valueMode = true; } break; - case '&': - tokens.Add(new DslToken(DslTokenKind.And, "&", start)); + case '(': + tokens.Add(new DslToken(DslTokenKind.OpenParen, "(", start)); _position++; _colonCount = 0; break; - case '|': - tokens.Add(new DslToken(DslTokenKind.Or, "|", start)); + case ')': + tokens.Add(new DslToken(DslTokenKind.CloseParen, ")", start)); _position++; _colonCount = 0; break; @@ -63,16 +72,6 @@ public IReadOnlyList Tokenize() _position++; _colonCount = 0; break; - case '(': - tokens.Add(new DslToken(DslTokenKind.OpenParen, "(", start)); - _position++; - _colonCount = 0; - break; - case ')': - tokens.Add(new DslToken(DslTokenKind.CloseParen, ")", start)); - _position++; - _colonCount = 0; - break; case '"': case '\'': tokens.Add(ReadQuoted(current)); @@ -87,6 +86,21 @@ public IReadOnlyList Tokenize() return tokens; } + private bool StartsWithLogicalKeyword(int position, string keyword) + { + if (position + keyword.Length > _source.Length) + return false; + + if (!_source[position..(position + keyword.Length)].Equals(keyword, StringComparison.OrdinalIgnoreCase)) + return false; + + var after = position + keyword.Length; + if (after < _source.Length && char.IsLetter(_source[after])) + return false; + + return true; + } + private DslToken ReadValue() { var start = _position; @@ -111,7 +125,7 @@ private DslToken ReadValue() while (_position < _source.Length) { current = _source[_position]; - if (current is '&' or '|' or ')' or '(') + if (current is ')' or '(' || StartsWithLogicalKeyword(_position, "AND") || StartsWithLogicalKeyword(_position, "OR")) { break; } @@ -141,7 +155,7 @@ private DslToken ReadEscapedValue(int tokenStart, int valueStart) while (_position < _source.Length) { var current = _source[_position]; - if (current is '&' or '|' or ')' or '(') + if (current is ')' or '(' || StartsWithLogicalKeyword(_position, "AND") || StartsWithLogicalKeyword(_position, "OR")) { break; } @@ -174,7 +188,7 @@ private DslToken ReadIdentifier() while (_position < _source.Length) { var current = _source[_position]; - if (current is ':' or '&' or '|' or '(' or ')' || char.IsWhiteSpace(current)) + if (current is ':' or '(' or ')' || char.IsWhiteSpace(current) || StartsWithLogicalKeyword(_position, "AND") || StartsWithLogicalKeyword(_position, "OR")) break; if (current == '\\') @@ -199,7 +213,7 @@ private DslToken ReadEscapedIdentifier(int tokenStart) while (_position < _source.Length) { var current = _source[_position]; - if (current is ':' or '&' or '|' or '(' or ')' || char.IsWhiteSpace(current)) + if (current is ':' or '(' or ')' || char.IsWhiteSpace(current) || StartsWithLogicalKeyword(_position, "AND") || StartsWithLogicalKeyword(_position, "OR")) break; if (current == '\\' && _position + 1 < _source.Length) diff --git a/tests/FlexQuery.NET.Tests/Parsers/DslQueryParserTests.cs b/tests/FlexQuery.NET.Tests/Parsers/DslQueryParserTests.cs index efc00ec..836d3ac 100644 --- a/tests/FlexQuery.NET.Tests/Parsers/DslQueryParserTests.cs +++ b/tests/FlexQuery.NET.Tests/Parsers/DslQueryParserTests.cs @@ -23,7 +23,7 @@ public void DslFilter_GroupedOrAndCondition_ParsedToNestedFilterGroup() { var opts = Parse(new() { - ["filter"] = "(name:eq:john|name:eq:doe)&age:gt:20" + ["filter"] = "(name:eq:john OR name:eq:doe) AND age:gt:20" }); opts.Filter.Should().NotBeNull(); @@ -41,7 +41,7 @@ public void DslFilter_NestedPathAndInOperator_ParsedCorrectly() { var opts = Parse(new() { - ["filter"] = "orders.customer.name:contains:'john doe'&status:in:Active,Pending", + ["filter"] = "orders.customer.name:contains:'john doe' AND status:in:Active,Pending", ["page"] = "2", ["pageSize"] = "15" }); @@ -88,7 +88,7 @@ public void DslFilter_PhaseTwoAndThreeOperators_AreParsedCorrectly() { var opts = Parse(new() { - ["filter"] = "(name:startswith:jo|name:endswith:hn)&status:notin:Inactive,Deleted&age:between:18,60&deletedAt:notnull" + ["filter"] = "(name:startswith:jo OR name:endswith:hn) AND status:notin:Inactive,Deleted AND age:between:18,60 AND deletedAt:notnull" }); opts.Filter!.Logic.Should().Be(LogicOperator.And); @@ -108,7 +108,7 @@ public void DslFilter_NestedDynamicConditions_ParsedToRecursiveGroups() { var opts = Parse(new() { - ["filter"] = "((city:eq:London|city:eq:Berlin)&(age:between:25,40|status:eq:Pending))" + ["filter"] = "((city:eq:London OR city:eq:Berlin) AND (age:between:25,40 OR status:eq:Pending))" }); opts.Filter.Should().NotBeNull(); @@ -134,7 +134,7 @@ public void Dsl_ValidSimple_ParsesIntoFilterGroup() { var opts = Parse(new() { - ["filter"] = "(name:eq:john|name:eq:doe)&age:gt:20" + ["filter"] = "(name:eq:john OR name:eq:doe) AND age:gt:20" }); opts.Filter.Should().NotBeNull(); @@ -161,7 +161,7 @@ public void Dsl_MalformedExpression_IsRejected() { var act = () => Parse(new() { - ["filter"] = "(name:eq:john|age:gt:20" + ["filter"] = "(name:eq:john OR age:gt:20" }); act.Should().Throw() @@ -802,7 +802,7 @@ public void DslIntegration_AllParameters_ParsedCorrectly() { var opts = Parse(new() { - ["filter"] = "((status:eq:Open|status:eq:Pending)&amount:gt:100)|customer.name:contains:john", + ["filter"] = "((status:eq:Open OR status:eq:Pending) AND amount:gt:100) OR customer.name:contains:john", ["sort"] = "createdAt:desc,name:asc", ["select"] = "Id,Name,CustomerName", ["include"] = "Orders,Profile", diff --git a/tests/FlexQuery.NET.Tests/Tests/FieldSecurityTests.cs b/tests/FlexQuery.NET.Tests/Tests/FieldSecurityTests.cs index 586587f..516afa7 100644 --- a/tests/FlexQuery.NET.Tests/Tests/FieldSecurityTests.cs +++ b/tests/FlexQuery.NET.Tests/Tests/FieldSecurityTests.cs @@ -186,7 +186,7 @@ public void Should_Fail_When_Field_Depth_Is_Exceeded() [Fact] public void NonStrictValidation_ShouldRemoveUnauthorizedFilterFields() { - var options = QueryOptionsParser.Parse(new Dictionary { { "filter", "Name:eq:john&SSN:eq:123" } }); + var options = QueryOptionsParser.Parse(new Dictionary { { "filter", "Name:eq:john AND SSN:eq:123" } }); var execOptions = new QueryExecutionOptions { BlockedFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "SSN" }, @@ -254,7 +254,7 @@ public void NonStrictValidation_ShouldRemoveUnauthorizedSortFields() [Fact] public void NonStrictValidation_ShouldRemoveUnauthorizedBlockedFields() { - var options = QueryOptionsParser.Parse(new Dictionary { { "filter", "Name:eq:john&SSN:eq:123" } }); + var options = QueryOptionsParser.Parse(new Dictionary { { "filter", "Name:eq:john AND SSN:eq:123" } }); var execOptions = new QueryExecutionOptions { BlockedFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "SSN" }, diff --git a/tests/FlexQuery.NET.Tests/Tests/FilterTests.cs b/tests/FlexQuery.NET.Tests/Tests/FilterTests.cs index a645767..ae11d01 100644 --- a/tests/FlexQuery.NET.Tests/Tests/FilterTests.cs +++ b/tests/FlexQuery.NET.Tests/Tests/FilterTests.cs @@ -342,7 +342,7 @@ public void Filter_DslNestedDynamicConditions_AppliesRecursiveGroups() { var opts = Parse(new() { - ["filter"] = "((city:eq:London|city:eq:Berlin)&(age:between:25,40|status:eq:Pending))" + ["filter"] = "((city:eq:London OR city:eq:Berlin) AND (age:between:25,40 OR status:eq:Pending))" }); opts.Paging.Disabled = true; diff --git a/tests/FlexQuery.NET.Tests/Tests/IncludeParserTests.cs b/tests/FlexQuery.NET.Tests/Tests/IncludeParserTests.cs index 2f205b0..6184dd1 100644 --- a/tests/FlexQuery.NET.Tests/Tests/IncludeParserTests.cs +++ b/tests/FlexQuery.NET.Tests/Tests/IncludeParserTests.cs @@ -81,7 +81,7 @@ public void DslParse_FilteredIncludes_ParsesFiltersAtEachLevel() [Fact] public void DslParse_ComplexFilterWithParentheses_IgnoresInnerParenthesesForSplit() { - var result = DslIncludeParser.Parse("orders(status:eq:Cancelled&(total:gt:100|type:eq:VIP)),profile"); + var result = DslIncludeParser.Parse("orders(status:eq:Cancelled AND (total:gt:100 OR type:eq:VIP)),profile"); result.Should().HaveCount(2); @@ -102,8 +102,8 @@ public void DslParse_ComplexFilterWithParentheses_IgnoresInnerParenthesesForSpli [InlineData("orders(Amount:gte:100)")] [InlineData("orders(Amount:lt:50)")] [InlineData("orders(Amount:lte:200)")] - [InlineData("orders(Status:eq:Active&Amount:gt:100)")] - [InlineData("orders(Status:eq:Active|Status:eq:Pending)")] + [InlineData("orders(Status:eq:Active AND Amount:gt:100)")] + [InlineData("orders(Status:eq:Active OR Status:eq:Pending)")] public void DslParse_ValidSyntax_DoesNotThrow(string input) { var act = () => DslIncludeParser.Parse(input); From cf43fed7519a53d02326c7bcb05250a9c697b0bc Mon Sep 17 00:00:00 2001 From: Peter John Casasola Date: Mon, 13 Jul 2026 08:13:37 +0800 Subject: [PATCH 5/6] docs: update readme file --- FlexQueryDemo/src/test/setup.ts | 1 + src/FlexQuery.NET.Adapters.AgGrid/README.md | 1 + src/FlexQuery.NET.Parsers.MiniOData/README.md | 2 +- src/FlexQuery.NET/README.md | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 FlexQueryDemo/src/test/setup.ts diff --git a/FlexQueryDemo/src/test/setup.ts b/FlexQueryDemo/src/test/setup.ts new file mode 100644 index 0000000..a9d0dd3 --- /dev/null +++ b/FlexQueryDemo/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/src/FlexQuery.NET.Adapters.AgGrid/README.md b/src/FlexQuery.NET.Adapters.AgGrid/README.md index 9e0b844..a9a768a 100644 --- a/src/FlexQuery.NET.Adapters.AgGrid/README.md +++ b/src/FlexQuery.NET.Adapters.AgGrid/README.md @@ -20,6 +20,7 @@ dotnet add package FlexQuery.NET.Adapters.AgGrid ```csharp using FlexQuery.NET.Adapters.AgGrid; +using FlexQuery.NET.Adapters.AgGrid.Models; [HttpPost("grid-data")] public async Task GetGridData([FromBody] AgGridRequest request) diff --git a/src/FlexQuery.NET.Parsers.MiniOData/README.md b/src/FlexQuery.NET.Parsers.MiniOData/README.md index 3135b5b..2e69d74 100644 --- a/src/FlexQuery.NET.Parsers.MiniOData/README.md +++ b/src/FlexQuery.NET.Parsers.MiniOData/README.md @@ -94,5 +94,5 @@ fails with a clear `MiniODataParseException` rather than being silently ignored. ## Documentation -- [MiniOData Integration Guide](https://flexquery.vercel.app/adapters/miniodata) +- [MiniOData Integration Guide](https://flexquery.vercel.app/parsers/miniodata) - [Query Formats Guide](https://flexquery.vercel.app/guide/query-formats) diff --git a/src/FlexQuery.NET/README.md b/src/FlexQuery.NET/README.md index f68aa39..4391373 100644 --- a/src/FlexQuery.NET/README.md +++ b/src/FlexQuery.NET/README.md @@ -38,7 +38,7 @@ The native FlexQuery DSL is included by default. ```http GET /api/products? -filter=category:eq:electronics|price:between:1000,5000 +filter=category:eq:electronics OR price:between:1000,5000 &select=id,name,price,supplier.name &sort=price:desc,name:asc &include=supplier,reviews From 9496a0aba1db75c26fd0a639a6a4dcbd5e5500d3 Mon Sep 17 00:00:00 2001 From: Peter John Casasola Date: Mon, 13 Jul 2026 08:27:46 +0800 Subject: [PATCH 6/6] docs: update guide details --- docs/.vitepress/theme/index.ts | 60 +++++++++++++++++++++++++++++---- docs/examples/formats.md | 10 +++--- docs/guide/basic-usage.md | 5 ++- docs/guide/comparison.md | 2 +- docs/guide/core-concepts.md | 2 +- docs/guide/debugging.md | 2 +- docs/guide/dotnet-comparison.md | 2 +- docs/guide/dto-mapping.md | 2 +- docs/guide/end-to-end.md | 4 +-- docs/guide/filtering.md | 4 +-- docs/guide/query-syntax.md | 3 +- docs/index.md | 2 +- docs/migration/v3-to-v4.md | 30 ++++++++++++++--- docs/parsers/fql.md | 2 +- 14 files changed, 98 insertions(+), 32 deletions(-) diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 44518f7..9dc1abc 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -3,28 +3,74 @@ import Layout from './Layout.vue' import './custom.css' import type { EnhanceAppContext } from 'vitepress' +function setPageMode(path: string): void { + const vpDoc = document.querySelector('.VPDoc') + if (!vpDoc) return + + vpDoc.classList.remove('page-guide', 'page-performance', 'page-examples', 'page-comparison', 'page-reference', 'page-home') + + if (path === '/' || path === '/index.html') { + vpDoc.classList.add('page-home') + return + } + + if (path.startsWith('/guide/performance/')) { + vpDoc.classList.add('page-performance') + return + } + + if ( + path === '/guide/comparison' || + path === '/guide/comparison-libraries' || + path === '/comparison/graphql-odata' || + path === '/guide/dotnet-comparison' + ) { + vpDoc.classList.add('page-comparison') + return + } + + if (path.startsWith('/examples/')) { + vpDoc.classList.add('page-examples') + return + } + + if (path.startsWith('/guide/') || path.startsWith('/migration/')) { + vpDoc.classList.add('page-guide') + return + } + + vpDoc.classList.add('page-reference') +} + export default { ...DefaultTheme, - // Layout, enhanceApp({ router }: EnhanceAppContext) { if (typeof window === 'undefined') return - router.onAfterRouteChanged = () => { - // Re-open the sidebar after navigation so it doesn't close on link click. - // VitePress uses a data attribute on to track the sidebar open state. + const setPageModeWhenReady = (path: string) => { + const vpDoc = document.querySelector('.VPDoc') + if (vpDoc) { + setPageMode(path) + } else { + requestAnimationFrame(() => setPageModeWhenReady(path)) + } + } + + setPageModeWhenReady(decodeURIComponent(window.location.pathname)) + + router.onAfterRouteChanged = (to: string) => { const el = document.querySelector('.VPSidebar') as HTMLElement | null if (el) { - // On narrow viewports VitePress removes aria-expanded; we just ensure - // the sidebar keeps its visible class rather than being hidden. el.style.display = '' el.removeAttribute('inert') } - // Keep the screen overlay clear — prevent the mask from re-appearing. const mask = document.querySelector('.VPSidebarMask') as HTMLElement | null if (mask) { mask.style.display = 'none' } + + setPageModeWhenReady(to) } } } diff --git a/docs/examples/formats.md b/docs/examples/formats.md index dda9185..1f4c56c 100644 --- a/docs/examples/formats.md +++ b/docs/examples/formats.md @@ -17,18 +17,18 @@ GET /api/customers?filter=status:eq:active&sort=name:asc ### Compound Filter (AND) -Multiple conditions are separated by commas (AND logic). URL-encode `&` as `%26`. +Multiple conditions are separated by commas (AND logic). Use the `AND` keyword to join additional conditions. **Request:** ```http -GET /api/customers?filter=status:eq:active%26city:eq:New York +GET /api/customers?filter=status:eq:'active' AND city:eq:'New York' ``` ### OR Logic **Request:** ```http -GET /api/customers?filter=status:eq:active|status:eq:pending +GET /api/customers?filter=status:eq:'active' OR status:eq:'pending' ``` ### Range and Set Filter @@ -36,7 +36,7 @@ GET /api/customers?filter=status:eq:active|status:eq:pending **Request:** ```http GET /api/customers - ?filter=(salary:between:50000,100000)%26(status:in:Active,Review) + ?filter=(salary:between:50000,100000) AND (status:in:'Active',Review) &sort=salary:asc ``` @@ -190,7 +190,7 @@ GET /api/customers?$filter=status eq 'active'&$count=true | :--- | :--- | :--- | :--- | :--- | | **URL-friendly** | ✅ Very | ⚠️ Needs encoding | ⚠️ Needs encoding | ✅ Good | | **Human-readable** | Medium | ✅ High | Medium | ✅ High | -| **Nested logic** | Limited | ✅ Full | ✅ Full | ✅ Full | +| **Nested logic** | ✅ Full | ✅ Full | ✅ Full | ✅ Full | | **Package required** | Core (default) | `FlexQuery.NET.Parsers.Fql` | Core (default) | `FlexQuery.NET.Parsers.MiniOData` | | **Registration** | None | `Fql.Register()` | None | `MiniOData.Register()` | | **Best for** | Internal tools | Developer tools | Complex conditions | OData migration | diff --git a/docs/guide/basic-usage.md b/docs/guide/basic-usage.md index e74d6ac..7899137 100644 --- a/docs/guide/basic-usage.md +++ b/docs/guide/basic-usage.md @@ -28,9 +28,9 @@ Filtering allows you to restrict the results based on property values. The stand - **In Collection**: `Status:in:Active,Pending` ### Multiple Filters -Multiple filters are combined using the **AND** operator (`&`), which must be URL-encoded as `%26` in HTTP requests. +Multiple filters are combined using the `AND` keyword. -`?filter=Status:eq:Active%26Price:gt:100` +`?filter=Status:eq:'Active' AND Price:gt:100` ### Nested Properties You can filter on nested navigation properties using dot notation. FlexQuery automatically handles generating the underlying SQL `JOIN` or EF Core `Include` logic. @@ -150,5 +150,4 @@ This single call handles: ## Best Practices -- **URL Encode:** Always remind frontend developers to use `encodeURIComponent()` (in JavaScript/TypeScript) on their filter strings. The `&` character will break HTTP routing if it is not encoded as `%26`. - **Use Paging Defaults:** Always specify a `DefaultPageSize` and `MaxPageSize` in your execution options to prevent accidental `SELECT * FROM Table` scenarios. diff --git a/docs/guide/comparison.md b/docs/guide/comparison.md index c11f128..a450388 100644 --- a/docs/guide/comparison.md +++ b/docs/guide/comparison.md @@ -82,7 +82,7 @@ OData focuses on standardized REST querying and interoperability. It is commonly ```http GET /api/users - ?filter=name:contains:alice%26status:eq:active + ?filter=name:contains:'alice' AND status:eq:'active' &sort=createdAt:desc &page=1 &pageSize=10 diff --git a/docs/guide/core-concepts.md b/docs/guide/core-concepts.md index bb14813..c9ed1e7 100644 --- a/docs/guide/core-concepts.md +++ b/docs/guide/core-concepts.md @@ -165,7 +165,7 @@ FlexQuery supports multiple query syntaxes. The active syntax is configured glob ### DSL Format (Default) ```http -GET /api/customers?filter=Status:eq:Active%26LastName:contains:Smi&sort=LastName:asc +GET /api/customers?filter=Status:eq:'Active' AND LastName:contains:Smi&sort=LastName:asc ``` ### FQL Format (SQL-like) diff --git a/docs/guide/debugging.md b/docs/guide/debugging.md index e2c4f43..01ebd2d 100644 --- a/docs/guide/debugging.md +++ b/docs/guide/debugging.md @@ -180,7 +180,7 @@ ORDER BY [u].[Name] ### "My filter isn't working" -1. Check `options.Filter` in the parsed AST — is it null? The format might not have been recognized, or a URL decoding issue occurred (e.g., using `&` instead of `%26` for multiple filters). +1. Check `options.Filter` in the parsed AST — is it null? The format might not have been recognized, or a syntax issue occurred (e.g., using `AND`/`OR` keywords instead of the old `&`/`|` symbols). 2. Check the operator — typos in operator strings are silently ignored during parsing but caught during validation. ### "Results are empty but shouldn't be" diff --git a/docs/guide/dotnet-comparison.md b/docs/guide/dotnet-comparison.md index 8fceb8a..092e873 100644 --- a/docs/guide/dotnet-comparison.md +++ b/docs/guide/dotnet-comparison.md @@ -93,7 +93,7 @@ Because expressions are string-based and parsed blindly, applications typically ### FlexQuery.NET ```http -GET /api/customers?filter=status:eq:active%26salary:gte:50000&sort=name:asc&page=2&pageSize=10 +GET /api/customers?filter=status:eq:'active' AND salary:gte:50000&sort=name:asc&page=2&pageSize=10 ``` ```csharp diff --git a/docs/guide/dto-mapping.md b/docs/guide/dto-mapping.md index 9a0317f..d643acc 100644 --- a/docs/guide/dto-mapping.md +++ b/docs/guide/dto-mapping.md @@ -48,7 +48,7 @@ public async Task GetCustomers([FromQuery] FlexQueryParameters pa ### The Client Request ```http -GET /api/customers?filter=displayName:contains:John%26company:eq:AcmeCorp&sort=displayName:asc +GET /api/customers?filter=displayName:contains:'John' AND company:eq:AcmeCorp&sort=displayName:asc ``` ### How it Works (Under the Hood) diff --git a/docs/guide/end-to-end.md b/docs/guide/end-to-end.md index dfbc466..9cbd03c 100644 --- a/docs/guide/end-to-end.md +++ b/docs/guide/end-to-end.md @@ -18,10 +18,10 @@ When introducing a new query engine to a team, developers often wonder "Where is A client wants to find all **Active** products in the **Electronics** category with a price greater than **$500**, sorted by the latest arrival. -Notice how the `&` symbol combining filters is URL-encoded as `%26` so it doesn't collide with the standard HTTP query parameter separator. +Notice how the `AND` keyword combines multiple filter conditions without requiring URL encoding. ```http -GET /api/products?filter=Status:eq:Active%26Category:eq:Electronics%26Price:gt:500&sort=CreatedAt:desc +GET /api/products?filter=Status:eq:Active AND Category:eq:Electronics AND Price:gt:500&sort=CreatedAt:desc ``` --- diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index ec272c0..701fbf7 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -44,10 +44,10 @@ GET /api/customers?filter=salary:gte:50000 GET /api/customers?filter=name:contains:alice ``` -**Compound (AND — URL-encode `&` as `%26`):** +**Compound (AND):** ```http -GET /api/customers?filter=status:eq:active%26city:eq:'New York' +GET /api/customers?filter=status:eq:active AND city:eq:'New York' ``` ### FQL Format (SQL-like) diff --git a/docs/guide/query-syntax.md b/docs/guide/query-syntax.md index 5321dbf..b64c7f8 100644 --- a/docs/guide/query-syntax.md +++ b/docs/guide/query-syntax.md @@ -76,8 +76,7 @@ FlexQueryCore.Configure(options => The most compact format. Fields, operators, and values are separated by colons: ``` -GET /api/customers?filter=City:eq:New York&sort=Name:asc -GET /api/customers?filter=City:eq:New York%26Status:eq:Active +GET /api/customers?filter=City:eq:'New York' AND Status:eq:'Active' GET /api/customers?filter=LastName:startswith:Smi&select=Id,LastName,Email ``` diff --git a/docs/index.md b/docs/index.md index f334c4d..f284b99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -151,7 +151,7 @@ All expression trees are translated to SQL by EF Core. No client-side evaluation | Format | Example | | :--- | :--- | -| **DSL** | `filter=status:eq:active & salary:gte:50000` | +| **DSL** | `filter=status:eq:active AND salary:gte:50000` | | **FQL** | `filter=status = "active" AND salary >= 50000` | --- diff --git a/docs/migration/v3-to-v4.md b/docs/migration/v3-to-v4.md index 7497532..fc9ad1d 100644 --- a/docs/migration/v3-to-v4.md +++ b/docs/migration/v3-to-v4.md @@ -1,4 +1,4 @@ -# Migrating from FlexQuery.NET 3.1.1 to 4.0 +# Migrating from v3.1.1 to v4.0.0 This guide walks through upgrading an existing FlexQuery.NET v3.1.1 application to v4. It focuses on the changes developers must make—organized by developer task rather than by individual API. @@ -397,7 +397,7 @@ if (aggregate.Function == AggregateFunction.Sum) { ... } #### Aggregates Require a Dedicated Parameter -Aggregates embedded in the `select` parameter are no longer recognized. A dedicated `aggregates` query parameter is required. +Aggregates embedded in the `select` parameter are no longer recognized. A dedicated `aggregate` query parameter is required. **Before:** ```http @@ -406,7 +406,7 @@ GET /api/orders?select=status,sum(amount),count(id)&groupBy=status **After:** ```http -GET /api/orders?select=status&aggregates=amount:sum,id:count&groupBy=status +GET /api/orders?select=status&aggregate=amount:sum,id:count&groupBy=status ``` #### AggregateModel and HavingCondition Namespaces Changed @@ -598,6 +598,24 @@ v3.1.1 extracted aggregate expressions like `sum(amount)` from the `select` para **Review:** If your application embeds aggregates in `select`, move them to the `aggregates` parameter. +#### DSL Logical Operators Changed from Symbols to Keywords + +v3.1.1 used single-character symbols for logical composition in DSL filters: `&` for AND and `|` for OR. v4 replaces these with keyword operators: `AND` and `OR`. + +**Before:** +```http +GET /api/customers?filter=status:eq:'active'&city:eq:'New York' +GET /api/customers?filter=status:eq:'active'|status:eq:'pending' +``` + +**After:** +```http +GET /api/customers?filter=status:eq:'active' AND city:eq:'New York' +GET /api/customers?filter=status:eq:'active' OR status:eq:'pending' +``` + +**Review:** Update all client code and documentation that constructs DSL filter strings. The keyword operators do not require URL encoding, which also simplifies filter string construction. + --- ## Troubleshooting @@ -786,7 +804,11 @@ No. Both versions cannot be referenced in the same project. Upgrade all FlexQuer #### Can I continue using DSL after upgrading? -Yes. DSL (default syntax) remains fully supported and unchanged. The only required change is replacing `QueryOptionsParser.Parse(parameters)` with `parameters.ToQueryOptions()`. +Yes. DSL (default syntax) remains fully supported. The main DSL change is that logical operators are now keyword-based (`AND`/`OR`) instead of symbolic (`&`/`|`). Update any filter strings accordingly, and replace `QueryOptionsParser.Parse(parameters)` with `parameters.ToQueryOptions()`. + +#### Why were the generic/indexed and raw JSON `filter` formats removed? + +v3 supported additional filter formats (generic/indexed `filter[0].field=...` and raw JSON `filter` strings) that added parser complexity and conflicted with model-binding conventions. The recommended replacement is to use the strongly-typed request DTOs: `FlexQueryParameters` for GET query-string parameters, `FlexQueryRequest` for POST JSON bodies, and `MiniODataRequest` for MiniOData-style requests. --- diff --git a/docs/parsers/fql.md b/docs/parsers/fql.md index 48221a3..b2e3ee6 100644 --- a/docs/parsers/fql.md +++ b/docs/parsers/fql.md @@ -10,7 +10,7 @@ FQL is a SQL-inspired query language parser package. It translates human-readabl ### Why It Exists -Many developers prefer SQL-like syntax for ad-hoc queries, admin tools, and developer-facing APIs. FQL lets you accept natural language expressions (`Status = 'Active' AND Salary >= 50000`) instead of colon-separated DSL (`status:eq:active & salary:gte:50000`), making APIs more accessible to users familiar with SQL. +Many developers prefer SQL-like syntax for ad-hoc queries, admin tools, and developer-facing APIs. FQL lets you accept natural language expressions (`Status = 'Active' AND Salary >= 50000`) instead of colon-separated DSL (`status:eq:active AND salary:gte:50000`), making APIs more accessible to users familiar with SQL. ### When to Use It