Releases: peterjohncasasola/FlexQuery.NET
Release list
v3.1.1
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 beforeORDER BY. Sort does not affect cardinality, and running it before Count caused SQLite decimalORDER BYinsideCOUNTsubqueries to fail with type mismatch errors. - Sort only in non-grouped path: Pre-GroupBy
ORDER BYon raw entity columns is unnecessary for grouped queries (sorting happens internally viaBuildGroupedSorts). 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.
ProjectionExecutionPlanmodel: New model class separating the projection plan from execution logic for cleaner separation of concerns.SqlFormatterandSqlParameterExtractor: New utility classes in EF Core for SQL formatting and parameter introspection (used by diagnostics).- Query diagnostics enhancements:
FlexQueryDiagnosticsCollectorimproved withTimelineEntrytracking,DiagnosticsDurationmeasurements, andConsoleExecutionListener. ProjectedFieldmodel: Extended withDbTypeandDirectionsupport for richer query parameter metadata.- Azure deployment workflow: Automated build and deployment to Azure App Service.
- GovernanceValidator startup validation: Added
ValidateConfiguration()for detectingBlockedFields∩AllowedFieldsoverlap 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.1No code changes required for existing v3.1.0 users.
v3.1.0
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 withListenerpropertyFlexQueryExecutionContext— internal context carryingQueryId,Stopwatch, listener, andCancellationTokenFlexQueryExecutionEventbase class with event data typesQueryParsedEvent,QueryTranslatedEvent,QueryExecutedEvent,QueryMaterializedEventIFlexQueryExecutionListener— 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 dialectProjectionBenchmarks—SelectTreeBuilder,DynamicTypeBuildercache hit/miss,QueryCacheKeyBuilderperformance
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> |
DebugResult → FlexQuery.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 onFlexQueryParametersfor direct-to-QueryOptionsparsing without callingQueryOptionsParser.Parse(). - Sample controllers simplified:
DapperCustomersControllerandEfCustomersControllermigrated to primary constructors and simplified to use the full FlexQueryAsync API instead of manual GroupBy handling. - MiniOData assembly name fix:
QueryOptionsParserregistration corrected fromFlexQuery.NET.MiniODatatoFlexQuery.NET.Parsers.MiniOData. - Internal call sites updated:
FlexQueryDapperExtensions,ProjectionEfCoreExtensions,QueryableEfCoreExtensions, andQueryableExtensionsall updated to useparameters.ToQueryOptions().
Migration Guide
Dapper Return Type
Replace explicit QueryResult<T> with QueryResult<object> or use var:
// Before:
QueryResult<Customer> result = await connection.FlexQueryAsync<Customer>(......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...v3.0.5
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 typeTryResolvePropertyChain(Type, string, out chain)— dot-path resolution with collection traversalTryGetCollectionElementType(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 queryReflectionCache
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.5Full test suite: 827 tests passing, zero regressions.
v3.0.4
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 |
|---|---|
BlockedFields ∩ AllowedFields |
No field can be both blocked and allowed |
SelectableFields ⊆ AllowedFields |
Selectable fields must be a subset of allowed fields |
FilterableFields ⊆ AllowedFields |
Filterable fields must be a subset of allowed fields |
SortableFields ⊆ AllowedFields |
Sortable fields must be a subset of allowed fields |
GroupableFields ⊆ AllowedFields |
Groupable fields must be a subset of allowed fields |
AggregatableFields ⊆ AllowedFields |
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.cscovering:DefaultProjectionRulepriority (SelectableFields > RoleAllowedFields > AllowedFields > fallback)DefaultProjectionRuleskips when explicitSelectis providedDefaultProjectionRuleskips when no governance is configuredDefaultProjectionRuleexcludesBlockedFieldsRoleAllowedFieldsas default projection sourceRoleAllowedFieldswithAllowedFieldsintersectionDefaultProjectionRulewith grouped queries (skips injection)- Wildcard expansion via
ExpandWildcardFields - Non-strict re-apply after AllowedFields removal
- Strict mode throws even with no explicit Select
GovernanceValidatorconfig validation (7 tests)
- 7 new SelectTree governance tests in
SecurityGovernanceEfCoreIntegrationTests.cs - 2 new tests in
PagingTests.csfor 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:
- Valid sort fields: Group-key fields and aggregate aliases are kept.
- Aggregate field → alias resolution: Sorting by
PricewhenAVG(Price) AS priceAvgexists resolves topriceAvg. - Invalid sorts removed: Fields not in the grouped projection (e.g.,
Idwhen grouping byCategory) are silently removed. - 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 toGroupedSortValidator.Validate, removing the generic parameter dependency. - Dapper:
SqlTranslator.Translatenow validates sorts throughGroupedSortValidator.Validatewhen the query hasGroupBy, 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 ...
v3.0.3
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 colIdmatching 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.IdandAgGridValueColumn.Idadded to capture the AG Grid column identifier, enabling correct resolution whencolId != 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! AgGridRequestimprovement: AddedGroupKeysproperty for handling SSRM grouping levels!- Updated
ApplyAgGridRequest: Correctly replaces grouping and aggregates for proper SSRM store state!
3. QueryResult Enhancements
- New
ResultCountproperty: Separate count for grouped/distinct queries vsTotalCount: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()toISqlTranslatorfor 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 inParserUtilitieshas been updated — see the migration section below.
5. New Test Coverage
- Added
AgGridResponseConverterTests - Added
ResultCountTests - Added
SqlTranslatorGroupedTestsfor Dapper grouped queries - Added
GroupedQueryExecutionTestsfor 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.ResultCountUpgrading
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.3If 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
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.2No code migration is required — all existing AgGridQueryOptionsParser.Parse() calls continue to work. The new extension methods are optional.
v3.0.1
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.Jqlto continue using JQL filter expressions. JqlQueryParseris no longer available from the Core package.QuerySyntax.Jqlhas been removed.
FlexQuery.NET.AgGrid → FlexQuery.NET.Adapters.AgGrid
Package renamed for consistent adapter naming. Update NuGet package references, namespaces, and using directives.
FlexQuery.NET.Kendo → FlexQuery.NET.Adapters.Kendo
Package renamed for consistent adapter naming. Update NuGet package references, namespaces, and using directives.
FlexQuery.NET.EFCore → FlexQuery.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
QueryRequestFlexQueryRequestApplyValidatedQueryOptionsToQueryResultAsyncToProjectedQueryResultAsync- 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.
FilteredIncludeParsernow throwsInvalidOperationExceptionwith a descriptive migration message instead of silently returningnullwhen 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/FlexQueryRequest→FlexQueryParametersApplyValidatedQueryOptions→ Use the unifiedFlexQuerypipelineToQueryResultAsync/ToProjectedQueryResultAsync→ Use the unifiedFlexQuerypipeline
v3.0.0
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
SqlTranslatorwith translation logic for SQL Server, SQLite, MySQL, and PostgreSQL. - Flat Projections: The
FlatProjectionBuildermaps nested request structures to flatLEFT JOINqueries, supporting hierarchical row hydration. - Aggregations: The
TranslateAggregatesmethod maps query aggregations (count, sum, min, max, average) to corresponding SQL aggregation functions. - Parameterization: SQL generation uses
SqlParameterContextto 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/city→address.city) - Case-insensitive parameter handling
- Automatic paging translation from
$topand$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
StrictFieldValidationtoBaseQueryExecutionOptions. Setting this tofalseinstructs the engine to remove unauthorized fields or nested includes from the query instead of throwing aQueryValidationException, 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.MiniODataReview 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:
QueryRequestandFlexQueryRequesthave 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 theAstnamespace. - 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:
- Update Target Frameworks: Ensure your consuming projects target .NET 6.0, .NET 8.0, or .NET 10.0.
- Migrate Request Models:
QueryRequestandFlexQueryRequestwere deprecated in v2.x and have been removed in v3.0. Replace any remaining usages withFlexQueryParameters. - 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()). - Resolve Namespace Changes: If you wrote custom AST manipulations, update your
usingdirectives to reference the newFlexQuery.NET.Parsers.Dsl.AstorFlexQuery.NET.Parsers.Jql.Astnamespaces. - Update Constants: Replace string literals in validation checks or custom operators with the new constant classes (e.g., replace
"eq"withFilterOperators.Equal).
v1.1.0
🚀 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.