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>(...);
// After:
var result = await connection.FlexQueryAsync<Customer>(...);HAVING Alias
If you reference the auto-generated alias for field-less aggregate HAVING filters, replace allCount with Count:
// Before:
options.Having.Add(new HavingNode("allCount:gt:10"));
// After:
options.Having.Add(new HavingNode("Count:gt:10"));DebugResult Import
// Add this using:
using FlexQuery.NET.Models;Upgrading
dotnet add package FlexQuery.NET --version 3.1.0
dotnet add package FlexQuery.NET.Dapper --version 3.1.0
dotnet add package FlexQuery.NET.EntityFrameworkCore --version 3.1.0
dotnet add package FlexQuery.NET.Parsers.Jql --version 3.1.0
dotnet add package FlexQuery.NET.Parsers.MiniOData --version 3.1.0
dotnet add package FlexQuery.NET.Adapters.AgGrid --version 3.1.0
dotnet add package FlexQuery.NET.Adapters.Kendo --version 3.1.0
dotnet add package FlexQuery.NET.AspNetCore --version 3.1.0