Skip to content

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>(...);

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