Skip to content

Releases: peterjohncasasola/FlexQuery.NET

v3.1.1

Choose a tag to compare

@github-actions github-actions released this 04 Jul 07:58

FlexQuery.NET v3.1.1 Release Notes

Release date: 2026-07-04

Overview

v3.1.1 is a stability and performance release that hardens the execution pipeline, adds validation for grouped-query constraints, fixes Dapper case-insensitive SQL generation, enables filtered include hydration in Dapper, removes the legacy projection engine, and adds cache optimization for common reflection lookups.


What's Fixed

1. Execution Pipeline Ordering

Affected packages: FlexQuery.NET, FlexQuery.NET.EntityFrameworkCore

The EF Core pipeline was reordered to the canonical sequence, fixing regressions where pipeline stages executed in the wrong order and caused SQL errors on SQLite:

Before (incorrect for grouped queries on SQLite):

Filter → Sort → Count → Distinct → GroupBy → GrandTotals → Paging → Includes → Select

After (correct canonical order):

Filter → Distinct → Count → Sort → GroupBy → GrandTotals → Paging → Includes → Select

Key fixes:

  • Count before Sort: COUNT(*) now executes before ORDER BY. Sort does not affect cardinality, and running it before Count caused SQLite decimal ORDER BY inside COUNT subqueries to fail with type mismatch errors.
  • Sort only in non-grouped path: Pre-GroupBy ORDER BY on raw entity columns is unnecessary for grouped queries (sorting happens internally via BuildGroupedSorts). Running it before GroupBy introduced decimal type issues on SQLite.

The base pipeline (QueryableExtensions) was similarly reordered for consistency.

2. High-Cardinality Paging Without Sort

Affected packages: FlexQuery.NET

When paging is requested without an explicit ORDER BY, the query engine now auto-generates an ORDER BY on the first selected field (or Id/Key/first property as fallback). This prevents non-deterministic paging results and eliminates database errors on SQL Server/Oracle that require ORDER BY for OFFSET/FETCH.

3. Dapper Case-Insensitive String Comparison

Affected packages: FlexQuery.NET.Dapper

Before: The CaseInsensitive option was ignored by the Dapper SQL generator. String comparisons always used exact case matching, producing different results than the EF Core provider.

After: SqlWhereBuilder now wraps string-type field comparisons with LOWER() when CaseInsensitive == true, matching EF Core's collation-delegated behavior:

-- Before (CaseInsensitive ignored):
WHERE [Name] = 'John'

-- After (CaseInsensitive == true):
WHERE LOWER([Name]) = 'john'

Affects all comparison operators (=, !=, >, <, >=, <=, Contains, StartsWith, EndsWith). Threaded through SqlTranslator, SqlJoinBuilder, and SqlExistsTranslator.

4. Dapper Filtered Includes Hydration

Affected packages: FlexQuery.NET.Dapper

Before: FilteredIncludes (include paths with inline filter criteria parsed from query parameters) were silently ignored by the Dapper provider. Only flat Includes were hydrated.

After: DapperRowHydrator.HydrateFilteredIncludes<T>() extracts include paths from the IncludeNode tree and populates child entities with filtered data. FlexQueryDapperExtensions now checks both Includes and FilteredIncludes when determining whether to hydrate.

5. SelectTreeBuilder Root Scalars for FilteredIncludes

Affected packages: FlexQuery.NET

Before: When FilteredIncludes was present without Select or SelectTree, the root entity's scalar properties were not marked for materialization, causing empty root rows in Dapper results.

After: SelectTreeBuilder.Build() now calls root.MarkIncludeAllScalars() after processing FilteredIncludes, matching the existing behavior for Includes.

6. Validation Rules for Grouped Queries

Affected packages: FlexQuery.NET

Four new validation rules enforce correct usage of grouped-query constructs:

Rule Error Code Rejects
HavingWithoutGroupByRule HavingWithoutGroupBy Having filters without GroupBy or aggregates
HavingAliasIntegrityRule HavingAliasMismatch Having references to non-declared aggregate aliases
GroupByIncludeConflictRule GroupByIncludeConflict GroupBy combined with Include/Expand
ExpandPathValidationRule IncludePathNotFound Include/Expand paths that don't exist on the entity type (recursive validation)

All rules registered in QueryValidator constructor.


Performance Improvements

1. IsScalarType Cache

Affected packages: FlexQuery.NET

ReflectionCache.IsScalarType() now caches type classification results. Previously, every field in every query re-evaluated whether a Type is scalar via reflection. For projection-heavy queries on entities with many properties, this repeated evaluation caused measurable CPU overhead. Cached via BoundedConcurrentCache<Type, bool>.

2. IncludeBuilder MethodInfo Cache

Affected packages: FlexQuery.NET

IncludeBuilder caches MethodInfo lookups for EF.Property and related expression-building APIs. Previously rebuilt on every include path evaluation.

3. Legacy Projection Engine Removed

Affected packages: FlexQuery.NET

Removed the old ProjectionEngineTests.cs (104 lines) and related dead code from ProjectionOptimizer, ExpressionPrinter, and QueryResultExtensions. The modern ProjectionMetadata/ProjectionMetadataBuilder architecture handles all projection scenarios.


Maintenance

  • XML documentation comments: Added descriptive XML docs to 4 new validation rules and resolved 125+ code analysis warnings across Core, EF Core, and Dapper projects.
  • ProjectionExecutionPlan model: New model class separating the projection plan from execution logic for cleaner separation of concerns.
  • SqlFormatter and SqlParameterExtractor: New utility classes in EF Core for SQL formatting and parameter introspection (used by diagnostics).
  • Query diagnostics enhancements: FlexQueryDiagnosticsCollector improved with TimelineEntry tracking, DiagnosticsDuration measurements, and ConsoleExecutionListener.
  • ProjectedField model: Extended with DbType and Direction support for richer query parameter metadata.
  • Azure deployment workflow: Automated build and deployment to Azure App Service.
  • GovernanceValidator startup validation: Added ValidateConfiguration() for detecting BlockedFieldsAllowedFields overlap at startup.

Breaking Changes

None. v3.1.1 is fully backward-compatible with v3.1.0.


Behavioral Changes

Change Impact Mitigation
EF Core pipeline: Count before Sort Grouped COUNT queries on SQLite no longer fail with decimal type errors None (fix)
EF Core pipeline: Sort only in non-grouped path Pre-GroupBy ORDER BY no longer emitted for grouped queries None (fix)
Dapper CaseInsensitive now honored String comparisons may produce different SQL with LOWER() wrapping Set CaseInsensitive = false if exact case SQL is required
Dapper FilteredIncludes now hydrated Previously ignored filtered includes now populate child entities None (fix)
Auto-generated ORDER BY for paging Queries with paging but no sort now produce deterministic results None (fix)
IsScalarType caching Slightly different memory profile (cache entries vs recomputation) None (internal)

New Test Coverage

Test Area Verifies
SqlTranslatorRegressionTests.CaseInsensitive_* Dapper Case-insensitive SQL generation with LOWER()
DialectTests.* (3 updated) Dapper Assertions updated for CaseInsensitive defaults
SqlFormatterTests (103 lines) EF Core SQL formatting utility
SqlParameterExtractorTests (82 lines) EF Core Parameter extraction utility
CacheKeyCorrectnessTests (163 new lines) Core Cache key stability across query shapes

Updated test assertions across the suite. 888 total tests pass (up from 874 in v3.1.0).


Upgrading

dotnet add package FlexQuery.NET --version 3.1.1
dotnet add package FlexQuery.NET.Dapper --version 3.1.1
dotnet add package FlexQuery.NET.EntityFrameworkCore --version 3.1.1
dotnet add package FlexQuery.NET.Parsers.Jql --version 3.1.1
dotnet add package FlexQuery.NET.Parsers.MiniOData --version 3.1.1
dotnet add package FlexQuery.NET.Adapters.AgGrid --version 3.1.1
dotnet add package FlexQuery.NET.Adapters.Kendo --version 3.1.1
dotnet add package FlexQuery.NET.AspNetCore --version 3.1.1

No code changes required for existing v3.1.0 users.

v3.1.0

Choose a tag to compare

@github-actions github-actions released this 26 Jun 00:28

FlexQuery.NET v3.1.0 Release Notes

Release date: 2026-06-25

Overview

v3.1.0 introduces query execution observability, AG Grid camelCase support, HAVING query pipeline fixes, a shared projection metadata layer unifying Dapper and EF Core serialization behavior, DynamicType materialization for GroupBy/aggregate results, and a new diagnostics API. The Dapper return type is unified with EF Core (QueryResult<object>).


What's New

1. Execution Diagnostics / Observability

New IFlexQueryExecutionListener interface with 4 lifecycle events that let you observe the full query pipeline:

Event When Data
QueryParsed After parsing input parameters QueryOptions, elapsed time
QueryTranslated After SQL generation SQL string, parameters
QueryExecuted After database query returns rows Row count, optional exception
QueryMaterialized After QueryResult<object> is built Final result, optional exception

Both Dapper and EF Core providers now accept an optional configureExecution callback on all overloads:

var result = await connection.FlexQueryAsync<Customer>(parameters, opts,
    exec => exec.Listener = new MyTelemetryListener());

New types:

  • FlexQueryExecutionConfig — config class with Listener property
  • FlexQueryExecutionContext — internal context carrying QueryId, Stopwatch, listener, and CancellationToken
  • FlexQueryExecutionEvent base class with event data types QueryParsedEvent, QueryTranslatedEvent, QueryExecutedEvent, QueryMaterializedEvent
  • IFlexQueryExecutionListener — 4 async methods for lifecycle hooks

2. AG Grid SSRM camelCase Support

AgGridResponseConverter.Convert() and ToAgGridServerSideResponse() extension methods accept a new camelCase parameter. When true, POCO property names in row data dictionaries are automatically converted from PascalCase to camelCase. Group metadata field names remain configurable via AgGridResponseFieldOptions.

3. DynamicType for GroupBy / Aggregate Results

GroupBy and aggregate queries now produce DynamicType instances instead of Dictionary<string, object>. DynamicType properties flow through ASP.NET Core's PropertyNamingPolicy, so camelCase serialization works automatically without manual conversion.

4. Shared Projection Metadata Layer

ProjectionMetadataBuilder extracted as a shared component used by both Dapper and EF Core providers. Reduces duplication and ensures consistent projection resolution (nested paths, field types, IEnumerable detection) across both pipeline implementations.

5. Sample Web API Project

New FlexQuery.NET.Samples.WebApi demonstrating EF Core, Dapper, AG Grid SSRM, and Kendo UI integrations with SQLite, Swagger, and demo frontends.

6. Benchmarks

  • DapperSqlGenerationBenchmarks — simple, complex-filter, and aggregate SQL generation against SqlServer dialect
  • ProjectionBenchmarksSelectTreeBuilder, DynamicTypeBuilder cache hit/miss, QueryCacheKeyBuilder performance

What's Fixed

1. HAVING Pipeline — Parser, SQL Generation, Alias Naming

Three independent bugs that caused HAVING clauses to be silently dropped or return incorrect results:

a) HavingParser regex regression:
The field portion of the HAVING expression was non-optional. Input like count:gt:20 would misinterpret gt as a field name and return null because no field was found before the colon.

// Before: field-less aggregate HAVING parsed as null
var having = parser.Parse("count:gt:20"); // null

// After:
var having = parser.Parse("count:gt:20"); // { Field: null, Aggregate: Count, Operator: GreaterThan, Value: 20 }

b) Dapper SqlTranslator.BuildHavingClause:
Field-less aggregates (COUNT(*)) emitted quoted [*] columns (COUNT([*])) and bound comparison values as strings ('20' instead of 20). SQLite rejected INTEGER > TEXT comparisons, returning zero rows.

-- Before (broken for SQLite):
HAVING COUNT([*]) > '20'

-- After:
HAVING COUNT(*) > @p0  -- @p0 = 20 (int)

c) BuildAggregateAlias:
Field-less aggregates produced allCount as the alias instead of the predictable Count.

-- Before:
SELECT COUNT(*) AS [allCount]
-- After:
SELECT COUNT(*) AS [Count]

2. Dapper TotalCount / ResultCount Alignment with EF Core

Before: ResultCount was unconditionally set to items.Count (the page size), even when IncludeTotalCount was false.

After: ResultCount is only set when counts are explicitly enabled. Checks both options.IncludeCount (user-level) and execOptions.IncludeTotalCount (server-level). Both providers now return identical QueryResult structure for all scenarios.

3. Dapper Connection Lifecycle

Documented that FlexQueryAsync<T>() auto-opens the connection if closed but never auto-closes it. Added CancellationToken parameter to ExecuteQueryAsync.


Breaking Changes

1. Dapper Return Type: QueryResult<T>QueryResult<object>

Affected package: FlexQuery.NET.Dapper

Before: FlexQueryAsync<T>() returned Task<QueryResult<T>>.
After: Returns Task<QueryResult<object>>.

This unifies the Dapper provider's return type with the EF Core provider. Required because Dapper now uses DynamicType for projection materialization, and the projected type is not known at compile time.

Migration:

// Before (will not compile):
QueryResult<Customer> result = await connection.FlexQueryAsync<Customer>(...);

// After:
QueryResult<object> result = await connection.FlexQueryAsync<Customer>(...);
// OR:
var result = await connection.FlexQueryAsync<Customer>(...);

2. DebugResult Namespace Change

Affected package: FlexQuery.NET

DebugResult moved from inline class in FlexQuery.NET.Extensions (inside FlexQueryDebugExtensions.cs) to FlexQuery.NET.Models.

Migration: Add using FlexQuery.NET.Models; where DebugResult is referenced.


Behavioral Changes

Change Impact Mitigation
Dapper return type → QueryResult<object> Compile break on explicit return type annotations Use var or change to QueryResult<object>
DebugResultFlexQuery.NET.Models Compile break if relying on FlexQuery.NET.Extensions Add using FlexQuery.NET.Models;
Dapper ResultCount null when count is off Matches EF Core; previously returned items.Count Add null check if needed
Field-less HAVING Count alias Previously allCount Update any HAVING filter referencing allCount
GroupBy/aggregate rows → DynamicType Property names respect JSON naming policy; no longer Dictionary<string, object> Access via properties instead of dictionary keys, or use reflection/DynamicType API

New Test Coverage

HAVING Pipeline (19 new tests)

Test Area Verifies
Parse_Having_FieldLessAggregate Parser count:gt:10 parses with null field
Parse_Having_ParenthesizedValue Parser (count:gt:10) parenthesized syntax
Parse_Having_ColonSeparatedField Parser Field-based HAVING with colons
Parse_Having_Count Parser count aggregate parsed
Parse_Having_Sum Parser sum aggregate parsed
Parse_Having_Avg Parser avg aggregate parsed
Parse_Having_Max Parser max aggregate parsed
Parse_Having_Min Parser min aggregate parsed
BuildHavingClause_CountStar_NoField SQL generation COUNT(*) > @p0 without field
BuildHavingClause_SumField SQL generation SUM([Amount]) > @p0 with field
BuildHavingClause_AvgField SQL generation AVG([Amount]) > @p0 with field
BuildHavingClause_MaxField SQL generation MAX([Amount]) > @p0 with field
BuildHavingClause_MinField SQL generation MIN([Amount]) > @p0 with field
BuildAggregateAlias_FieldLessAggregate_ReturnsCount Alias Field-less → Count
BuildAggregateAlias_FieldAggregate_ReturnsFieldFunction Alias Field + function → AmountSum
Having_Count_Grouped_Sorted_Paged Integration Full pipeline: Count HAVING + sort + paging
Having_Sum_Grouped_Sorted_Paged Integration Full pipeline: Sum HAVING + sort + paging
Having_Avg_Grouped_Projected Integration HAVING with projection
Having_Max_Min_Grouped_Projected Integration Multiple aggregates HAVING with projection

GroupBy / Integration (204 new test lines)

Additional GroupBy execution tests across EF Core and Dapper covering dict vs DynamicType materialization, grouping with sort/paging combinations, and aggregate result structures.

Parser (117 new test lines)

Additional parser coverage for edge cases and boundary conditions.

AgGridResponseConverter

Adapted existing tests to cover the new camelCase parameter (both true and false paths).


Additional Changes (post-commit)

  • ToQueryOptions extension method: New FlexQueryParametersExtensions.ToQueryOptions() convenience extension on FlexQueryParameters for direct-to-QueryOptions parsing without calling QueryOptionsParser.Parse().
  • Sample controllers simplified: DapperCustomersController and EfCustomersController migrated to primary constructors and simplified to use the full FlexQueryAsync API instead of manual GroupBy handling.
  • MiniOData assembly name fix: QueryOptionsParser registration corrected from FlexQuery.NET.MiniOData to FlexQuery.NET.Parsers.MiniOData.
  • Internal call sites updated: FlexQueryDapperExtensions, ProjectionEfCoreExtensions, QueryableEfCoreExtensions, and QueryableExtensions all updated to use parameters.ToQueryOptions().

Migration Guide

Dapper Return Type

Replace explicit QueryResult<T> with QueryResult<object> or use var:

// Before:
QueryResult<Customer> result = await connection.FlexQueryAsync<Customer>(......
Read more

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

v3.0.5

Choose a tag to compare

@github-actions github-actions released this 24 Jun 04:19

FlexQuery.NET v3.0.5 Release Notes

Release date: 2026-06-24

Overview

v3.0.5 enables expression caching by default, adds ReflectionCache for centralized property metadata caching, hardens DynamicTypeBuilder with bounded FIFO eviction, fixes an EF Core operator handler cache poisoning bug, and expands regression test coverage across all caching layers.


What's New

1. Expression Caching Enabled by Default

FlexQueryCacheSettings.EnableCache is now true by default (was false). Expression trees for predicates and projections are cached after the first build for each unique query shape. Cache keys are deterministic and cover all query dimensions (filter, sort, select, group-by, aggregates, includes, paging). Bounded at 2000 entries per cache instance with FIFO eviction.

Caching is automatically disabled when ExpressionMappings (custom field→expression mappings) are present.

To disable:

FlexQueryCacheSettings.EnableCache = false;

2. EF Core Operator Cache Poisoning Fix

UseEfCoreOperators() now sets a marker in options.Items["__EfCoreOperators"] that is included in the expression cache key. Previously, the OperatorHandlerRegistry global static state was not part of the cache key — identical QueryOptions with different operator handler registrations would share a cached expression, returning wrong SQL. Now EF Core operator mode and default operator mode produce distinct cache keys.

3. ReflectionCache

New FlexQuery.NET.Caching.ReflectionCache centralizes all property metadata lookups across the validation, projection, wildcard expansion, and grouping pipelines. Four cache methods backed by ConcurrentDictionary:

  • GetProperty(Type, string) — case-insensitive, per (type, name)
  • GetProperties(Type) — per type
  • TryResolvePropertyChain(Type, string, out chain) — dot-path resolution with collection traversal
  • TryGetCollectionElementType(Type, out elementType)IEnumerable<T> interface scan

Refactored call sites (14 files): SafePropertyResolver, ProjectionBuilder, DefaultProjectionRule, FieldAccessValidator, AggregateResultBuilder, ProjectionOptimizer, QueryBuilder, FlatProjectionBuilder (6 sites), TypeHelper.

4. DynamicTypeBuilder Cache Hardening

Added bounded FIFO eviction via FlexQueryCacheSettings.MaxCacheSize. Cache key changed from Type.GetHashCode() (unstable) to Type.FullName (stable). Added Clear() and Count API. Added ArgumentNullException guard on null input.

5. Cache Key Correctness Tests

3 new tests in CacheKeyCorrectnessTests:

Test Verifies
CanCache_WithExpressionMappings_ReturnsFalse ExpressionMappings disables caching
CacheKey_IdenticalQuery_DifferentEntityType_DifferentKey Entity type is in the cache key
SortOrder_DifferentFieldOrder_DifferentKey Sort order is preserved in the key

6. New Test Coverage

Area Tests Focus
ReflectionCacheTests 35 GetProperty, GetProperties, chain resolution, collection detection, thread safety, cache-miss consistency, path edge cases
DynamicTypeBuilderTests 14 Shape generation, FIFO eviction, thread safety, null guard, cache reuse
CacheKeyCorrectnessTests 8 Filter/select/sort/entity-type differentiation, ExpressionMappings gate
CacheIsolationTests 3 ParserCache deep-clone isolation

Migration Guide

Expression Caching

No code changes required. Caching is transparent — cache keys are deterministic and the cache is bounded. If memory pressure is a concern:

FlexQueryCacheSettings.EnableCache = false;  // Disable globally
// Or per-query:
options.EnableCache = false;  // Disable for a single query

ReflectionCache

No code changes required. All existing code paths now delegate to ReflectionCache internally. The public API is unchanged.

DynamicTypeBuilder

No code changes required. The cache is now bounded — if you observe cache eviction of projection types, increase FlexQueryCacheSettings.MaxCacheSize (default 2000).


Upgrading

dotnet add package FlexQuery.NET --version 3.0.5
dotnet add package FlexQuery.NET.Adapters.AgGrid --version 3.0.5
dotnet add package FlexQuery.NET.Dapper --version 3.0.5

Full test suite: 827 tests passing, zero regressions.

v3.0.4

Choose a tag to compare

@github-actions github-actions released this 23 Jun 19:32

FlexQuery.NET v3.0.4 Release Notes

Release date: 2026-06-24

Overview

v3.0.4 closes the default projection governance gap, fixes a SelectTree governance bypass vulnerability, adds unified grouped sort validation across all providers, and provides a governance test coverage audit.

Governance: When no explicit Select is specified, the system now injects a governed default projection instead of returning all entity fields. Introduces DefaultProjectionRule, wildcard expansion at injection time, GovernanceValidator startup checks, and role-based auto-projection. Critical fix: SelectTree projection paths are now recursively validated against all governance rules, closing a bypass that allowed blocked fields through tree-based query entry points.

Grouped Sort Validation: All providers now share a centralized GroupedSortValidator that ensures grouped queries (GroupBy + Aggregates + Sort + Paging) produce valid SQL with deterministic ordering — removing invalid sorts, resolving aggregate field names to aliases, and injecting group-key fallbacks.

Critical Security Fix — SelectTree Governance Bypass: FieldAccessValidator previously validated only options.Select (the flat field list). The options.SelectTree projection path — used by JsonParser.Parse, all adapter parsers (AG Grid, Kendo, OData), and the JQL/DSL parsers — was never inspected. Blocked fields (SSN, Orders.Total) could survive validation when projected through SelectTree, effectively bypassing all governance rules: BlockedFields, AllowedFields, SelectableFields, RoleAllowedFields, FieldAccessResolver, and MaxFieldDepth.


What's New

1. Default Projection Injection

Problem: When clients sent a query without $select (or equivalent), the projection builder returned all entity fields regardless of governance configuration. This meant AllowedFields, BlockedFields, and RoleAllowedFields were effectively ignored for unprojected queries, leaking sensitive fields by default.

Fix: A new DefaultProjectionRule runs first in the validation pipeline. When no explicit projection (Select, SelectTree, or HasProjection()) is present, it injects a default Select using the following priority:

Priority Source Example
1 SelectableFields { "Id", "Name", "Email" }
2 RoleAllowedFields (current role) Admin → { "Id", "Name", "Salary" }
3 AllowedFields { "Id", "Name", "Email" }
4 Entity metadata minus BlockedFields { "Id", "Name", ... all scalar props }

If none of the above sources are configured, the rule skips injection (preserving the existing behavior of selecting all fields).

Effect: Unprojected queries now automatically respect field governance — blocked fields are excluded, allowed fields are whitelisted, and role-based restrictions take effect by default.

2. Wildcard Pattern Expansion

DefaultProjectionHelper.ExpandWildcardFields expands wildcard patterns like Orders.* at injection time by recursively walking navigation properties on the target entity type via reflection.

Example:

SelectableFields = new HashSet<string> { "Id", "Name", "Orders.*" }

Expands to something like:

{ "Id", "Name", "Orders.OrderId", "Orders.Total", "Orders.Status", "Orders.CreatedAt" }

This ensures wildcard-based governance rules are resolved eagerly, before the projection builder receives the field list.

3. Non-Strict Re-Apply of Default Projection

In non-strict mode (StrictFieldValidation = false), if an explicit Select is provided but every field is removed by validation (e.g., the user selected fields not in AllowedFields), the default projection is re-injected automatically.

Before: An empty Select after non-strict validation resulted in no projection.
After: The system falls back to the governed default projection, ensuring the response is never empty or broken.

4. Role-Based Auto-Projection

RoleAllowedFields now serves as a valid source for the default projection. When no SelectableFields are configured but RoleAllowedFields contains entries for the CurrentRole, those fields are used as the default projection.

opts.CurrentRole = "manager";
opts.RoleAllowedFields = new Dictionary<string, HashSet<string>>
{
    ["admin"] = new() { "Id", "Name", "Email", "Salary", "InternalNotes" },
    ["manager"] = new() { "Id", "Name", "Email", "Department" },
    ["user"] = new() { "Id", "Name", "Email" }
};

// When no Select is specified, manager inherits: { "Id", "Name", "Email", "Department" }

5. Paging Fallback Sort Respects Governance

QueryBuilder.ApplyPaging now uses the first field from options.Select as the fallback sort key (before falling back to Id, then Key, then the first scalar property). This ensures deterministic pagination respects the governed projection.

Before: Fallback was always Id → first property, regardless of projection.
After: Fallback prefers the first selected field, which is governed by the default projection injection.

6. Governance Configuration Validation

New GovernanceValidator.ValidateConfiguration() enables startup-time validation of governance config consistency:

Check Validates
BlockedFieldsAllowedFields No field can be both blocked and allowed
SelectableFieldsAllowedFields Selectable fields must be a subset of allowed fields
FilterableFieldsAllowedFields Filterable fields must be a subset of allowed fields
SortableFieldsAllowedFields Sortable fields must be a subset of allowed fields
GroupableFieldsAllowedFields Groupable fields must be a subset of allowed fields
AggregatableFieldsAllowedFields Aggregatable fields must be a subset of allowed fields

Call at application startup:

GovernanceValidator.ValidateConfiguration(execOptions);

Throws InvalidOperationException with a descriptive message when configuration is inconsistent.

7. New Test Coverage

  • 24 new unit tests in FieldSecurityTests.cs covering:
    • DefaultProjectionRule priority (SelectableFields > RoleAllowedFields > AllowedFields > fallback)
    • DefaultProjectionRule skips when explicit Select is provided
    • DefaultProjectionRule skips when no governance is configured
    • DefaultProjectionRule excludes BlockedFields
    • RoleAllowedFields as default projection source
    • RoleAllowedFields with AllowedFields intersection
    • DefaultProjectionRule with grouped queries (skips injection)
    • Wildcard expansion via ExpandWildcardFields
    • Non-strict re-apply after AllowedFields removal
    • Strict mode throws even with no explicit Select
    • GovernanceValidator config validation (7 tests)
  • 7 new SelectTree governance tests in SecurityGovernanceEfCoreIntegrationTests.cs
  • 2 new tests in PagingTests.cs for fallback sort behavior

8. Documentation Updates

  • Security & Governance guide updated with default projection, wildcard expansion, and config validation sections

9. Grouped Sort Validation (Cross-Provider)

Problem: Grouped queries (GroupBy + Aggregates + Sort + Paging) had inconsistent and broken sort behavior across the EF Core and Dapper providers:

Scenario EF Core (before) Dapper (before)
Sort by non-projected field (Id with GroupBy=[Category]) Silently dropped (nondeterministic order) Generated ORDER BY [Id] — column not in GROUP BY, SQL error at runtime
Sort by aggregate source field (Price when AVG(Price) AS priceAvg) Resolved to priceAvg (correct) Generated ORDER BY [Price] — column not in GROUP BY, SQL error
All sorts invalid No ORDER BY — nondeterministic Generated invalid SQL
Paging without sort Injected group-key sort (deterministic) No ORDER BY — nondeterministic paging

Fix: A new GroupedSortValidator in FlexQuery.NET.Core centralizes grouped sort logic and is shared by both providers:

  1. Valid sort fields: Group-key fields and aggregate aliases are kept.
  2. Aggregate field → alias resolution: Sorting by Price when AVG(Price) AS priceAvg exists resolves to priceAvg.
  3. Invalid sorts removed: Fields not in the grouped projection (e.g., Id when grouping by Category) are silently removed.
  4. Fallback injection: If all sorts are invalid or empty, a deterministic fallback by the first group-key ascending is injected.

Provider changes:

  • EF Core: Replaced inline BuildGroupedSorts<TShape> / ResolveGroupedSortField (which depended on reflection over the dynamic TShape type) with a call to GroupedSortValidator.Validate, removing the generic parameter dependency.
  • Dapper: SqlTranslator.Translate now validates sorts through GroupedSortValidator.Validate when the query has GroupBy, preventing invalid SQL and ensuring deterministic paging.

Behavior after fix:

Scenario EF Core Dapper
Sort by group key (Category with GroupBy=[Category]) ORDER BY Category ORDER BY "Category"
Sort by aggregate alias (priceAvg with AVG(Price) AS priceAvg) ORDER BY priceAvg ORDER BY "priceAvg"
Sort by aggregate source field (Price with AVG(Price) AS priceAvg) ORDER BY priceAvg ORDER BY "priceAvg"
Sort by non-projected field (Id with GroupBy=[Category]) Removed, fallback to Category Removed, fallback to "Category"
All sorts invalid Fallback to first group key Fallback to first group key
Paging without sort Injects group-key sort Injects group-key sort

10. Grouped Query Contract Documentation

New architecture document at docs/architecture/grouped-query-contract.md defining the expected behavior for grouped projections across both providers, including:

  • Valid vs invalid sort fields
  • Aggregate field → alias ...
Read more

v3.0.3

Choose a tag to compare

@github-actions github-actions released this 23 Jun 10:06

FlexQuery.NET v3.0.3 Release Notes

Release date: 2026-06-23

Overview

v3.0.3 fixes AG Grid SSRM grouped sort validation by resolving aggregate and group-key sorts against the current grouped projection, preventing invalid SQL in Dapper and non-deterministic pagination in EF Core. Also introduces Server-Side Row Model response support, Dapper grouping and distinct, QueryResult.ResultCount, and the aggregate alias naming convention redesign.


What's New

1. AG Grid SSRM Grouped Sort Validation

Problem: sortModel entries were passed through unchanged to the SQL layer. At grouped levels, aggregate sorts like colId: "price" (with aggFunc: "AVG") generated ORDER BY Price instead of ORDER BY priceAvg. Detail-column sorts like colId: "id" reached GROUP BY queries and broke. Empty sortModels caused SQL Server pagination to crash (OFFSET without ORDER BY).

Fix: AgGridQueryOptionsParser.Parse() now validates and resolves every sortModel entry against the current grouped projection:

Scenario Before After
Aggregate sort (colId: "price", aggFunc: "AVG") ORDER BY [Price] (Dapper: broken SQL) ORDER BY [priceAvg]
Detail sort (colId: "id") ORDER BY [Id] (broken in GROUP BY) Removed silently
All sorts invalid or empty No ORDER BY (pagination crash) Fallback: category ASC
colId != field (id: "avg_price", field: "price") ORDER BY [avg_price] (broken) ORDER BY [priceAvg]
Nested group (groupKeys: ["Electronics"]) Same broken SQL Validated against brand, priceAvg, quantitySum
Ungrouped / leaf level Pass through Pass through (unchanged)

Resolution chain:

sortModel.colId → valueCol/id lookup → BuildAggregateAlias(aggFunc, field) → SortNode.Field
sortModel.colId → rowGroupCol[id] lookup → GetProjectionName(field) → SortNode.Field

Key details:

  • "average" normalized to "avg" in the sort validation path, consistent with the aggregate builder
  • Fallback sort uses the current group field's projection name (via GetProjectionName)
  • Only the current group column (at rowGroupCols[groupKeys.Count]) is valid — parent group keys are not in the projection
  • colId matching uses ordinal comparison (column IDs are case-sensitive in AG Grid)
  • Zero changes to the EF Core pipeline, Dapper translator, QueryBuilder, or GroupByBuilder

New column models:

  • AgGridGroupColumn.Id and AgGridValueColumn.Id added to capture the AG Grid column identifier, enabling correct resolution when colId != field

2. AgGrid SSRM Response Support

  • New ToAgGridServerSideResponse() extension method: Converts QueryResult directly to AgGrid Server-Side Row Model compatible response!
  • New AgGridResponseConverter: Handles both group rows and leaf rows for SSRM!
  • New models: AgGridGroupRow, AgGridLeafRow, AgGridResponseFieldOptions, AgGridServerSideResponse!
  • AgGridRequest improvement: Added GroupKeys property for handling SSRM grouping levels!
  • Updated ApplyAgGridRequest: Correctly replaces grouping and aggregates for proper SSRM store state!

3. QueryResult Enhancements

  • New ResultCount property: Separate count for grouped/distinct queries vs TotalCount:
    • TotalCount: Total source records (before grouping/distinct)
    • ResultCount: Rows produced by final query (after grouping/distinct)

4. Dapper Grouping & Distinct Support

  • Added full GROUP BY and DISTINCT support to Dapper provider!
  • Added TranslateSourceCount() to ISqlTranslator for source record count!
  • Added ExtractCountSql() helper to get count of final shaped results!
  • Enhanced ExecuteQueryAsync() to calculate both TotalCount and ResultCount!

5. Aggregate Alias Convention Redesign

Aggregate aliases now use a field-first, camelCase format instead of FUNCTION_Field.

Syntax Before After
sum(Total) SUM_Total totalSum
count(Id) COUNT_Id idCount
avg(Price) AVG_Price priceAvg
min(Total) MIN_Total totalMin
max(Total) MAX_Total totalMax
count() COUNT_All allCount
avg(Order.Total) AVG_Order_Total orderTotalAvg

Why?

  • Improves JSON serialization compatibility.
  • Avoids serializer-generated names such as suM_Total.
  • Provides more natural JavaScript and TypeScript property names.
  • Improves integration with AG Grid, PrimeVue, React, and other frontend data grids.
  • Establishes a consistent long-term aggregate naming convention.

Affected areas

If you reference aggregate aliases directly, update:

  • Aggregate sorting fields
  • HAVING expressions
  • AG Grid column bindings
  • Frontend property access
  • Custom projections and integration tests

Example:

Before:

{
  "SUM_Total": 1500,
  "COUNT_Id": 12
}

After:

{
  "totalSum": 1500,
  "idCount": 12
}

Benefits:

  • Better JSON serialization — no awkward transformer artifacts (SUm_Quantity)

  • Natural JavaScript/TypeScript property names

  • Lexical grouping of related aggregates (priceAvg, priceMin, priceSum)

  • Cleaner API contracts

  • Consistent AG Grid / PrimeVue / React data binding

  • All built-in adapters (AG Grid, Kendo) generate the new format automatically.

  • The BuildAggregateAlias() utility method in ParserUtilities has been updated — see the migration section below.

5. New Test Coverage

  • Added AgGridResponseConverterTests
  • Added ResultCountTests
  • Added SqlTranslatorGroupedTests for Dapper grouped queries
  • Added GroupedQueryExecutionTests for Dapper API grouped queries

6. Documentation Updates

  • Updated AgGrid adapter docs with SSRM features and new alias convention
  • Updated migration guide (v2-to-v3) with aggregate alias migration steps
  • Updated grouping, projection, and examples docs with new alias format
  • Updated Dapper provider docs (ef-core.md, sql-generation.md) with new aliases

Migration Guide

Aggregate Alias Migration

If you programmatically construct AggregateModel instances with explicit Alias values, update them to the new format:

// Before
options.Aggregates.Add(new AggregateModel
{
    Field = "Total",
    Function = "sum",
    Alias = "SUM_Total"          // ← old format
});

// After
options.Aggregates.Add(new AggregateModel
{
    Field = "Total",
    Function = "sum",
    Alias = "totalSum"           // ← new format
});

If you use the built-in parsers (HTTP query parameters, AG Grid adapter, Kendo adapter), the aliases are generated automatically — no code changes needed.

Sort by aggregate alias:

// Before
options.Sort = [new SortNode { Field = "SUM_Total", Descending = true }];

// After
options.Sort = [new SortNode { Field = "totalSum", Descending = true }];

HAVING clauses do not require changes to the HavingCondition itself (it uses function + field, not the alias). The alias is resolved internally by BuildAggregateAlias().

JSON response diff:

// Before
{ "CustomerId": 1, "SUM_Total": 1250.00, "COUNT_Id": 3 }

// After
{ "CustomerId": 1, "totalSum": 1250.00, "idCount": 3 }

SQL diff (Dapper):

-- Before
SELECT SUM("Total") AS "SUM_Total" FROM "Orders" GROUP BY "CustomerId"

-- After
SELECT SUM("Total") AS "totalSum" FROM "Orders" GROUP BY "CustomerId"

Using new AgGrid SSRM response

[HttpPost("grid")]
public async Task<IActionResult> GetGridData([FromBody] AgGridRequest request)
{
    var options = request.ToQueryOptions();
    var result = await _context.Products.FlexQueryAsync<Product>(options);
    var agGridResponse = result.ToAgGridServerSideResponse(request);
    return Ok(agGridResponse);
}

Using QueryResult.ResultCount

var result = await dbContext.Products.FlexQueryAsync<Product>(options);
// Total records in source table (after filters): result.TotalCount
// Number of groups/rows after grouping/distinct: result.ResultCount

Upgrading

Update all relevant packages to v3.0.3:

dotnet add package FlexQuery.NET --version 3.0.3
dotnet add package FlexQuery.NET.Adapters.AgGrid --version 3.0.3
dotnet add package FlexQuery.NET.Dapper --version 3.0.3

If you have hardcoded aggregate alias strings anywhere in your application code (sort fields, alias overrides, response parsing), update them to the new field-first camelCase format. No other migration steps are required.

v3.0.2

Choose a tag to compare

@github-actions github-actions released this 22 Jun 07:27

FlexQuery.NET v3.0.2 Release Notes

Release date: 2026-06-22

Overview

v3.0.2 streamlines the AG Grid integration with a simplified, extension-method-based API and improves documentation for the adapter package. This release contains no breaking changes.

What's New

Simplified AG Grid API

The AG Grid adapter now provides extension methods on AgGridRequest and QueryOptions, removing the need to reference AgGridQueryOptionsParser directly.

Before (v3.0.1):

using FlexQuery.NET.Adapters.AgGrid.Parsers;

[HttpPost("grid")]
public async Task<IActionResult> GetGridData([FromBody] JsonElement payload)
{
    var options = AgGridQueryOptionsParser.Parse(payload);
    var result = await _context.Users.FlexQueryAsync<User>(options);
    return Ok(result);
}

After (v3.0.2):

[HttpPost("grid")]
public async Task<IActionResult> GetGridData([FromBody] AgGridRequest request)
{
    var options = request.ToQueryOptions();
    var result = await _context.Users.FlexQueryAsync<User>(options);
    return Ok(result);
}

New Extension Methods

Method Description
AgGridRequest.ToQueryOptions() Converts an AG Grid request into QueryOptions
QueryOptions.ApplyAgGridRequest(AgGridRequest) Merges AG Grid filters, sorts, paging, grouping, and aggregates into an existing QueryOptions instance
string.FromAgGridJson() Parses a raw JSON string into QueryOptions

Other Changes

  • Documentation: README and AG Grid adapter guide updated to reflect the new simplified API.

Upgrading

Update the FlexQuery.NET.Adapters.AgGrid package to v3.0.2:

dotnet add package FlexQuery.NET.Adapters.AgGrid --version 3.0.2

No code migration is required — all existing AgGridQueryOptionsParser.Parse() calls continue to work. The new extension methods are optional.

v3.0.1

Choose a tag to compare

@github-actions github-actions released this 22 Jun 06:39

FlexQuery.NET 3.0.1

Release date: 2026-06-22

Overview

3.0.1 is a cleanup release that renames packages for consistency, extracts the JQL parser into its own package, removes deprecated APIs, and tightens include filter validation. Although it contains breaking changes, the version is kept at 3.0.1 because the 3.0.0 release had near-zero adoption.


Breaking Changes

JQL parser removed from FlexQuery.NET Core

JQL support has been extracted into the dedicated FlexQuery.NET.Parsers.Jql package.

  • Install FlexQuery.NET.Parsers.Jql to continue using JQL filter expressions.
  • JqlQueryParser is no longer available from the Core package.
  • QuerySyntax.Jql has been removed.

FlexQuery.NET.AgGridFlexQuery.NET.Adapters.AgGrid

Package renamed for consistent adapter naming. Update NuGet package references, namespaces, and using directives.

FlexQuery.NET.KendoFlexQuery.NET.Adapters.Kendo

Package renamed for consistent adapter naming. Update NuGet package references, namespaces, and using directives.

FlexQuery.NET.EFCoreFlexQuery.NET.EntityFrameworkCore

Package and namespace renamed for clarity. Update NuGet package references and using directives.

JQL fallback removed from FilteredIncludeParser

Inline include filters no longer support JQL-style expressions. The following syntax is no longer supported:

orders(Status = 'Cancelled')

Use FlexQuery DSL syntax instead:

orders(Status:eq:Cancelled)

Unsupported JQL syntax now throws InvalidOperationException.

Removed deprecated APIs

  • QueryRequest
  • FlexQueryRequest
  • ApplyValidatedQueryOptions
  • ToQueryResultAsync
  • ToProjectedQueryResultAsync
  • Other APIs previously marked [Obsolete]

JqlParser.Parse(string query)JqlParser.Parse(string filter)

Parameter renamed to align with DSL and MiniOData terminology. Callers using named arguments must update their code.


What's New

  • FlexQuery.NET.Parsers.Jql package — JQL filter parser extracted from Core into a dedicated install-on-demand package.
  • FilteredIncludeParser now throws InvalidOperationException with a descriptive migration message instead of silently returning null when encountering deprecated JQL-style include filters.

Migration Guide

Package renames

Old Package New Package
FlexQuery.NET.AgGrid FlexQuery.NET.Adapters.AgGrid
FlexQuery.NET.Kendo FlexQuery.NET.Adapters.Kendo
FlexQuery.NET.EFCore FlexQuery.NET.EntityFrameworkCore

Update all project references and using directives to use the new package names.

JQL

If you use JQL filter expressions (query= parameter), add a reference to FlexQuery.NET.Parsers.Jql and replace JqlQueryParser with JqlParser.

Inline include filters

Replace JQL-style inline include filters:

// Before (removed)
orders(Status = 'Cancelled')

// After
orders(Status:eq:Cancelled)

Deprecated APIs

Replace any usage of the removed APIs with the modern equivalents:

  • QueryRequest / FlexQueryRequestFlexQueryParameters
  • ApplyValidatedQueryOptions → Use the unified FlexQuery pipeline
  • ToQueryResultAsync / ToProjectedQueryResultAsync → Use the unified FlexQuery pipeline

v3.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Jun 11:20

FlexQuery.NET v3.0.0 Release Notes

FlexQuery.NET v3.0.0 is a major release that introduces a modular, provider-agnostic architecture along with new first-party integrations for Dapper, AG Grid, and MiniOData.

This release focuses on decoupling the query engine from Entity Framework, improving extensibility, strengthening validation, and improving runtime performance through caching and parser optimizations.

Why v3.0?

Version 3.0 lays the foundation for a modular FlexQuery ecosystem.

Applications can now choose only the integrations they need while sharing a common query model across:

  • Entity Framework Core
  • Dapper
  • Raw SQL providers
  • AG Grid
  • OData-style clients

This reduces coupling and enables FlexQuery to support a broader range of application architectures.

Highlights

  • New Dapper and Raw SQL support through FlexQuery.NET.Dapper
  • Provider-agnostic query execution architecture
  • New AG Grid and MiniOData integrations
  • Improved caching and parser performance
  • Configurable validation behavior

📦 New Packages & Features

FlexQuery.NET.Dapper

A new SQL translation engine that enables FlexQuery to execute on Dapper or raw ADO.NET.

  • Dialect Translation: Introduces SqlTranslator with translation logic for SQL Server, SQLite, MySQL, and PostgreSQL.
  • Flat Projections: The FlatProjectionBuilder maps nested request structures to flat LEFT JOIN queries, supporting hierarchical row hydration.
  • Aggregations: The TranslateAggregates method maps query aggregations (count, sum, min, max, average) to corresponding SQL aggregation functions.
  • Parameterization: SQL generation uses SqlParameterContext to bind parameters, avoiding inline values.

FlexQuery.NET.AgGrid

A dedicated adapter that translates AG Grid server-side requests into FlexQuery query models.

Supported capabilities include:

  • Text, number, date, and set filters
  • Multi-condition AND/OR filter groups
  • Multi-column sorting
  • Pagination translation
  • Row grouping
  • Aggregate mapping through ValueCols
  • JSON payload parsing

This allows AG Grid applications to reuse FlexQuery's filtering, sorting, grouping, and aggregation pipeline without custom request translation code.

FlexQuery.NET.MiniOData

An optional compatibility layer for applications that expose OData-style query parameters.

Supported query options include:

  • $filter
  • $orderby
  • $select
  • $top
  • $skip
  • $expand
  • $count

Additional capabilities:

  • Nested path translation (address/cityaddress.city)
  • Case-insensitive parameter handling
  • Automatic paging translation from $top and $skip
  • Integration with FlexQuery validation and security rules

This allows existing OData-style clients to integrate with FlexQuery without requiring a full OData implementation.


⚡ Performance & Architecture

Performance Improvements

  • Improved query parsing and projection performance through internal caching optimizations.
  • Reduced reflection overhead during expression generation.
  • Improved cache isolation and memory predictability for long-running applications.
  • Replaced legacy unbounded caching strategies with bounded cache implementations.

Parser & Core Refactoring

The query parsing pipeline was decomposed into specialized parser components to improve maintainability, extensibility, and testability.

Examples include:

  • FilterParser
  • SortParser
  • SelectParser
  • JsonQueryParser

This replaces portions of the previous monolithic parser implementation with focused parser components.

Non-Strict Validation

  • Added StrictFieldValidation to BaseQueryExecutionOptions. Setting this to false instructs the engine to remove unauthorized fields or nested includes from the query instead of throwing a QueryValidationException, allowing execution to continue using only permitted members.

Package Migration

v3 introduces optional packages that can be installed independently.

Example

dotnet add package FlexQuery.NET.Dapper
dotnet add package FlexQuery.NET.AgGrid
dotnet add package FlexQuery.NET.MiniOData

Review your package references and install only the integrations required by your application.

Architecture Changes

Modular Package Ecosystem

FlexQuery.NET has been reorganized into focused packages that can evolve independently while sharing a common query abstraction layer.

Benefits include:

  • Reduced dependencies
  • Smaller deployment footprint
  • Easier integration with non-EF data providers
  • Improved maintainability and extensibility

🛠 Breaking Changes

Target Frameworks

  • Added: net10.0
  • Removed: net7.0 (EOL)
  • Supported: net6.0, net8.0, net10.0

API Deprecations and Removals

  • Request Models: QueryRequest and FlexQueryRequest have been removed after being deprecated in the v2.x release series.
  • FlexQueryParameters remains the supported request model and should be used for all new integrations.
  • AST Restructuring: Legacy parsers (DslParser, JqlParser) and their associated node types have been refactored and relocated to the Ast namespace.
  • Constant Typing: Magic strings used for error codes and operators have been replaced with strongly typed constants (ContextKeys, FilterOperators, QueryOptionKeys, ValidationErrorCodes).

📋 Migration Guide

If you are upgrading from v2.x to v3.0.0, please follow these steps:

  1. Update Target Frameworks: Ensure your consuming projects target .NET 6.0, .NET 8.0, or .NET 10.0.
  2. Migrate Request Models: QueryRequest and FlexQueryRequest were deprecated in v2.x and have been removed in v3.0. Replace any remaining usages with FlexQueryParameters.
  3. Update DI Registrations: FlexQuery no longer registers all adapters by default. You must explicitly install and register the packages you use using their respective extension methods (e.g., services.AddFlexQueryMiniOData()).
  4. Resolve Namespace Changes: If you wrote custom AST manipulations, update your using directives to reference the new FlexQuery.NET.Parsers.Dsl.Ast or FlexQuery.NET.Parsers.Jql.Ast namespaces.
  5. Update Constants: Replace string literals in validation checks or custom operators with the new constant classes (e.g., replace "eq" with FilterOperators.Equal).

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 02 May 03:27

🚀 Release v1.1.0

Query Debug Mode: New ToFlexQueryDebug() extension method to inspect parsed AST and generated Expression Trees.
Expression Printer: Custom visitor to translate internal Expression Trees into readable C#-like syntax.
AST Preservation: QueryOptions now stores the parsed AST from JQL/DSL for debugging.
ToString() overrides for JQL AST nodes for better visibility in debug logs.