Skip to content

v3.0.6

Choose a tag to compare

@github-actions github-actions released this 24 Jun 05:44

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 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.

// 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.

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 early

After: 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 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:

// 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.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.