Skip to content

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

Merged
chullybun merged 32 commits into
mainfrom
graph-ql
Jul 28, 2026
Merged

feat: add CoreEx.Data.GraphQL (GraphQL-lite) query bridge#172
chullybun merged 32 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
…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 thread src/CoreEx.Data.GraphQL/GraphQLLiteOptions.cs
chullybun and others added 2 commits July 27, 2026 16:27
… cases

Addresses a follow-up AI code review of CoreEx.Data.GraphQL:

- Exception mapping: map ConflictException/DuplicateException/ConcurrencyException/
  AuthenticationException/AuthorizationException/BusinessException to their own error
  codes instead of collapsing into EXECUTION_ERROR; ValidationException now surfaces
  structured per-property extensions.messages.
- Unexpected (non-IExtendedException) errors are now always logged and only expose
  their real message when CoreEx:IncludeExceptionInProblemDetails is enabled (mirrors
  the REST WebApi contract); known IExtendedException types log only when
  ShouldBeLogged is true.
- Add GraphQLLiteOptions.MaxRootFields to bound alias/root-field fan-out per document,
  failing fast with TOO_MANY_ROOT_FIELDS before any backend work is performed.
- Add GraphQLLiteOptions.EnableIntrospection (default false, secure-by-default) gating
  __schema/__type; IGraphQLEngine.GetSchemaAsync() is unaffected. Contoso.Products.Api
  sample opts in, with a commented-out RequireAuthorization() demo on the endpoint.
- Cap introspection's advertised field nesting to mirror GraphQLTypeShape's runtime
  MaxDepth, so clients are never told about a field the runtime would reject.
- Fix a silent integer-overflow bug where an 'after' cursor encoding int.MaxValue
  wrapped to page 1 instead of erroring.
- Reject where/orderBy arguments on single-item roots (ARGUMENT_ERROR) instead of
  silently translating and discarding them.

Adds 28 new regression tests (148/148 passing across net8/9/10) plus a Depth0..Depth9
model chain for exercising the introspection depth cap, and updates README/AGENTS.md/
hosts-layer.md docs to describe the new secure defaults and remaining caveats.

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

Simplifies the AddGet resolver-authoring experience and closes a couple of gaps found
during review:

- New GraphQLLiteArgs wraps a resolver's raw argument dictionary and adds
  GetIdentifier<TId>(name = "id"), replacing the hand-rolled TryGetValue + type-check +
  throw boilerplate every AddGet resolver previously needed (see the Contoso.Products.Api
  sample's simplified 'product' root).
- AddGet<TItem> now constrains TItem : IReadOnlyIdentifier, since an item root is always
  conceptually a fetch-by-id; this also makes introspection's id: ID! argument
  advertisement unconditional rather than a runtime type-check.
- GetIdentifier throws ArgumentException (not InvalidCastException) for a missing/empty/
  wrong-typed argument, so it still routes through GraphQLEngine.MapException's
  ARGUMENT_ERROR branch instead of falling into the logged/masked unexpected-exception path.
- GraphQLNameValidator gains a shared IsValidName(string) used by both its own
  ValidateFieldName (request-time where/orderBy field names) and the new
  GraphQLLiteOptions.ThrowIfReservedOrDuplicate root-name grammar check
  (registration-time), removing a duplicate regex.

Updated GraphQLLiteOptionsTests/GraphQLEngineTests/DepthChain/Person test fixtures for
the new resolver signature and identifier constraint; full CoreEx.Data.GraphQL.Test.Unit
suite (148/148, net8/9/10) and the Contoso Products sample's GraphQL integration tests
(7/7, net8/9/10) pass.

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

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 75 out of 75 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

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

  • orderBy items are documented as “each object should specify a single field” because GraphQL input-object field order is not spec-guaranteed, but the implementation currently accepts multi-key objects and will translate them in whatever enumeration order the dictionary yields. This makes sort precedence ambiguous/non-portable for orderBy: [{ a: ASC, b: DESC }]. Consider rejecting multi-key objects with an ARGUMENT_ERROR so precedence is always expressed via list order ([{a: ASC}, {b: DESC}]).

- Route MapCoreExGraphQLLite through WebApi.PostAsync<GraphQLLiteResponse>
  instead of raw Results.Json, so ProblemDetails/exception-handling
  middleware applies as a safety net for anything the engine's own
  exception mapping doesn't catch.
- Wrap GraphQLEngine.ExecuteAsync with a new GraphQLEngineInvoker
  (InvokerBase<GraphQLEngine>) for OpenTelemetry tracing, and add
  WithCoreExGraphQLTelemetry() to register its activity source.
- Split GraphQLServiceCollectionExtensions.cs into
  GraphQLExtensions.DependencyInjection.cs /
  GraphQLExtensions.OpenTelemetry.cs, matching the repo's established
  CoreEx<Area>Extensions per-concern file convention; renamed the DI
  class to GraphQLExtensions to match.
- Wire .WithCoreExGraphQLTelemetry() into the Contoso Products sample's
  OpenTelemetry tracing chain.
- Align README/AGENTS.md/hosts-layer.md/host-setup instructions with
  the new WebApi pipeline integration and telemetry capability.

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

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 77 out of 77 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/CoreEx.Data.GraphQL/GraphQLLiteArgs.cs:34

  • GetIdentifier<TId> documentation says it "converts"/is "convertible", but the implementation only accepts values that are already of type TId (e.g., a numeric ID supplied as a string via variables would be rejected). This is both misleading for consumers and unnecessarily strict for the GraphQL ID scalar (commonly string-or-int). Consider adding common coercions (string↔numeric, string→Guid, etc.) or, at minimum, rewording the docs to match the current behavior.
    src/CoreEx.AspNetCore/CoreExAspNetCoreExtensions.GraphQLLite.cs:45
  • Typo in comment: "Self -deserializing" should be hyphenated as "Self-deserializing".
            // Self -deserializing the request body to the standard GraphQL-over-HTTP request envelope and self-handling any deserialization errors to return a GraphQL-lite error response.

@chullybun
chullybun merged commit 8555aae into main Jul 28, 2026
4 checks passed
@chullybun
chullybun deleted the graph-ql branch July 28, 2026 17:49
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