CoreEx.Data.GraphQL
4.0.0-preview-3
dotnet add package CoreEx.Data.GraphQL --version 4.0.0-preview-3
NuGet\Install-Package CoreEx.Data.GraphQL -Version 4.0.0-preview-3
<PackageReference Include="CoreEx.Data.GraphQL" Version="4.0.0-preview-3" />
<PackageVersion Include="CoreEx.Data.GraphQL" Version="4.0.0-preview-3" />
<PackageReference Include="CoreEx.Data.GraphQL" />
paket add CoreEx.Data.GraphQL --version 4.0.0-preview-3
#r "nuget: CoreEx.Data.GraphQL, 4.0.0-preview-3"
#:package CoreEx.Data.GraphQL@4.0.0-preview-3
#addin nuget:?package=CoreEx.Data.GraphQL&version=4.0.0-preview-3&prerelease
#tool nuget:?package=CoreEx.Data.GraphQL&version=4.0.0-preview-3&prerelease
CoreEx.Data.GraphQL
A transport-agnostic GraphQL-lite bridge over the
CoreEx.DataOData-esque dynamic$filter/$orderby/paging query capability andJsonFilterfield include projection.
Overview
CoreEx.Data.GraphQL lets a domain expose its existing QueryAsync/GetAsync repository or service methods
through a single GraphQL-lite /query endpoint, without hand-authoring a GraphQL schema, resolvers, or a
new execution engine. It parses the standard GraphQL-over-HTTP request envelope (query, operationName,
variables) with GraphQL-Parser (AST only — no
execution engine), translates the GraphQL-native where/orderBy structured arguments 1:1 onto the same
OData-esque filter/orderby strings consumed by an entity's existing QueryArgsConfig, exposes list
query roots as Relay Cursor Connections, and flattens the
requested selection set into JSON include paths consumed by CoreEx.Json.JsonFilter — the exact same
projection mechanism CoreEx.AspNetCore's WebApi already uses for $fields/$exclude.
The engine is deliberately transport-agnostic: it references only CoreEx.Data (→ CoreEx.Events →
CoreEx), has zero dependency on ASP.NET Core, and is consumed via the IGraphQLEngine contract
(CoreEx.Data.GraphQL namespace, CoreEx project) so that hosting bridges — such as a minimal API endpoint in
CoreEx.AspNetCore — never need to reference this package's implementation types directly.
Key capabilities
- 🧩 Query-only GraphQL-lite bridge: parses a GraphQL document, resolves top-level root fields against
explicitly registered query/item roots — no mutations, subscriptions, cross-repository nested resolvers
(dataloaders), interfaces, unions, or directives in v1. Fragments and inline fragments are rejected with an
explicit
FRAGMENTS_NOT_SUPPORTEDerror rather than being silently ignored. - 🏷️
__typenamesupport: the standard__typenamemeta-field is answerable at every selection depth (Connection, Edge, node, and any nested object), since mainstream GraphQL clients (Apollo Client, Relay, urql) auto-inject it into every selection set for cache normalization. - 🔤 Field aliases at every depth:
field: realNamealiasing is honored throughout the selection set, not just at the root — the response is reshaped (viaGraphQLResponseShaper) to match the client's requested keys. - 🎯 Native GraphQL
where/orderBy, exactQueryArgsConfigcompatibility: list query roots accept a GraphQL-idiomatic, field-keyedwhereinput (operator objects or bare-scalar equality shorthand, composed viaand/or/not) and anorderBylist of field/direction objects — mirroring mainstream conventions (Hot Chocolate, Prisma). These are pure syntax translations (GraphQLFilterTranslator/GraphQLOrderByTranslator) onto the OData-esquefilter/orderbystrings; the translated string is always parsed/validated by the entity's own, unmodifiedQueryArgsConfig(QueryFilterParser/QueryOrderByParser) — so whatever operators and fields aQueryArgsConfigalready exposes for the REST$filter/$orderbyquery strings are supported exactly, with no separate allow-list to maintain. - 🔗 Relay Cursor Connections paging: list query roots return the spec-shaped
edges { node cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCountresponse viafirst/afterforward pagination (backward pagination —last/before— is out of scope for v1 and rejected with an explicit error).totalCountis only computed when the client's selection actually requests it. - 🪆 Nested DTO shape support: a
node's selection set may traverse arbitrarily deep into a DTO's own object graph (e.g.node { address { street city } }) since projection is performed over one already-materialized result viaJsonFilter, not via per-field resolvers. - 🧾 GraphQL-shaped errors:
GraphQLArgumentTranslationException,QueryFilterParserException,QueryOrderByParserException,ValidationException,NotFoundException, and unknown-field errors are mapped to{ message, path, extensions.code }error objects. - 🔍 Spec-compliant introspection:
__schema/__type(name:)(plus__typename) implement the official GraphQL introspection schema, built once from the registered roots (seeInternal.GraphQLIntrospectionSchemaBuilder) and exposed identically viaIGraphQLEngine.GetSchemaAsync(). Each query root'swhere/orderByarguments are described as fully-typed<Item>WhereInput/<Item>OrderByInputINPUT_OBJECTgraphs (and/or/notcomposition,eq/ne/gt/ge/lt/le/in/startsWith/endsWith/containsoperator inputs, and a sharedSortDirectionenum), derived directly from the root's existingQueryArgsConfig.ToJsonSchema()field descriptions — no extra configuration needed; see Non-goals below for the remaining simplifications. - 🧷 Explicit, code-based registration:
services.AddCoreExGraphQLLite((o, sp) => o.AddQuery(...).AddGet(...))— no attribute-based auto-discovery. - 🔌
WebApipipeline integration: theCoreEx.AspNetCorehosting bridge (MapCoreExGraphQLLite) executes throughCoreEx.AspNetCore.Http.WebApi.PostAsync<GraphQLLiteResponse>(...)— the same response-shaping pipeline every other CoreEx REST endpoint uses — so an unexpected bug that escapes the engine's own exception mapping still surfaces as a standard CoreExProblemDetailsresponse instead of an unhandled 500. - 📡 OpenTelemetry:
WithCoreExGraphQLTelemetry()(inOpenTelemetry.Trace,CoreEx.Data.GraphQLpackage) wiresGraphQLEngineInvoker's activity source into the OTEL tracer provider, so everyExecuteAsynccall produces a span alongside the rest of a host's CoreEx instrumentation.
Key types
| Type | Description |
|---|---|
GraphQLEngine |
The concrete IGraphQLEngine implementation: parses the document, resolves root fields, applies JsonFilter projection, and assembles the GraphQLEngineResult (including the Relay Connection shape for query roots). ExecuteAsync is wrapped by GraphQLEngineInvoker for OpenTelemetry tracing. |
GraphQLEngineInvoker |
InvokerBase<GraphQLEngine> used internally by ExecuteAsync; its activity source is registered via WithCoreExGraphQLTelemetry(). |
GraphQLLiteOptions |
The DI options builder: AddQuery<TItem> (list roots bound to a QueryArgsConfig + QueryAsync-shaped delegate), AddGet<TItem> (single-item roots), and AddReferenceDataQueries(sp, queryArgsConfig, prefix, excludeTypes) (bulk-registers every reference data type known to ReferenceDataOrchestrator as a query root, keyed by its alternate/GraphQL-friendly name). |
GraphQLExtensions |
AddCoreExGraphQLLite(IServiceCollection, Action<GraphQLLiteOptions, IServiceProvider>) registration extension, and (in GraphQLExtensions.OpenTelemetry.cs) WithCoreExGraphQLTelemetry(OpenTelemetryBuilder). |
GraphQLQueryRoot / GraphQLItemRoot |
Registered list-query and single-item root field descriptors. |
Internal.GraphQLFilterTranslator / Internal.GraphQLOrderByTranslator |
Translate the GraphQL-native where/orderBy structured arguments to the OData-esque filter/orderby strings consumed by QueryArgsConfig. |
Internal.GraphQLCursor |
Encodes/decodes the opaque, offset-based Relay Cursor Connections cursor. |
IGraphQLEngine (in CoreEx, namespace CoreEx.Data.GraphQL) |
The transport-agnostic contract: ExecuteAsync(document, operationName, variables, ct) and GetSchemaAsync(ct). |
GraphQLEngineResult / GraphQLEngineError (in CoreEx, namespace CoreEx.Data.GraphQL) |
The plain result/error POCOs returned by ExecuteAsync, mirroring the GraphQL-over-HTTP response shape. |
Usage
// Program.cs (or a domain composition extension)
builder.Services.AddCoreExGraphQLLite((o, sp) =>
{
o.AddQuery<ProductLite>("products", ProductQueryArgsConfig.Default, async (qa, pa, ct) => await CoreEx.ExecutionContext.GetRequiredService<IProductReadService>().QueryAsync(qa, pa, ct).ConfigureAwait(false))
// GetIdentifier<TId> validates the named argument (default "id") for presence and type (it casts to TId, it does not convert) and throws an ArgumentException - mapped by the engine to an ARGUMENT_ERROR GraphQL error - if
// it is missing, empty, or the wrong type, instead of an unhandled KeyNotFoundException/NullReferenceException surfacing as an opaque EXECUTION_ERROR.
.AddGet<Product>("product", (args, ct) => CoreEx.ExecutionContext.GetRequiredService<IProductReadService>().GetAsync(args.GetIdentifier<string>(), ct));
});
// ...
app.MapCoreExGraphQLLite("/api/query"); // Additive GraphQL-lite bridge alongside the existing REST endpoints.
// Optional: OpenTelemetry tracing for GraphQLEngine.ExecuteAsync, alongside the rest of the host's CoreEx instrumentation.
builder.WithCoreExTelemetry()
.WithCoreExGraphQLTelemetry()
.UseOtlpExporter();
To expose every reference data type known to ReferenceDataOrchestrator as a GraphQL query root (one root per type, keyed by its alternate/GraphQL-friendly name), use AddReferenceDataQueries instead of one AddQuery call per type:
builder.Services.AddCoreExGraphQLLite((o, sp) =>
{
// Bulk-register all ref-data types as query roots (prefix defaults to "ref_"; use null for no prefix).
o.AddReferenceDataQueries(sp, ReferenceDataQueryArgsConfig.Default, prefix: "ref_");
// Mix with regular entity roots as needed.
o.AddQuery<ProductLite>("products", ProductQueryArgsConfig.Default, async (qa, pa, ct) => ...);
});
Every reference data type known to ReferenceDataOrchestrator is exposed — not just types with a registered alternate name. Each root is named <prefix><name> (hyphens replaced with underscores, since GraphQL field names cannot contain them), where <name> is the type's IReferenceDataProvider.AlternateNames entry where one is registered, otherwise the type's own Type.Name. Pass excludeTypes to opt specific types out of this bulk registration.
A hosting bridge (e.g. MapCoreExGraphQLLite in CoreEx.AspNetCore) resolves IGraphQLEngine from DI and
calls ExecuteAsync with the parsed request envelope, returning { data, errors } as the HTTP response
body via WebApi.PostAsync<GraphQLLiteResponse>(...) — the same response-shaping pipeline every other CoreEx
REST endpoint uses, so ProblemDetails/exception handling and logging middleware still apply as a safety net
for anything the engine's own exception mapping doesn't catch. Since IGraphQLEngine is registered as a
singleton, root resolvers that need scoped dependencies (e.g. a repository or application service) should
resolve them per-invocation rather than capturing an instance resolved from the root IServiceProvider at
registration time — as shown above via CoreEx.ExecutionContext.GetRequiredService<T>(), which reads from
the ambient ExecutionContext's scoped service provider (set by the UseExecutionContext() middleware every
CoreEx host already registers), so no extra IHttpContextAccessor wiring is required.
A client queries the products root using native GraphQL where/orderBy and first/after Relay paging
— translated 1:1 to ProductQueryArgsConfig's existing filter/orderby support:
{
products(where: { sku: { startsWith: "spec" } }, orderBy: [{ text: DESC }], first: 10) {
edges {
node { sku text }
cursor
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
Non-goals (v1)
- No mutations or subscriptions — read/query only.
- No cross-repository nested resolvers (dataloaders/N+1 batching) — a
node's selection set may traverse nested properties already present on the DTO returned by a singleQueryAsync/GetAsynccall, but cannot request a field that would require invoking a different registered root. - No fragments (spreads or inline), interfaces, unions, or directives — a fragment in the document produces
an explicit
FRAGMENTS_NOT_SUPPORTEDerror rather than being silently skipped. - No backward pagination (
last/before) — Relay Cursor Connectionsfirst/afterforward pagination only; alast/beforeargument produces an explicit error rather than being silently ignored. - No standard GraphQL SDL export — the schema is only queryable at runtime via
__schema/__type(or the equivalentIGraphQLEngine.GetSchemaAsync()), not printable as a.graphqlSDL document. where/orderByargument field names in the generated<Item>WhereInput/<Item>OrderByInputtypes are the all-lowercase names already reported byQueryArgsConfig.ToJsonSchema()(e.g.subcategoryrather thansubCategory), not the DTO's camelCase JSON naming — cosmetic only, since field matching is case-insensitive.- Every field of a given JSON-schema type (
string/integer/number/boolean) shares one generic<Type>FilterInputoperator set (e.g.StringFilterInput) rather than a per-field-restricted shape, so a field may advertise an operator (e.g.gt) its specific configuration does not actually permit —QueryFilterParserstill enforces the real legality at execution time (defense in depth). - CLR
enumand reference-data (IReferenceData) output properties are described as theStringscalar (matching their actual JSON wire representation), not a specENUMtype. - A single-item
AddGetroot only advertises anid: ID!argument where its registered item type implementsIReadOnlyIdentifier<TId>, since theAddGetregistration API does not declare an argument shape today; it always advertisesincludeText/includeInactivealongside it, since the engine honours both for item roots too (see below). - Not a replacement for the REST
$filter/$orderby/$fieldsquery-string endpoints — this is an additive bridge sharing the same underlying pipeline. - Introspection is disabled by default (
GraphQLLiteOptions.EnableIntrospection = false) — a request for__schema/__typeproduces anINTROSPECTION_DISABLEDerror until explicitly enabled (e.g. so client tooling like GraphiQL, Postman, or Apollo/Relay codegen can introspect the schema in development). The directIGraphQLEngine.GetSchemaAsync()API is unaffected by this toggle. - No query-cost/complexity budget beyond
MaxRootFields—GraphQLLiteOptions.MaxRootFields(defaultnull, unlimited) only bounds the number of root fields (including aliased repeats) in one document; there is no per-request node/complexity scoring, and nested selection depth is bounded only by the underlyingGraphQL-Parserlibrary's ownMaxDepthdefault. - Introspection's advertised nesting depth is an approximation of the runtime cap —
__typefield resolution mirrorsGraphQLTypeShape's runtime depth cap (MaxDepth = 8) along each type's first traversal path, but the introspection type registry is keyed by CLR type name and short-circuits on repeat visits (cycle guard), so a type reachable both shallow and deep keeps whichever depth it was first visited at — it does not perform a true per-path re-evaluation for every possible route to that type. - No authorization is applied by default —
MapCoreExGraphQLLitemounts the endpoint anonymously unless the caller supplies its ownconfiguredelegate (e.g.rb => rb.RequireAuthorization()); since this endpoint can reach the same underlying data as[Authorize]-protected REST controllers, hosts should apply equivalent authorization explicitly.
AI Usage Guide
An AGENTS.md file is included with this package. AI coding assistants (GitHub Copilot, Claude, Cursor, etc.) that support workspace-injected package documentation will automatically surface concise usage guidance, code examples, and Do Not rules for this package without requiring a local CoreEx checkout.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CoreEx.Data (>= 4.0.0-preview-3)
- GraphQL-Parser (>= 9.5.1)
-
net8.0
- CoreEx.Data (>= 4.0.0-preview-3)
- GraphQL-Parser (>= 9.5.1)
-
net9.0
- CoreEx.Data (>= 4.0.0-preview-3)
- GraphQL-Parser (>= 9.5.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.0.0-preview-3 | 60 | 8/4/2026 |