Skip to content

feat: add CoreEx.Data.GraphQL (GraphQL-lite) query bridge - #172

Open
chullybun wants to merge 29 commits into
mainfrom
graph-ql
Open

feat: add CoreEx.Data.GraphQL (GraphQL-lite) query bridge#172
chullybun wants to merge 29 commits into
mainfrom
graph-ql

Conversation

@chullybun

@chullybun chullybun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds CoreEx.Data.GraphQL — a transport-agnostic bridge that lets a native GraphQL query drive an entity's existing QueryArgsConfig-based QueryAsync/GetAsync pipeline, with no parallel query engine and no new authorization surface. GraphQL where/orderBy syntax is rewritten 1:1 onto the OData-esque QueryArgs/PagingArgs strings already understood by QueryFilterParser/QueryOrderByParser, so field/operator legality continues to be enforced by the same battle-tested parser used by REST $query.

Key capabilities

  • Native GraphQL where/orderBy translated syntactically to the OData-esque equivalent, then parsed by the existing, unmodified QueryFilterParser/QueryOrderByParser — exact operator/field parity with $query, including whatever QueryArgsConfig exposes (startswith(), ge, etc.).
  • Relay Cursor Connections paging (edges, pageInfo, totalCount) bridged onto PagingArgs, including a totalCount-only fast path that avoids over-fetching rows.
  • Field-selection projection via JsonFilter, honoring aliases and __typename at every depth; fragments are rejected loudly rather than silently ignored. The resolved selection is also fed into QueryArgs.IncludeFields for list roots, so the underlying QueryArgsConfig-driven data layer can project only what was actually selected.
  • includeText/includeInactive ref-data options, supported (and schema-advertised) on both list (AddQuery) and single-item (AddGet) roots — an item-root product(id: "...", includeText: true) { category categoryText } works the same way a list root does.
  • Spec-compliant introspection: __schema/__type(name:)/__typename are real and built once from the registered roots. Each query root's where/orderBy are described as fully-typed <Item>WhereInput/<Item>OrderByInput INPUT_OBJECT graphs (derived automatically from QueryArgsConfig.ToJsonSchema()), so schema-aware tooling (Postman, Nitro, Apollo Sandbox, GraphiQL) gets full autocomplete with no extra configuration.
  • Injection-safe: string operands are quote-escaped, and field names originate from the GraphQL Name grammar, so the constructed OData string can't be broken out of.
  • GraphQL-shaped error mapping: ArgumentException, KeyNotFoundException, GraphQLArgumentTranslationException, QueryFilterParserException, QueryOrderByParserException, ValidationException, NotFoundException, and unknown-field/unknown-root errors all map to distinct { message, path, extensions.code } shapes instead of a generic, unhelpful EXECUTION_ERROR.
  • Endpoint customization hook: MapCoreExGraphQLLite accepts an optional configure callback for further endpoint customisation (auth, rate limiting, etc.), and tags the endpoint for discoverability.
  • Layering: transport-agnostic contract (IGraphQLEngine, GraphQLEngineResult, GraphQLEngineError) lives in core CoreEx; the implementation lives in CoreEx.Data.GraphQL; the minimal-API HTTP bridge (MapCoreExGraphQLLite) lives in CoreEx.AspNetCore. Dependencies point the right way.

Hardening found via iterative review

Across several rounds of independent/automated review, the following were found and fixed with regression tests: culture-sensitive float literal parsing; unprojected object-typed leaf fields silently returning the whole DTO; undefined GraphQL variable references silently resolving to null; silent longint overflow in argument conversion; last-wins duplicate response-key aliases; totalCount-only over-fetching; a type-shape cache key that ignored JsonSerializerOptions; OperationCanceledException being swallowed into a field error instead of propagating; reserved/duplicate root-name registration; malformed numeric literal handling; a hasNextPage edge case; and the final response JSON silently falling back to default (rather than the configured) JsonSerializerOptions.

While hardening this feature, a genuine pre-existing, unrelated CoreEx.Json.JsonFilter bug was also found and fixed: sibling scalar fields where one name is a raw string prefix of another (e.g. the common ref-data category/categoryText Code/Text pair) were incorrectly treated as a nested-path relationship and the shorter field silently dropped from Include-filtered output — this affected REST $fields projection too, not just GraphQL-lite.

Docs

  • New src/CoreEx.Data.GraphQL/README.md and AGENTS.md, kept current with every capability above (introspection, includeText on item roots, etc.).
  • docs/capabilities.md, samples/docs/hosts-layer.md, samples/docs/patterns.md, .github/instructions/coreex-host-setup.instructions.md, .github/instructions/coreex-api-controllers.instructions.md, and .github/agents/coreex-expert.agent.md updated with the registration API and the CoreEx.ExecutionContext.GetRequiredService<T>() scoped-resolution pattern.
  • .github/instructions/coreex-repositories.instructions.md codifies the QueryArgsConfig visibility exception and folder-placement convention established while building this feature.

Testing

  • 118/118 CoreEx.Data.GraphQL.Test.Unit tests passing across net8/9/10.
  • 7/7 sample Contoso.Products.Test.Api GraphQLLite_* integration tests passing across net8/9/10, including a spec-compliant introspection test driven by a standard client-tooling introspection query (embedded resource).
  • 705/705 CoreEx.Test.Unit passing across net8/9/10 (includes the JsonFilter regression coverage above).
  • Full CoreEx.sln build clean (0 warnings, 0 errors).

chullybun and others added 11 commits July 23, 2026 15:06
Adds a transport-agnostic GraphQL-lite bridge over existing CoreEx.Data
dynamic querying (QueryArgs/PagingArgs), with:
- CoreEx.Data.GraphQL: engine, DI-driven root registration, selection-set
  to JsonFilter projection, arg mapping, and schema/discovery generation.
- CoreEx (src): IGraphQLEngine/GraphQLEngineResult/GraphQLEngineError contracts.
- CoreEx.AspNetCore: MapCoreExGraphQLLite minimal-API hosting bridge.
- Contoso.Products sample wiring + integration tests.
- Unit tests for the engine, arg mapper and value converter.
- Updated coreex-conventions.instructions.md with explicit private-method
  and access-level XML doc requirements.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… aliases at every depth

Closes the two most impactful GraphQL-ecosystem interop gaps found in review:
- __typename is now resolvable at root and any nested selection depth. Without
  this, any standard client (Apollo Client, Relay, urql) that auto-injects
  __typename into every selection set would get a spurious error on every
  single request.
- Fragment spreads/inline fragments now produce an explicit FRAGMENTS_NOT_SUPPORTED
  error instead of being silently skipped, which previously risked returning
  incomplete data with a misleading 200/no-errors response.
- Field aliases are now honored at every selection depth (not just root) via a
  new GraphQLResponseShaper reshaping pass, fixing a real correctness bug where
  nested aliases were silently ignored.

Updated README non-goals to accurately describe remaining gaps (no standard
introspection/SDL, no directives/interfaces/unions) and added unit test
coverage for all three fixes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… paging

Replace the initial filter/orderby/skip/take GraphQL argument convention
with GraphQL-idiomatic structured input:

- where: field-keyed operator objects (or bare-scalar equality shorthand)
  composed via and/or/not, translated 1:1 to the OData-esque filter string
  consumed by the entity's existing QueryArgsConfig (GraphQLFilterTranslator).
- orderBy: a list of field/direction objects, translated to the equivalent
  orderby string (GraphQLOrderByTranslator).
- Relay Cursor Connections paging (edges { node cursor } pageInfo totalCount)
  via first/after forward pagination, backed by an opaque offset cursor
  (GraphQLCursor). last/before are explicitly rejected (out of scope for v1).

Both translators are pure syntax rewrites with zero field/operator
legality checks by design -- real validation happens downstream, exactly
as it does today for the REST $filter/$orderby query strings, via the
same QueryArgsConfig parse pipeline. This preserves exact behavioral
parity with the existing OData-esque querying rather than introducing a
second, parallel validation surface.

Also:
- GraphQLSchemaBuilder discovery document now reports where/orderBy shapes
  (sourced from QueryArgsConfig.ToJsonSchema()) and the fixed Connection/
  Edge/PageInfo shape for query roots.
- GraphQLEngine.MapException maps GraphQLArgumentTranslationException to
  an ARGUMENT_ERROR code.
- Full unit test rewrite/expansion (78 tests) covering translators, cursor
  round-tripping, and engine-level where/orderBy/paging behavior against a
  real QueryArgsConfig.Parse pipeline (not string hacks).
- Sample Contoso.Products GraphQL-lite integration tests updated to the new
  query/response shape; verified against live Postgres infra.
- README updated for the new capabilities and non-goals (no last/before,
  no standard introspection).
The item-root GraphQL-lite test suite only covered the not-found path;
add the missing success-path counterpart, asserting the same known
seeded product (sku/text) that the REST Product_Get_Found test already
verifies, for parity coverage across both bridges.
The repo's .editorconfig naming rule (async_methods_async_suffix)
requires all async methods -- test methods included -- to end with
'Async'. Several GraphQLEngineTests methods contained 'Async' mid-name
(e.g. describing the method under test) but did not end with it,
triggering IDE1006 naming warnings. Renamed all 23 affected test
methods to end with the required suffix; no behavioral change.
Add src/CoreEx.Data.GraphQL/AGENTS.md matching the established
per-package AI Usage Guide pattern (registration, query syntax
example, Do Not rules, Further Reading), and cross-link it from
the README's new 'AI Usage Guide' section, consistent with every
other src/*/AGENTS.md-bearing package.

Also reviewed the README end-to-end against the current
implementation (GraphQLEngine, GraphQLLiteOptions, GraphQLQueryRoot/
GraphQLItemRoot, all Internal/* translators/resolvers, and the
CoreEx.AspNetCore MapCoreExGraphQLLite hosting bridge) -- confirmed
accurate, no content changes required beyond the new AI Usage Guide
section.
…ntroller, and capabilities docs

- coreex-expert.agent.md: add CoreEx.Data.GraphQL row to per-package AI usage guide table
- coreex-host-setup.instructions.md: add optional AddCoreExGraphQLLite/MapCoreExGraphQLLite registration to API host table and key points
- coreex-api-controllers.instructions.md: cross-link GraphQL-lite as an optional alternative to a REST query endpoint
- docs/capabilities.md: add GraphQL-lite Query Bridge subsection under Data Access & Persistence

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…older-placement convention

- coreex-repositories.instructions.md: document the public-visibility exception for QueryArgsConfig types backing a CoreEx.Data.GraphQL root, and the colocation-with-repository convention (5+ config escalation threshold) vs. a dedicated Query/ subfolder
- ProductQueryArgsConfig.cs: add the required one-line justification comment for its public visibility, per the new convention

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Parse GraphQL int/float literals with InvariantCulture (was CurrentCulture,
  causing 9.99 to be misparsed as 999 on e.g. de-DE machines).
- Require a selection set for object-typed leaf fields (item roots, node,
  and nested complex properties) instead of silently returning the whole
  unfiltered DTO; raises a new SELECTION_REQUIRED error.
- Throw GraphQLArgumentTranslationException for an undefined \
  reference instead of silently resolving to null, and make sure this (and
  any other argument-conversion error) is caught and mapped to an
  ARGUMENT_ERROR in ExecuteAsync's main loop rather than propagating
  unhandled - a real bug this fix exposed that had no prior coverage.
- Guard GetInt against unchecked long-to-int truncation, throwing instead
  of silently wrapping out-of-range values.
- Reject duplicate root-level response-key aliases (DUPLICATE_FIELD) rather
  than letting the last selection silently win.
- Add a needsItems flag to BuildConnectionPagingArgs so a totalCount-only
  selection caps Take at 1 instead of over-fetching a full page of rows
  that get discarded.
- Key the GraphQLTypeShape field-map cache by (Type, JsonSerializerOptions)
  instead of Type alone, so a second engine with different JSON options
  cannot get the wrong cached shape.
- Let OperationCanceledException propagate out of the engine's root
  resolvers instead of being swallowed into an EXECUTION_ERROR.

Adds regression tests for all of the above.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…edService pattern

The sample's Program.cs was simplified to resolve scoped dependencies via
CoreEx.ExecutionContext.GetRequiredService<T>() (reads the ambient scoped
service provider set by UseExecutionContext() middleware, which every
CoreEx host already registers) instead of an explicit
IHttpContextAccessor/HttpContext.RequestServices dance. Bring the
GraphQL-lite README, AGENTS.md, XML doc remarks, host-setup instructions,
and samples/docs/hosts-layer.md usage snippets in line with it.

Also corrects docs/capabilities.md's GraphQL-lite example, which had
drifted from the shipped API: AddCoreExGraphQLLite takes a two-parameter
(GraphQLLiteOptions, IServiceProvider) delegate, AddQuery/AddGet take
(QueryArgs?, PagingArgs?, ct) / (args dictionary, ct) - not a wrapped
RequestOptions object or a typed id parameter - and orderBy uses the
field-name-as-key shape ({ name: ASC }), not { field: "name" }.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ExecuteAsync_ResolverThrowsOperationCanceled_PropagatesRatherThanBecomingAnEngineError
was a void test method calling ThrowAsync(...) without awaiting the returned
Task, so NUnit never observed the assertion outcome - it would pass even if
the OperationCanceledException were swallowed into an EXECUTION_ERROR
result. Made the method async Task and awaited the assertion (and added
the Async suffix per convention). Verified by temporarily reverting the
production fix this test targets: it now fails as expected, then passes
once reverted back.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 18:19
@chullybun chullybun added the enhancement New feature or request label Jul 24, 2026
@chullybun chullybun added this to the v4.0.0-preview-3 milestone Jul 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces CoreEx.Data.GraphQL, a transport-agnostic “GraphQL-lite” query bridge that translates GraphQL where/orderBy and Relay-style paging onto the existing QueryArgsConfig + QueryArgs/PagingArgs pipeline, plus a minimal-API hosting bridge in CoreEx.AspNetCore and sample usage/tests/docs.

Changes:

  • Add new CoreEx.Data.GraphQL package (engine, translators, selection/projection, schema discovery) and new CoreEx-level contracts/result envelope types.
  • Add ASP.NET Core minimal-API bridge (MapCoreExGraphQLLite) and wire it into the Contoso Products sample with matching integration tests.
  • Update solution/test filters, dependency versions, and documentation/instructions to reflect the new capability and registration pattern.

Reviewed changes

Copilot reviewed 54 out of 54 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/CoreEx.Data.GraphQL.Test.Unit/Model/PersonQueryArgsConfig.cs Minimal QueryArgsConfig used by unit tests to exercise end-to-end translation/parsing.
tests/CoreEx.Data.GraphQL.Test.Unit/Model/Person.cs Test DTO shape for nested selection/projection tests.
tests/CoreEx.Data.GraphQL.Test.Unit/Internal/GraphQLValueConverterTests.cs Unit coverage for argument conversion (literals, variables, culture, overflow).
tests/CoreEx.Data.GraphQL.Test.Unit/Internal/GraphQLOrderByTranslatorTests.cs Unit coverage for orderBy translation and validation errors.
tests/CoreEx.Data.GraphQL.Test.Unit/Internal/GraphQLFilterTranslatorTests.cs Unit coverage for where translation and operand escaping/validation errors.
tests/CoreEx.Data.GraphQL.Test.Unit/Internal/GraphQLCursorTests.cs Unit coverage for cursor encoding/decoding behavior.
tests/CoreEx.Data.GraphQL.Test.Unit/Internal/GraphQLArgsMapperTests.cs Unit coverage for mapping GraphQL args to QueryArgs/PagingArgs (incl. count-only path).
tests/CoreEx.Data.GraphQL.Test.Unit/GraphQLEngineTests.cs End-to-end engine tests (projection, paging, schema, errors, cancellation).
tests/CoreEx.Data.GraphQL.Test.Unit/CoreEx.Data.GraphQL.Test.Unit.csproj New multi-targeted unit test project for GraphQL-lite engine.
src/CoreEx/Data/GraphQL/IGraphQLEngine.cs New transport-agnostic engine contract in CoreEx.
src/CoreEx/Data/GraphQL/GraphQLEngineResult.cs CoreEx-level result envelope for GraphQL-over-HTTP-style responses.
src/CoreEx/Data/GraphQL/GraphQLEngineError.cs CoreEx-level error envelope (message/path/extensions).
src/CoreEx.Data.GraphQL/README.md Package documentation (capabilities, usage, non-goals).
src/CoreEx.Data.GraphQL/Internal/GraphQLValueConverter.cs AST + variables → CLR argument conversion and typed accessors.
src/CoreEx.Data.GraphQL/Internal/GraphQLTypeShape.cs Reflected DTO field shape caching keyed by serializer options.
src/CoreEx.Data.GraphQL/Internal/GraphQLSelectionResolver.cs Selection-set validation + JsonFilter include-path flattening.
src/CoreEx.Data.GraphQL/Internal/GraphQLSchemaBuilder.cs Discovery document builder composing QueryArgsConfig schema + DTO field shape.
src/CoreEx.Data.GraphQL/Internal/GraphQLResponseShaper.cs Alias-aware reshaping plus __typename injection post-JsonFilter.
src/CoreEx.Data.GraphQL/Internal/GraphQLOrderByTranslator.cs GraphQL orderBy list-of-objects → OData-esque order-by string.
src/CoreEx.Data.GraphQL/Internal/GraphQLFilterTranslator.cs GraphQL where input object → OData-esque filter string.
src/CoreEx.Data.GraphQL/Internal/GraphQLCursor.cs Offset-based cursor codec for Relay forward pagination.
src/CoreEx.Data.GraphQL/Internal/GraphQLConnectionResolver.cs Validates/records fixed Connection/Edge/PageInfo selection shapes.
src/CoreEx.Data.GraphQL/Internal/GraphQLArgumentTranslationException.cs Dedicated exception type for GraphQL arg-shape translation failures.
src/CoreEx.Data.GraphQL/Internal/GraphQLArgsMapper.cs Maps resolved args into QueryArgs + paging semantics (incl. count-only optimization).
src/CoreEx.Data.GraphQL/GraphQLServiceCollectionExtensions.cs DI registration extension for singleton IGraphQLEngine.
src/CoreEx.Data.GraphQL/GraphQLQueryRoot.cs Query-root descriptor binding root name + config + resolver.
src/CoreEx.Data.GraphQL/GraphQLLiteOptions.cs Options container for explicit root registration (AddQuery/AddGet).
src/CoreEx.Data.GraphQL/GraphQLItemRoot.cs Single-item root descriptor binding root name + resolver.
src/CoreEx.Data.GraphQL/GraphQLEngine.cs Concrete engine implementation (parse, execute roots, apply projection, map errors).
src/CoreEx.Data.GraphQL/GlobalUsing.cs Global usings for the new package.
src/CoreEx.Data.GraphQL/CoreEx.Data.GraphQL.csproj New package project definition and dependencies.
src/CoreEx.Data.GraphQL/AGENTS.md AI usage guide for the new package (registration and do-not rules).
src/CoreEx.AspNetCore/GlobalUsing.cs Adds global using for GraphQL-lite engine contract types.
src/CoreEx.AspNetCore/CoreExAspNetCoreExtensions.GraphQLLite.cs Minimal-API bridge endpoint mapping GraphQL-over-HTTP envelope to IGraphQLEngine.
samples/tests/Contoso.Products.Test.Api/ReadTests.ProductGraphQLLite.cs Sample integration tests proving parity with REST query behavior.
samples/src/Contoso.Products.Infrastructure/Repositories/ProductQueryArgsConfig.cs Makes config public for host-level GraphQL-lite root registration.
samples/src/Contoso.Products.Api/Program.cs Registers engine roots and maps /api/products/query endpoint.
samples/src/Contoso.Products.Api/GlobalUsing.cs Adds global usings for GraphQL-lite registration and config access.
samples/src/Contoso.Products.Api/Contoso.Products.Api.csproj Adds project reference to CoreEx.Data.GraphQL.
samples/docs/patterns.md Documents GraphQL-lite query bridge as an API pattern.
samples/docs/hosts-layer.md Documents host wiring and usage constraints for the /query bridge.
README.md Adds the new package to the repo/package catalog.
docs/capabilities.md Adds “GraphQL-lite Query Bridge” capability documentation.
Directory.Packages.props Adds GraphQL-Parser dependency version.
CoreEx.slnx Adds new project + tests to solution index.
CoreEx.sln Adds new projects to the Visual Studio solution.
CoreEx.Core.Test.Parallel.slnf Includes new unit test project in the parallel test filter.
CoreEx.Core.slnf Includes new project + tests in core solution filter.
AGENTS.md Updates agent entry-point doc to include new data/query packages.
.github/instructions/coreex-repositories.instructions.md Documents QueryArgsConfig visibility exception for GraphQL-lite host registration.
.github/instructions/coreex-host-setup.instructions.md Documents GraphQL-lite registration and mapping APIs for hosts.
.github/instructions/coreex-conventions.instructions.md Updates documentation conventions guidance (incl. private methods, test exceptions).
.github/instructions/coreex-api-controllers.instructions.md Notes optional GraphQL-lite surface alongside REST query endpoints.
.github/agents/coreex-expert.agent.md Adds new package guide link for the expert agent references.
Comments suppressed due to low confidence (1)

src/CoreEx.Data.GraphQL/Internal/GraphQLValueConverter.cs:94

  • GetBool returns null for unsupported types, which can silently ignore invalid inputs (especially via variables) rather than returning an ARGUMENT_ERROR. Consider throwing GraphQLArgumentTranslationException for any non-null, non-boolean value.

Comment thread src/CoreEx/Data/GraphQL/GraphQLEngineResult.cs
Comment thread src/CoreEx.Data.GraphQL/Internal/GraphQLValueConverter.cs
Comment thread src/CoreEx.Data.GraphQL/Internal/GraphQLSelectionResolver.cs
Comment thread src/CoreEx.Data.GraphQL/GraphQLEngine.cs Outdated
…late package assets

- nuget-publish.ps1: add src\CoreEx.Data.GraphQL to ProjectsToPublish (was missing, so it never got packed/published).
- CoreEx.Template: add CoreEx.Data.GraphQL AGENTS.md to the coreex-ai docs-cache copy list, and a CoreEx.Data.GraphQL PackageVersion entry to the scaffolded solution's central _Directory.Packages.props so consumers can add the package with a resolvable version.
- Update the docs-sync skill file lists and package-guide counts (16 -> 17) across SKILL.md/README.md/coreex-ai-workflows.md/agents/README.md to include CoreEx.Data.GraphQL.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 18:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 61 out of 61 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

src/CoreEx/Data/GraphQL/GraphQLEngineResult.cs:36

  • GraphQLEngineResult.Failure is declared as params IEnumerable<GraphQLEngineError> but call sites pass a single GraphQLEngineError (e.g. Failure(NewError(...))), which won’t compile. Use params GraphQLEngineError[] (or a non-params IEnumerable<...> parameter) so the call sites are valid.
    src/CoreEx.Data.GraphQL/GraphQLEngine.cs:138
  • When edges/node are aliased (supported by GraphQLConnectionResolver), the error Path currently uses the literal field names ("edges", "node") rather than the response keys (aliases). This makes errors point at the wrong JSON path for clients using aliases; use the resolved aliases in the errorPath passed to GraphQLSelectionResolver.Resolve.
    src/CoreEx.Data.GraphQL/Internal/GraphQLValueConverter.cs:79
  • GetInt returns null for unsupported value types, which can silently turn an invalid argument (e.g. first: "abc" or a wrong-typed variable) into “not specified”, falling back to defaults or even a NOT_FOUND. For GraphQL arguments this should be an ARGUMENT_ERROR so callers get deterministic feedback.

Comment thread src/CoreEx.AspNetCore/CoreExAspNetCoreExtensions.GraphQLLite.cs
Comment thread src/CoreEx.Data.GraphQL/Internal/GraphQLTypeShape.cs
A single IGraphQLEngine instance dispatches to an arbitrary set of registered
roots (AddQuery/AddGet), so the entity is identified by the request body's
query, not the URL. Nesting the route under /products/ implied REST-style
one-endpoint-per-resource scoping, contradicting the engine's multi-root
design and the MapCoreExGraphQLLite default of '/query'. Renamed across the
sample host, its integration tests, and the READMEs/AGENTS.md/hosts-layer.md
that documented the old path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 25, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 61 out of 61 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

src/CoreEx/Data/GraphQL/GraphQLEngineResult.cs:36

  • GraphQLEngineResult.Failure is declared as params IEnumerable<GraphQLEngineError> but all call sites pass a single GraphQLEngineError (e.g. GraphQLEngine.ExecuteAsync), which will not compile. The params type should be GraphQLEngineError[] (or accept a single IEnumerable<GraphQLEngineError> without params).
    src/CoreEx.Data.GraphQL/README.md:70
  • The Key types table says IGraphQLEngine, GraphQLEngineResult, and GraphQLEngineError are in the CoreEx.Data namespace, but the types added in this PR are in CoreEx.Data.GraphQL. This should be corrected so consumers can locate the API quickly.

Comment thread src/CoreEx.Data.GraphQL/README.md
Comment thread src/CoreEx.Data.GraphQL/README.md Outdated
Comment thread src/CoreEx.AspNetCore/CoreExAspNetCoreExtensions.GraphQLLite.cs Outdated
…tInt/GetBool validation, DateOnly/TimeOnly scalars, doc fixes)

- GraphQLValueConverter.GetInt/GetBool now throw GraphQLArgumentTranslationException for invalid/unsupported values instead of silently returning null.
- GraphQLTypeShape now classifies DateOnly/TimeOnly as scalar, matching QueryFilterFieldConfigBase.
- GraphQLSelectionResolver, GraphQLConnectionResolver and GraphQLEngine now build GraphQLEngineError.Path from the client's requested alias (not the raw field name) at every nesting level, so error paths always match the actual response JSON.
- Fixed stale single-arg AddCoreExGraphQLLite(o => ...) examples in the AspNetCore XML remarks and GraphQL README to the actual two-arg (o, sp) => ... signature.
- Added regression tests for each fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 25, 2026 19:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 62 out of 62 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

src/CoreEx/Data/GraphQL/GraphQLEngineResult.cs:36

  • GraphQLEngineResult.Failure currently takes params IEnumerable<GraphQLEngineError> which makes call sites like GraphQLEngineResult.Failure(NewError(...)) a compile error (a single GraphQLEngineError is not an IEnumerable<GraphQLEngineError>). Change this overload to accept params GraphQLEngineError[] (or a single IEnumerable<GraphQLEngineError> without params).
    src/CoreEx.Data.GraphQL/README.md:20
  • README describes the IGraphQLEngine contract as being in the CoreEx.Data namespace, but the actual contract is CoreEx.Data.GraphQL (in the CoreEx project). This is likely to mislead consumers and should be corrected.
    src/CoreEx.Data.GraphQL/README.md:65
  • The Key types table lists the wrong AddCoreExGraphQLLite signature; the extension takes Action<GraphQLLiteOptions, IServiceProvider> (as shown in the usage snippet), not Action<GraphQLLiteOptions>.
    src/CoreEx.Data.GraphQL/README.md:70
  • The Key types table says IGraphQLEngine / GraphQLEngineResult / GraphQLEngineError are in the CoreEx.Data namespace, but they are defined in CoreEx.Data.GraphQL. Aligning this avoids confusion when searching/importing types.

…signature docs, unused response param)

- README incorrectly stated IGraphQLEngine/GraphQLEngineResult/GraphQLEngineError live in the CoreEx.Data namespace; they are declared under CoreEx.Data.GraphQL. Fixed both mentions.
- README Key types table still showed the stale single-arg AddCoreExGraphQLLite(IServiceCollection, Action<GraphQLLiteOptions>) signature; updated to the actual two-arg Action<GraphQLLiteOptions, IServiceProvider>.
- Removed the unused HttpResponse response minimal-API parameter from MapCoreExGraphQLLite (the handler always writes via request.HttpContext).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 25, 2026 19:44
…d-name validation, docs)

- GraphQLArgsMapper.BuildConnectionPagingArgs now returns a RequiresTotalCountForHasNextPage
  flag and forces IsCountRequested when PagingArgs.MaximumTake is so small that the usual
  one-item over-fetch is structurally impossible (MaximumTake <= first). GraphQLEngine falls
  back to a TotalCount-based hasNextPage derivation in that case. Added a regression test for
  MaximumTake = 1.
- Added GraphQLNameValidator to validate where/orderBy field-name dictionary keys against the
  GraphQL Name grammar before composing OData-esque filter/order-by strings, closing an
  injection/malformed-string risk when arguments arrive via JSON variables.
- GraphQLConnectionResolver's "unknown field" error messages now list __typename alongside the
  other allowed edge/pageInfo/root fields.
- Doc/wording fixes: GlobalUsing.cs ordering, AddGet cref signature drift (Service Collection
  extensions + GraphQLItemRoot), GraphQLCursor remarks (offset: not arrayconnection:), and
  GraphQLOrderByTranslator remarks recommending single-key-per-item orderBy objects.
- Made MovementQueryArgsConfig/OrderQueryArgsConfig public, aligning the samples with the
  "QueryArgsConfig is always public" guidance in coreex-repositories.instructions.md.
- Reverted non-functional CoreEx.sln header churn (BOM-only line, VisualStudioVersion suffix).
- Fixed ReadTests.ProductGraphQLLite's where/orderBy variable types to the actual generated
  ProductLiteWhereInput/ProductLiteOrderByInput names.
- Added Products integration tests exercising the standard client-tooling introspection query
  (fragments + all IntrospectionQuery fields) and __type(name:) lookups, validating GraphQL
  schema discovery works end-to-end over HTTP.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 14:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 68 out of 68 changed files in this pull request and generated 2 comments.

Comment thread src/CoreEx.Data.GraphQL/GraphQLEngine.cs
Comment thread samples/src/Contoso.Products.Api/Program.cs
…ERROR; harden AddGet id validation

- GraphQLEngine.MapException now classifies ArgumentException (and subtypes) and
  KeyNotFoundException as ARGUMENT_ERROR rather than the generic EXECUTION_ERROR,
  since resolver delegates commonly throw these standard .NET types to reject a
  missing/invalid argument.
- Contoso.Products.Api's AddGet<Product> resolver now validates the 'id' argument
  explicitly via TryGetValue + a non-empty string pattern, throwing ArgumentException
  instead of relying on an indexer + null-forgiving lookup that could surface as an
  opaque EXECUTION_ERROR or an unhandled NullReferenceException.
- Updated the matching README.md/AGENTS.md/docs/capabilities.md/samples/docs/hosts-layer.md/
  coreex-host-setup.instructions.md code samples to the safer pattern.
- Added GraphQLEngineTests coverage for the new ARGUMENT_ERROR mappings and confirmed the
  EXECUTION_ERROR fallback still applies to unmapped exception types.
- Refactored GraphQLLite_Introspection_StandardClientQuery_Succeeds to load its query from
  an embedded resource (Resources/ReadTests/ProductGraphQLLite_StandardIntrospectionQuery.graphql)
  instead of an inline C# raw string literal, matching existing test conventions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 15:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 69 out of 69 changed files in this pull request and generated 1 comment.

Comment thread src/CoreEx.Data.GraphQL/GraphQLEngine.cs
chullybun and others added 2 commits July 27, 2026 10:32
…Filter

JsonFilter.CreateDictionary demoted a field to 'intermediary' (non-leaf,
dropped from Include output) whenever any other requested field's name was a
raw string prefix of it, with no path-segment-boundary check. This meant
requesting sibling scalar fields such as 'category'/'categoryText' (a common
CoreEx reference-data Code/Text pair) silently dropped 'category' from the
result, regardless of request order, since 'categoryText' textually starts
with 'category'.

Add IsProperDescendantPath, which additionally requires the character
immediately following the shorter path to be '.' or '[' (a genuine nested
path-segment boundary) before demoting. This affects any consumer of
JsonFilter.TryJsonFilter, including REST \ projection and GraphQL-lite
field selection.

Add regression tests covering both the internal CreateDictionary heuristic
and the end-to-end TryJsonFilter behaviour for the category/categoryText case.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…hread selected fields into list-root QueryArgs; endpoint configuration hook

- GraphQLIntrospectionSchemaBuilder: AddGet (item) root fields now advertise
  includeText/includeInactive arguments alongside the existing id: ID!,
  matching the behaviour GraphQLEngine already honours for item roots via
  GraphQLArgsMapper.BuildQueryArgs. Previously this worked at runtime but was
  invisible to schema-aware tooling (Postman, Nitro, Apollo Sandbox) since it
  wasn't declared in introspection.
- GraphQLEngine: list-root execution now sets queryArgs.IncludeFields from
  the resolved GraphQL selection paths, so the underlying QueryArgs-driven
  data layer can project only the fields actually selected by the request.
- GraphQLQueryRoot: log query/paging args at Debug level (when enabled) for
  easier local troubleshooting of list-root invocations.
- PagingArgs/QueryArgs: add ToString() overrides to support the new debug
  logging with a readable summary.
- MapCoreExGraphQLLite: accept an optional configure callback so callers can
  further customise the mapped endpoint (e.g. auth, rate limiting), and tag
  the endpoint with a display name/"GraphQL" group for discoverability.

Update README/AGENTS docs and GraphQLEngineTests to reflect the item-root
schema change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 17:32
…esponse

ExecuteAsync captures jsonOptions = JsonDefaults.SerializerOptions for
per-request serialization, but the final JsonSerializer.SerializeToElement
call for the top-level data object omitted it and fell back to the default
serializer options, which could produce a response JSON shape inconsistent
with the rest of the engine's output.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 73 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/CoreEx.Data.GraphQL/Internal/GraphQLOrderByTranslator.cs:37

  • orderBy list items are documented as "each object should specify a single field" because input-object field order is not spec-guaranteed; however the current implementation allows multi-key objects and relies on dictionary enumeration order, which can produce nondeterministic precedence. Consider rejecting multi-key objects with a clear translation error so precedence is always well-defined.

Comment thread src/CoreEx.Data.GraphQL/GlobalUsing.cs
Comment thread src/CoreEx.Data.GraphQL/GraphQLQueryRoot.cs Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 73 changed files in this pull request and generated 2 comments.

Comment thread src/CoreEx/Data/PagingArgs.cs Outdated
…marks wording, align Skip immutability check

- Move Microsoft.Extensions.Logging to GlobalUsing.cs instead of a file-level using in GraphQLQueryRoot.cs.
- Reword ProductQueryArgsConfig remarks: all sample QueryArgsConfig types are public, so drop the misleading 'not the usual internal' framing.
- Align PagingArgs.Skip to call CheckImmutable before assignment, matching Take/IsCountRequested.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 17:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 73 changed files in this pull request and generated 1 comment.

Comment thread src/CoreEx.Data.GraphQL/GraphQLEngine.cs Outdated
…resolver try/catch

BuildQueryArgs(args) (used for includeText/includeInactive translation) was called outside the try/catch in
ExecuteItemRootAsync, so a malformed argument value (e.g. includeText passed a non-boolean) threw a
GraphQLArgumentTranslationException that escaped ExecuteAsync and aborted the whole request, instead of
being mapped to a per-field ARGUMENT_ERROR like every other item/list-root argument-translation failure.

Moved the call inside the existing try block, matching ExecuteQueryRootAsync's equivalent BuildQueryArgs
call. Added a regression test that reproduces the previous unhandled-throw behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 27, 2026 18:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 73 changed files in this pull request and generated 1 comment.

Comment thread src/CoreEx.Data.GraphQL/Internal/GraphQLTypeShape.cs Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 18:19
…e complex struct properties

GraphQLTypeShape.BuildFieldMap and GraphQLIntrospectionSchemaBuilder.EnsureObjectType both used
`elementType ?? property.PropertyType` / `node.ElementType ?? node.PropertyType` to pick the type to
recurse into for a complex property. ElementType is only populated for collections, so a non-collection
nullable complex struct property (e.g. Money? Total) fell through to the raw declared PropertyType -
Nullable<Money> - and reflection recursed into Nullable<T>'s own HasValue/Value properties instead of
Money's Amount/Currency. This broke both selection-set validation (spurious UNKNOWN_FIELD errors for
genuine struct properties) and introspection (registering a bogus "Nullable`1" OBJECT type).

Unwrap via Nullable.GetUnderlyingType(...) ?? propertyType at both call sites. Added a GraphQLEngineTests
regression test (readonly record struct Money, nullable Invoice.Total property) that reproduces the
previous UNKNOWN_FIELD failure.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 73 changed files in this pull request and generated 1 comment.

Comment on lines +66 to +73
private void ThrowIfReservedOrDuplicate(string name)
{
if (name.StartsWith("__", StringComparison.Ordinal))
throw new ArgumentException($"Root field name '{name}' is reserved for GraphQL introspection; names starting with '__' are not permitted.", nameof(name));

if (_queryRoots.ContainsKey(name) || _itemRoots.ContainsKey(name))
throw new ArgumentException($"A root field named '{name}' is already registered.", nameof(name));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants