Skip to content

v6.0.0

Choose a tag to compare

@lukemurray lukemurray released this 20 Jul 03:38

EntityGraphQL 6.0 is the culmination of the 6.0.0-beta1 through 6.0.0-beta9 releases. The beta sections below carry the full, detailed lists of changes, fixes and migration notes - and see the upgrade guide for a consolidated migration path from 5.x.

6.0 Highlights

  • GraphQL spec conformance - partial results per the spec (failing top-level fields no longer fail the whole operation), September 2025 GraphQL spec support (@oneOf, argument/input-field deprecation, document descriptions, stricter validation) and MapGraphQL() follows the GraphQL over HTTP spec by default.
  • A real async model - ResolveAsync<TService>() fields, end-to-end CancellationToken support (into resolvers, mutations, subscriptions and the in-flight EF query), async database materialization for root list fields, and concurrency control per field, per service and per query (MaxQueryConcurrency defaults to 100).
  • Security hardening - authorization fails closed, introspection respects authorization, a parser recursion backstop against deeply-nested document DoS, opt-in query guards (MaxQueryDepth, MaxFieldSelections, MaxFieldAliases, MaxQueryComplexity) and per-field rate limiting.
  • New schema-building features - UseAggregate() for SQL-translated aggregates over collections, AddFieldsFrom<T>()/AddQueryFieldsFrom<T>() to group field definitions into classes, filter improvements (GraphQL variables, service fields, selectMany, filtering by paged child fields) and async subscription setup for external brokers.
  • Performance - paging answers hasNextPage with a cheap EXISTS instead of COUNT(*), async result processing is compiled and targeted (~2x faster on async-heavy results), opt-in compiled-delegate caching, and WebSocket subscription delivery is ordered and non-blocking.
  • Modern targets - net8.0, net9.0 and net10.0 with a dependency-light core package.

Breaking Changes from Beta 9

  • MapGraphQL() HTTP status codes now fully follow the GraphQL over HTTP spec for the application/graphql-response+json media type (the default response type): a response with no data entry (a request error - e.g. document parse/validation failure) returns 400 instead of 200, and a response with both data and errors (partial success) returns 294 per the spec. Clean results remain 200. Responses negotiated as legacy application/json keep returning 200 for every well-formed request. Clients following the spec read the response body regardless of status code for application/graphql-response+json.

  • AddFieldsFrom<TFrom>() / AddQueryFieldsFrom<TType>() (introduced in beta 9) now require the grouping class to implement the new IFieldsFor<TContext> marker interface declaring which type its [GraphQLField] methods are written against - the entity type for schema.Type<Person>().AddFieldsFrom<T>(), the query context for AddQueryFieldsFrom<T>(). Registering a class against the wrong schema type is now a compile error instead of a schema-build surprise, and the marker gives compile-time tooling the intended context type. The interface is contravariant, so a fields class written for a base type or shared interface can be added to types deriving from/implementing it. A class may also implement IFieldsFor<> for several contexts to group a feature across types (e.g. occupancy fields for Building, Floor and Space in one class) - each registration only adds the methods written for that context. See Grouping fields across multiple types. Migration: add : IFieldsFor<TheContextType> to each grouping class.

  • A [GraphQLField] method parameter whose type is a schema entity type (and not the field's context type) now fails at schema build with a clear error instead of being silently classified as a DI service - that classification almost always meant a mistyped context parameter that resolved (or failed confusingly) at query time. The query context type is excluded (a DbContext parameter remains a service), and the new [GraphQLService] parameter attribute opts out for the rare case where an entity type really is a DI service.

Changes since Beta 9

  • [GraphQLField] methods (via AddFieldsFrom<T>()/AddQueryFieldsFrom<T>() or static on a schema type) can now return an Expression<Func<TContext, ...>> instead of a value. The method is a factory invoked once at schema build time and the returned expression is registered exactly like AddField(...).Resolve(...) - a service-free expression composes fully into the database-bound pass (translates to SQL, only the columns it uses are selected), and extra expression parameters after the context are resolved as services with the standard two-pass execution and dependency extraction. The factory method itself takes no parameters. See Expression fields with AddFieldsFrom.

Bug Fixes since Beta 9

  • Fixed ResolveBulk fields failing with KeyNotFoundException when selected via a fragment spread (...MyFragment) or an inline fragment (... on Type { bulkField }) - the bulk loader was never registered because fragments expand after bulk registration ran. Bulk fields are now registered through fragments at any nesting depth, the bulk list expression path is taken from the compile-time selection path rather than a field's parent-node chain (which for fragment-selected fields pointed at the fragment definition, not the selection point), and selecting the same bulk field both directly and via a fragment loads the data once, not twice.
  • Fixed ResolveBulk fields failing when reached through a nested single-object navigation (e.g. tasks { project { createdBy } }) or a mutation result selection (e.g. mutation { updateTask { project { createdBy } } }) - single-object contexts are now wrapped for the bulk key batching, object navigations off a list context project per element, and the duplicate mutation result-selection path node is skipped by reference. Also released in 5.7.3. Thanks @alex-birch #526
  • Fixed custom type converters (AddCustomTypeConverter) registered for a base type never matching values of derived types - the lookup was an exact type match, so the documented Newtonsoft pattern AddCustomTypeConverter<JToken>(...) never fired (JToken is abstract; runtime values are JArray/JValue/JObject), breaking e.g. list-of-enum variables sent through Newtonsoft.Json with Supplied variable can not be applied to defined variable type. From-type converter lookup now walks the base-type chain (the most derived registration wins).
  • Fixed ResolveBulk fields reached through chained single-object navigations (e.g. projects { tasks { assignee { manager { bulkField } } } }) throwing Value cannot be null - the null-guard built while collecting the bulk keys assumed every navigation step was a list. Single-object steps are now null-guarded properly (a null parent yields null and is filtered before the bulk key selection). Also released in 5.7.3.