feat(select): add field projection (Select) support#335
Conversation
- Honor mapper.Configuration.CaseSensitive in WalkSegment property lookup. - Drop the direct-property-walk fallback in ResolvePath: when no mapper key matches the full path or any prefix, throw GridifySelectException instead of silently bypassing the mapper. This restores the IgnoreNotMappedFields contract for unmapped fields that happen to exist as real properties on T. - Add XML documentation comments to ApplySelect overloads. - Add EF integration test for collection navigation projection (groups.name) to verify the expression tree translates to SQL rather than falling back to client-side evaluation. - Add unit test covering the partial-mapper + IgnoreNotMappedFields end-to-end path through ApplySelect.
- Introduce GridifySelectFieldNotMappedException : GridifySelectException
thrown only from the "no mapper key matched" branch of ResolvePath.
- Narrow the catch in SelectExpressionBuilder.Build from
GridifySelectException to the new subtype, so structural errors raised
during the prefix walk (two-level collection, "not a property of type")
surface to the caller even when IgnoreNotMappedFields is true. The
field is mapped — it just isn't projectable.
- Add SelectExpressionBuilder.ValidatePaths: a validator-only resolver
that tokenizes + walks the mapper without invoking SelectTypeFactory,
so validating user input no longer emits and caches dynamic types per
path. Refactor IsValidSelect to call it and to wrap the body in
catch (Exception), matching the never-throws convention used by
IsValid for IGridifyFiltering / IGridifyOrdering.
- Tests: cover structural errors propagating under IgnoreNotMappedFields,
the typed subexception, IsValidSelect's correct true/false outcomes
under IgnoreNotMappedFields, and the diagnostic-message regression
("Field 'X' is not mapped" instead of "Select produced no fields.").
- extensions.md: add ApplySelect, GridifySelect, GridifyQueryableSelect sections matching the existing ApplyFiltering / Gridify pattern (LINQ-equivalent example, IGridifySelecting overload, cross-link to the Selecting section in gridifyQuery.md). - gridifyQuery.md: fix opening grammar, replace the leftover editorial line with a Select forward-reference, add an "If Select is set" bullet to the IsValid notes, and add a Selecting section covering shape rules (nested / one-level collection), validation via IsValidSelect, and the NativeAOT / Elasticsearch limitations. - queryBuilder.md: add AddSelect / BuildSelect / BuildSelectWithPaging rows to the method table and an AddSelect section with a BuildSelectWithPaging example.
PR Summary
|
There was a problem hiding this comment.
Pull request overview
Adds first-class Select (field projection) support across Gridify’s pipeline (extensions + QueryBuilder), enabling callers to project an IQueryable<T> into runtime-emitted shapes containing only requested fields (including one-level nested objects / collections), with EF Core translating this into column-pruned SQL.
Changes:
- Introduces Select parsing/validation + projection builder (tokenizer, expression builder, runtime type emission + caching).
- Wires Select through
ApplySelect,GridifySelect/GridifyQueryableSelect,IGridifySelecting+IsValidSelect, and QueryBuilder’sBuildSelect*API. - Adds unit + EF integration tests and updates docs for the new selection feature.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/Gridify/Syntax/SelectTokenizer.cs |
Adds select-string tokenization + syntax validation. |
src/Gridify/Builder/SelectExpressionBuilder.cs |
Builds projection expressions (flat/nested/collection) and validator-only path resolution. |
src/Gridify/Builder/SelectTypeFactory.cs |
Emits and caches runtime projection types via Reflection.Emit. |
src/Gridify/Builder/SelectShape.cs |
Defines the resolved select “shape” tree used by the builder/factory. |
src/Gridify/Exceptions/GridifySelectException.cs |
Adds base exception type for select failures. |
src/Gridify/Exceptions/GridifySelectFieldNotMappedException.cs |
Adds typed exception for unmapped select fields. |
src/Gridify/IGridifySelecting.cs |
Introduces additive interface for queries that support Select. |
src/Gridify/GridifyQuery.cs |
Implements IGridifySelecting by adding a Select property. |
src/Gridify/GridifyExtensions.cs |
Replaces stub ApplySelect, adds IsValidSelect, and introduces GridifySelect* pipeline methods. |
src/Gridify/IQueryBuilder.cs |
Adds AddSelect + BuildSelect* APIs to QueryBuilder contract. |
src/Gridify/QueryBuilder.cs |
Implements select forwarding/building/projection + paging variant. |
test/Gridify.Tests/SelectTests.cs |
Unit tests for tokenizer, type factory, expression builder, ApplySelect, pipeline, and validation. |
test/Gridify.Tests/QueryBuilderSelectTests.cs |
Tests QueryBuilder’s new select APIs + forwarding from AddQuery. |
test/EntityFrameworkSqlProviderIntegrationTests/SelectSqlTests.cs |
EF integration tests asserting column-pruned SQL and translatable collection projection. |
docs/pages/guide/queryBuilder.md |
Documents QueryBuilder AddSelect/BuildSelect* usage. |
docs/pages/guide/gridifyQuery.md |
Documents GridifyQuery.Select, validation, limitations, and NativeAOT note. |
docs/pages/guide/extensions.md |
Documents ApplySelect, GridifySelect, and GridifyQueryableSelect. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CaseSensitive controls map-key matching, not CLR property lookup, so WalkSegment now always reflects with IgnoreCase — nested selects like "childClass.name" no longer break under case-sensitive mappers when keys are camelCase but CLR properties are PascalCase. Resolved segments are also canonicalized (mapper-stored "From" casing for matched keys, CLR prop.Name for walked suffixes) so case-only variants share one cached emitted type and produce stable property names regardless of the user's input casing.
|
Thanks for the detailed PR, the overall direction looks good to me however I found one correctness issue that I think is worth fixing before approval. It looks like single-field Select aliases do not always use the mapper expression itself. For example, if a mapper exposes a friendly field name like The likely cause is the Could you add a small regression test for a single-segment alias mapped to a nested value, and ideally one for a collection projection alias too? One smaller API consistency note: since Once the alias projection issue is fixed, I think this will be in good shape. |
The allSingleSegment branch in SelectExpressionBuilder.BuildShape was
passing every single-segment alias through ExtractFirstSegmentExpr,
which walks inward to find the first member directly on rootParam.
That broke two cases:
* alias to a nested value (childName -> x.ChildClass.Name) projected
ChildClass instead of the Name leaf.
* alias to a collection projection (tags -> x.Items.Select(i => i.Tag))
was unwrapped back to x.Items, losing the projection.
Bind first.Body / first.LeafType directly for the single-segment
branch. ExtractFirstSegmentExpr is still used for the nested/collection
branch where grouping by the first root segment is required.
Also clarify the IsValid contract (non-breaking): XML docs on
IQueryBuilder.IsValid and AddSelect now state the select string is
not validated and point to IsValidSelect. Refresh the stale summary on
the IGridifyQuery IsValid extension to mention Select. Document the
SelectTypeFactory cache as process-wide and unbounded, recommending
upfront validation when select strings come from untrusted input.
|
Thanks for the careful review! Both addressed in Alias projection: the
Two design notes worth flagging for separate discussion (no action expected here):
|
Description
Adds first-class field projection to Gridify. Callers can supply a
Selectstring (e.g.,select=name,address.city,orders.amount) alongsideFilter,OrderBy,Page, andPageSize, and Gridify projects the queryable to a sequence of dynamically-typed objects containing only the requested fields. Under EF Core this translates to column-pruned SQL.This replaces the half-implemented
ApplySelectstub inGridifyExtensions.csand addresses the integration gaps that caused PR #62 to be closed (no breaking changes; full integration withGridify(),GridifyQueryable(),QueryBuilder; verified EF compatibility).Highlights
IGridifyQuery/IGridifyFiltering/IGridifyOrdering/IGridifyPaginationare unchanged. The newIGridifySelectinginterface is additive —GridifyQueryimplements it, but direct implementors ofIGridifyQueryare unaffected.ApplySelectoverloads, the projectingGridifySelect/GridifyQueryableSelectpipeline methods, andQueryBuilder.AddSelect+BuildSelect/BuildSelectWithPagingfamily.QueryBuilder.AddQueryforwardsSelectautomatically when the supplied query also implementsIGridifySelecting.ToQueryString()assertions confirm column-prunedSELECToutput, including a collection-projection (groups.name) test that catches client-side-evaluation regressions.IsValidSelectmirrors the existing Filter / Order validators (without List<string> errorsoverload); the compositeIGridifyQuery.IsValidis extended to also coverSelectwhen the query implementsIGridifySelecting.System.Reflection.Emit.TypeBuilder, cached per shape signature with double-checked locking.Type of change
Checklist
Limitations / non-goals
select=fullName:firstName,...) — out of scope.count,sum) — out of scope.orders.amountworks;orders.items.pricedoes not). Deeper projections throwGridifySelectExceptionwith a clear message.Gridify.Elasticsearchis a follow-up (Elasticsearch projects via_sourcefield filtering, which is a parallel design problem).SelectTypeFactorythrowsGridifySelectExceptionreferencing NativeAOT support #140.Tests
ApplySelectend-to-end onIQueryable<T>,IsValidSelect(including theIgnoreNotMappedFieldspath and structural-error propagation), and theQueryBuilder.AddSelect/BuildSelect*API.ToQueryString()for flat, two-field, filter-then-select, and collection-projection cases.Notes for reviewers
The opening commits introduce the building blocks (tokenizer, factory, expression builder); the later commits wire them into the public API and tests. The two
fix(select):commits at the head address findings from a structured pre-PR review — narrowingIgnoreNotMappedFieldsto a typedGridifySelectFieldNotMappedExceptionsubtype so structural errors (multi-level collection, missing-property-on-resolved-type) propagate even with the flag on, refactoringIsValidSelectto use a validator-only path resolver that doesn't pollute theSelectTypeFactorycache, and tightening the validator tocatch (Exception)matching the existingIsValidvalidators' contract.