FlexQuery.NET.Parsers.Jql
3.0.6
See the version list below for details.
dotnet add package FlexQuery.NET.Parsers.Jql --version 3.0.6
NuGet\Install-Package FlexQuery.NET.Parsers.Jql -Version 3.0.6
<PackageReference Include="FlexQuery.NET.Parsers.Jql" Version="3.0.6" />
<PackageVersion Include="FlexQuery.NET.Parsers.Jql" Version="3.0.6" />
<PackageReference Include="FlexQuery.NET.Parsers.Jql" />
paket add FlexQuery.NET.Parsers.Jql --version 3.0.6
#r "nuget: FlexQuery.NET.Parsers.Jql, 3.0.6"
#:package FlexQuery.NET.Parsers.Jql@3.0.6
#addin nuget:?package=FlexQuery.NET.Parsers.Jql&version=3.0.6
#tool nuget:?package=FlexQuery.NET.Parsers.Jql&version=3.0.6
FlexQuery.NET
Dynamic filtering, sorting, paging, and projection for IQueryable in .NET.
FlexQuery.NET is a lightweight and powerful dynamic query engine for .NET. It allows you to transform complex API query parameters into optimized, EF Core-translatable expression trees with a single line of code.
⚡ Key Features
- Dynamic Querying: Powerful DSL, JQL, and JSON-based filtering.
- IQueryable-Native: 100% server-side translation—no client-side evaluation.
- Advanced Projection: Automatic SQL
SELECToptimization including nested includes. - Governance & Security: Built-in field-level validation and operator restrictions.
- High Performance: Thread-safe expression caching for ultra-low latency.
🚀 Quick Start
1. Installation
dotnet add package FlexQuery.NET
dotnet add package FlexQuery.NET.EntityFrameworkCore
dotnet add package FlexQuery.NET.Dapper
dotnet add package FlexQuery.NET.AspNetCore
dotnet add package FlexQuery.NET.Adapters.AgGrid
2. Entity Framework Core (Default)
Securely execute a dynamic query directly from your controller against an EF Core DbContext. The provider handles translation, pagination, and async execution automatically.
[HttpGet("users")]
public async Task<IActionResult> GetUsers([FromQuery] FlexQueryParameters parameters)
{
var result = await _context.Users.FlexQueryAsync(parameters, options =>
{
options.AllowedFields = new HashSet<string> { "Id", "Name", "Email", "Status" };
options.StrictFieldValidation = true;
options.UseNoTracking = true; // Optimization for read-only queries
});
return Ok(result);
}
3. Dapper & Raw SQL Integration
For high-performance API endpoints or non-EF Core projects, use the Dapper provider to generate secure, dialect-aware, fully parameterized SQL queries.
using FlexQuery.NET.Dapper;
using FlexQuery.NET.Dapper.Dialects;
[HttpGet("users")]
public async Task<IActionResult> GetUsersDapper([FromQuery] FlexQueryParameters parameters)
{
using var connection = new SqlConnection("Server=...;");
// Generates parameterized SQL, handles dialects (SQL Server, Postgres, MySQL, etc.)
var result = await connection.FlexQueryAsync<User>(parameters, options =>
{
options.Dialect = new SqlServerDialect();
options.AllowedFields = new HashSet<string> { "Id", "Name", "Email" };
});
return Ok(result);
}
4. AG Grid Adapter
FlexQuery.NET.Adapters.AgGrid parses AG Grid's Enterprise Server-Side Row Model JSON payloads natively, translating pagination, filtering, sorting, row grouping, and aggregations into FlexQuery operations.
[HttpPost("grid")]
public async Task<IActionResult> GetGridData([FromBody] AgGridRequest request)
{
// 1. Parse AG Grid request into canonical QueryOptions
var options = request.ToQueryOptions();
// 2. Execute via EF Core or Dapper
var result = await _context.Users.FlexQueryAsync<User>(options, opts =>
{
opts.AllowedFields = new HashSet<string> { "Id", "Name", "Status", "CreatedAt" };
});
// 3. Return format expected by AG Grid.
// Grouped SSRM responses should prefer ResultCount when available.
return Ok(new { rowData = result.Data, rowCount = result.ResultCount ?? result.TotalCount });
}
5. MiniOData Parser
Migrating from OData? FlexQuery.NET.Parsers.MiniOData acts as a drop-in bridge, automatically detecting and parsing OData syntax ($filter, $orderby, $top, $skip) on the same endpoint that handles JSON and JQL queries.
// Program.cs
builder.Services.AddFlexQueryMiniOData();
// Controller
[HttpGet("products")]
public async Task<IActionResult> GetProducts([FromQuery] FlexQueryParameters parameters)
{
// Auto-detects OData parameters like:
// ?$filter=Price gt 50 and Category eq 'Electronics'&$orderby=Name desc
var result = await _context.Products.FlexQueryAsync(parameters);
return Ok(result);
}
6. Example Query Requests
FlexQuery unifies multiple formats under the same API without configuration:
# Native DSL
GET /api/users?filter=age:gt:18&sort=createdAt:desc&page=1&pageSize=20
# JQL Syntax
GET /api/users?filter=Age > 18 AND Status = 'Active'
# OData Syntax (requires MiniOData package)
GET /api/users?$filter=Age gt 18 and Status eq 'Active'
📚 Documentation
Counting Semantics
QueryResult<T> exposes three different row counts. They answer different questions:
| Property | Meaning |
|---|---|
TotalCount |
Filtered source records |
ResultCount |
Shaped rows before paging |
Data.Count |
Current page rows |
For a normal query, TotalCount and ResultCount usually match:
1432 products
pageSize = 20
TotalCount = 1432
ResultCount = 1432
Data.Count = 20
For grouped or shaped queries, ResultCount is the count most UI grids need for paging:
1432 products
GROUP BY Brand
4 brand groups
TotalCount = 1432
ResultCount = 4
Data.Count = current page of groups
HAVING is applied before ResultCount:
1432 products
GROUP BY Brand
HAVING SUM(Quantity) > 100
2 groups remain
TotalCount = 1432
ResultCount = 2
For AG Grid SSRM grouping, prefer:
var rowCount = result.ResultCount ?? result.TotalCount;
TotalCount semantics are unchanged for backward compatibility.
For detailed guides, API references, and advanced scenarios, visit our documentation site:
👉 https://flexquery.vercel.app
Quick Links
- Getting Started
- Query Composition
- Security & Field Access
- Dapper Provider
- AG Grid Integration
- MiniOData Parser
📄 License
FlexQuery.NET is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FlexQuery.NET (>= 3.0.6)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
-
net6.0
- FlexQuery.NET (>= 3.0.6)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0.0)
-
net8.0
- FlexQuery.NET (>= 3.0.6)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
# FlexQuery.NET v3.0.6 Release Notes
**Release date:** 2026-06-24
## Overview
v3.0.6 focuses on correctness, reliability, and concurrency hardening across the Dapper and core query pipelines in the Dapper provider (invalid SQL Server/Oracle paging and IncludeTotalCount semantics), a thread-safety bug in `QueryOptionsParser` that affected all providers including EF Core, hardens `ExtractCountSql` against false positives in nested subqueries, and pins EF Core package dependency versions to patched releases.
---
## What's Fixed
### 1. SQL Server / Oracle Paging Validation
**Before:** `SqlTranslator.Translate()` could generate invalid SQL for SQL Server and Oracle:
```sql
SELECT [Id], [Name], [Email]
FROM [SqlEntities] AS [SqlEntities]
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
-- Msg 10753: OFFSET/FETCH requires ORDER BY
```
The database would throw a runtime error with no context about which query options caused the problem.
**After:** `SqlTranslator.Translate()` now validates that paging queries have an ORDER BY clause when using dialects that require it. Throws `InvalidOperationException` at translation time with a clear message:
```
Paging requires an ORDER BY clause when using the SqlServerDialect dialect.
Add at least one Sort field to QueryOptions.Sort, or set Paging.Disabled = true.
```
**Implementation:**
- Added `RequiresOrderByForPaging` property to `ISqlDialect` interface
- `SqlServerDialect` and `OracleDialect` return `true` (OFFSET/FETCH requires ORDER BY)
- `PostgreSqlDialect`, `MySqlDialect`, `MariaDbDialect`, `SqliteDialect` return `false` (LIMIT/OFFSET does not require ORDER BY)
- Validation guard placed in `SqlTranslator.Translate()` between sort resolution and clause building
- Only triggers when paging is enabled, the dialect requires ORDER BY, and no sort is present (GroupedSortValidator injects a fallback sort for GroupBy queries)
### 2. Dapper IncludeTotalCount Semantics
**Before:** `FlexQueryAsync<T>()` with `IncludeTotalCount = false` returned `TotalCount = items.Count` (the number of records in the current page), which is semantically incorrect — the caller explicitly disabled total count calculation but still received a misleading value.
**After:** `IncludeTotalCount = false` returns `TotalCount = null`. No count query is executed.
```csharp
// Before (wrong):
var result = await connection.FlexQueryAsync<Order>(options, new DapperQueryOptions
{
IncludeTotalCount = false
});
result.TotalCount; // 20 (current page size — misleading)
// After (correct):
result.TotalCount; // null
```
### 3. Core — QueryOptionsParser Thread-Safety Fix
**Affected packages:** `FlexQuery.NET` (core), `FlexQuery.NET.EntityFrameworkCore`, `FlexQuery.NET.Parsers.Jql`, `FlexQuery.NET.Parsers.MiniOData`
**Before:** `QueryOptionsParser._parsers` was a `static readonly List<IQueryParser>` modified in-place by `RegisterParser()`. The `Parse()` method enumerated this list with `FirstOrDefault()` / `Last()` without synchronization. Under concurrent request processing — or parallel test execution — one thread calling `RegisterParser` could modify the list while another thread was enumerating it, throwing `InvalidOperationException: Collection was modified; enumeration operation may not execute.`
```text
System.InvalidOperationException : Collection was modified; enumeration operation may not execute.
at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
at System.Collections.Generic.List`1.Enumerator.MoveNext()
at System.Linq.Enumerable.TryGetFirst(...)
at FlexQuery.NET.Parsers.QueryOptionsParser.Parse(FlexQueryParameters, QuerySyntax)
```
**After:** `RegisterParser` uses copy-on-write — creates a new list, inserts at priority position 0, then atomically swaps the reference. `Parse` captures the list reference locally before enumerating, so concurrent `RegisterParser` calls never affect an in-progress parse.
```csharp
// Thread-safe: creates a new list without disturbing in-flight parses
public static void RegisterParser(IQueryParser parser)
{
var updated = new List<IQueryParser>(_parsers);
updated.Insert(0, parser);
_parsers = updated;
}
```
### 4. ExtractCountSql Hardening
**Affected packages:** `FlexQuery.NET.Dapper`
**Before:** `ExtractCountSql` used naive `IndexOf` string searching for keywords `"ORDER BY"`, `"LIMIT"`, and `"OFFSET"`. This could truncate the SQL at the wrong position when those keywords appeared inside subqueries, derived tables, aliases, or identifiers.
```csharp
var keywords = new[] { "ORDER BY", "LIMIT", "OFFSET" };
var minIdx = sql.Length;
foreach (var kw in keywords)
{
var idx = sql.IndexOf(kw, StringComparison.OrdinalIgnoreCase);
if (idx >= 0 && idx < minIdx) minIdx = idx;
}
```
Example of incorrect truncation — the inner `ORDER BY` would be matched instead of the outer one:
```sql
SELECT * FROM (SELECT * FROM Orders ORDER BY Id) AS sub ORDER BY Name
-- ^^^^^^^^
-- IndexOf matches here first, truncating too early
```
**After:** Uses `\b` word-boundary regex patterns to prevent matching inside identifiers, plus parentheses-depth tracking (`IsInsideParentheses`) to skip matches nested inside subqueries:
```csharp
var patterns = new[] { @"\bORDER\s+BY\b", @"\bLIMIT\b", @"\bOFFSET\b" };
foreach (var pattern in patterns)
{
var match = Regex.Match(sql, pattern, RegexOptions.IgnoreCase);
while (match.Success)
{
if (!IsInsideParentheses(sql, match.Index))
{
if (match.Index < minIdx) minIdx = match.Index;
break;
}
match = match.NextMatch();
}
}
```
The depth tracker scans from position 0 to the match index, counting `(` as +1 and `)` as −1. A match at depth == 0 is top-level (stripped); a match at depth > 0 is inside a subquery (preserved).
### 5. EF Core Package Dependency Pinning
**Affected packages:** `FlexQuery.NET.EntityFrameworkCore`
Updated the `Microsoft.EntityFrameworkCore` package version ranges from minimum-compatible versions to specific patched releases:
| Target Framework | Before | After |
|---|---|---|
| `net6.0` | `6.0.0` | `6.0.36` |
| `net8.0` | `8.0.0` | `8.0.13` |
This updates the minimum package versions to patched releases that include security and reliability fixes while remaining API-compatible with EF Core 6 and EF Core 8. The `net10.0` target was already at `10.0.0-preview.*` and was not changed.
### 6. No-op ORDER BY Injection Rejected
The design deliberately chose fail-fast validation over silent `ORDER BY (SELECT NULL)` injection. See the design rationale:
- `ORDER BY (SELECT NULL)` is a constant-expression ORDER BY — every row produces the same value
- This makes paging non-deterministic: pages can overlap, skip records, or return duplicates
- Silent injection hides a developer mistake that would manifest as data corruption, not a crash
- Fail-fast validation is consistent with the existing FlexQuery.NET philosophy (the validation pipeline already throws `QueryValidationException` for invalid fields, operators, and type mismatches)
---
## New Test Coverage
| Test | Area | Verifies |
|------|------|----------|
| `SqlServer_PagingWithoutSort_Throws` | Paging validation | SQL Server + paging + no sort throws `InvalidOperationException` |
| `Oracle_PagingWithoutSort_Throws` | Paging validation | Oracle + paging + no sort throws `InvalidOperationException` |
| `SqlServer_GroupByPaging_Succeeds` | Paging validation | GroupedSortValidator fallback sort enables paging on SQL Server |
| `Sqlite_PagingWithoutSort_Succeeds` | Paging validation | SQLite paging works without ORDER BY |
| `PostgreSql_PagingWithoutSort_Succeeds` | Paging validation | PostgreSQL paging works without ORDER BY |
| `Dapper_IncludeTotalCountFalse_ReturnsNullTotalCount` | IncludeTotalCount | `false` returns `TotalCount == null` |
| `Dapper_IncludeTotalCountTrue_ReturnsActualCount` | IncludeTotalCount | `true` returns correct total count |
| `ExtractCountSql_TopLevelOrderBy_IsStripped` | Count SQL hardening | Top-level ORDER BY is removed |
| `ExtractCountSql_TopLevelOrderByAndLimit_AreStripped` | Count SQL hardening | ORDER BY + LIMIT removed |
| `ExtractCountSql_TopLevelOrderByOffsetFetch_AreStripped` | Count SQL hardening | ORDER BY + OFFSET/FETCH removed |
| `ExtractCountSql_OrderByInsideSubquery_IsPreserved` | Count SQL hardening | ORDER BY inside subquery kept |
| `ExtractCountSql_LimitInsideSubquery_IsPreserved` | Count SQL hardening | LIMIT inside subquery kept |
| `ExtractCountSql_OffsetInsideSubquery_IsPreserved` | Count SQL hardening | OFFSET inside subquery kept |
| `ExtractCountSql_NoPagingClauses_ReturnsWrappedSql` | Count SQL hardening | SQL without paging wraps correctly |
| `ExtractCountSql_KeywordInAlias_DoesNotTriggerFalsePositive` | Count SQL hardening | Keywords in aliases not matched |
| `ExtractCountSql_DeeplyNestedSubqueries_OnlyStripsTopLevelClauses` | Count SQL hardening | Multi-level nesting preserved |
---
## Migration Guide
### SQL Server / Oracle Paging
If you call `SqlTranslator.Translate()` directly with paging enabled and no sort, the call will now throw `InvalidOperationException`. Fix: add a sort field or disable paging:
```csharp
// Before:
var command = translator.Translate(new QueryOptions
{
Paging = { Page = 1, PageSize = 20 }
});
// After:
var command = translator.Translate(new QueryOptions
{
Sort = { new SortNode { Field = "Id" } },
Paging = { Page = 1, PageSize = 20 }
});
```
No action required if you use `FlexQueryAsync<T>()` or go through validation (`ValidateOrThrow<T>()`) — those paths either inject default sorts or disable paging before translation.
### IncludeTotalCount
If your code reads `result.TotalCount` after setting `IncludeTotalCount = false`, add a null check:
```csharp
// Before (risks NullReferenceException if called with IncludeTotalCount = false):
var count = result.TotalCount.Value;
// After:
var count = result.TotalCount ?? items.Count;
```
---
## Behavioral Changes
These changes correct previously incorrect behavior and may require minor code updates.
| Change | Impact | Mitigation |
|--------|--------|------------|
| `SqlTranslator.Translate()` now throws for SQL Server/Oracle when paging is enabled without ORDER BY | Queries that would have failed at the database now fail earlier at translation time | Add a sort field or disable paging |
| `IncludeTotalCount = false` returns `null` instead of `items.Count` | Semantically correct — callers that did not null-check `TotalCount` may get `NullReferenceException` | Add null check on `TotalCount` |
---
## Upgrading
```bash
dotnet add package FlexQuery.NET --version 3.0.6
dotnet add package FlexQuery.NET.Dapper --version 3.0.6
dotnet add package FlexQuery.NET.EntityFrameworkCore --version 3.0.6
dotnet add package FlexQuery.NET.Parsers.Jql --version 3.0.6
dotnet add package FlexQuery.NET.Parsers.MiniOData --version 3.0.6
dotnet add package FlexQuery.NET.Adapters.AgGrid --version 3.0.6
dotnet add package FlexQuery.NET.Adapters.Kendo --version 3.0.6
dotnet add package FlexQuery.NET.AspNetCore --version 3.0.6
```
**Full test suite:** 843 tests passing, zero regressions. Additionally resolves a pre-existing flaky test (`FilteredIncludeTests.ToProjectedQueryResultAsync_AppliesFilteredIncludes`) caused by the concurrent parser-registration race.