v3.0.6
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:
SELECT [Id], [Name], [Email]
FROM [SqlEntities] AS [SqlEntities]
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
-- Msg 10753: OFFSET/FETCH requires ORDER BYThe 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
RequiresOrderByForPagingproperty toISqlDialectinterface SqlServerDialectandOracleDialectreturntrue(OFFSET/FETCH requires ORDER BY)PostgreSqlDialect,MySqlDialect,MariaDbDialect,SqliteDialectreturnfalse(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.
// Before (wrong):
var result = await connection.FlexQueryAsync<Order>(options, new DapperQueryOptions
{
IncludeTotalCount = false
});
result.TotalCount; // 20 (current page size — misleading)
// After (correct):
result.TotalCount; // null3. 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.
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.
// 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.
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:
SELECT * FROM (SELECT * FROM Orders ORDER BY Id) AS sub ORDER BY Name
-- ^^^^^^^^
-- IndexOf matches here first, truncating too earlyAfter: Uses \b word-boundary regex patterns to prevent matching inside identifiers, plus parentheses-depth tracking (IsInsideParentheses) to skip matches nested inside subqueries:
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
QueryValidationExceptionfor 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:
// 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:
// 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
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.6Full test suite: 843 tests passing, zero regressions. Additionally resolves a pre-existing flaky test (FilteredIncludeTests.ToProjectedQueryResultAsync_AppliesFilteredIncludes) caused by the concurrent parser-registration race.