EntityGraphQL v6.0.0-beta9 Release Notes
Release Date: 2026-07-06 // 3 months ago-
The final beta planned before 6.0.0.
๐ฅ Breaking Changes
- โฌ๏ธ Dropped the
netstandard2.1target. EntityGraphQL now targetsnet8.0,net9.0andnet10.0. This also removes theSystem.Text.Json,Microsoft.CSharpandSystem.ComponentModel.Annotationspackage dependencies that only the netstandard target required. - ๐ Security (fail closed) โ authorization now denies by default when a requirement is present but cannot be satisfied. A bare
[GraphQLAuthorize]/[Authorize](no roles or policy) now correctly requires an authenticated user instead of being a silent no-op, andIsAuthorizedrequiresIdentity.IsAuthenticated. InEntityGraphQL.AspNet, a policy-protected field is now denied (rather than allowed through) when noIAuthorizationServiceis registered or the user isnull. Fields/types with no authorization attribute are unaffected. This is a behavior change: previously-open access that relied on any of these gaps will now be blocked. - Introspection now respects authorization โ introspection queries (
__schema/__ type) only return the types and fields the requesting user is authorized to access, so anonymous users can no longer enumerate role/policy-protected type, field and argument names.schema.ToGraphQLSchemaString()is unchanged and always outputs the full schema (for SDL files / codegen). Note introspection output can now differ per user, which may affect schema-caching tooling running as an unprivileged user. - 0๏ธโฃ
ExecutionOptions.MaxQueryConcurrencynow defaults to 100 (previously unlimited). An async field resolved for a list runs per item, so an unbounded default turned a large result set into an unbounded number of concurrent operations (socket/thread-pool exhaustion, hammering rate-limited APIs). SetMaxQueryConcurrency = nullto restore unlimited. Note this does not make non-thread-safe services (e.g. a scopedDbContext) safe in async list fields โ useServiceConcurrencyLimitsormaxConcurrency: 1for those. Also fixed the concurrency wrapper for field shapes it previously failed on (value-type service parameters such asCancellationToken, rebound per-element parameters, nested list results โ errors likeunbound variableorNo coercion operator), andIAsyncEnumerablefields are no longer wrapped (the stream's enumeration, not its creation, is the async work). - API surface โ
LinqRuntimeTypeBuilderandExpressionReplacerare nowinternal;ExpressionUtil.ListToSingleMethodsis no longer public (it was a mutable static).GraphQLHelper.InjectServicesgained optional parameters and accepts a null service provider.ConcurrencyLimitFieldExtension.ExecuteWithConcurrencyLimitAsyncis now generic (<TResult>) so the wrapped expression keeps its static type. - ๐ New GraphQL spec validations may reject previously accepted (invalid) documents โ duplicate non-repeatable directives, conflicting field selection merges,
@skip/@includeon a subscription root field and deprecating a required argument are now errors. See the spec compliance entry under Changes.
๐ Changes
- ๐ New
schema.Query().AddFieldsFrom<T>()(and any object type) plus theschema.AddQueryFieldsFrom<T>()shortcut - group related field definitions into classes using[GraphQLField]marked methods instead of manyAddField()calls. Method parameters of the declaring type bind to the field context (the query context for root fields - this also applies to existing[GraphQLField]methods on the context type, which previously resolved the context from the service provider), other complex types resolve as services and remaining parameters become GraphQL arguments. Methods returningIQueryable<T>compose with EF - filters and selections translate to SQL. - โก๏ธ Updated dependencies:
Humanizer.Core2.14.1 -> 3.0.10 (used for singularizing names when building schemas - inflection rule changes could alter a generated name in rare cases),Parlot1.4.3 -> 1.5.7,System.Runtime.CachingandMicrosoft.Extensions.Logging.Abstractions9.0.9 -> 10.0.9. - ๐
ParameterReplacer.ReplaceByTypeis marked obsolete and will be removed in 7.0 - it replaces any parameter of the matching type, which can capture parameters you did not intend. It remains functional in 6.x. - ๐ Root-level list fields are now materialized asynchronously when the LINQ provider's query objects implement
IAsyncEnumerable<T>- EF Core's do, so the database round-trip no longer blocks a thread and the request'sCancellationTokencan cancel the in-flight query. No EF dependency was added; in-memory data is enumerated synchronously as before. Note forExecutionOptions.BeforeExecutingusers: root list fields on the database-bound pass no longer end in an in-treeToList()call - the deferred query is materialized after the expression executes. September 2025 GraphQL spec compliance (document descriptions, full Unicode and OneOf runtime behavior shipped in earlier betas):
__Type.isOneOfis now exposed per the spec (previously the non-standard nameoneField). It isfalsefor input object types,truefor@oneOftypes andnullfor other types.__Directive.isRepeatableadded to introspection (all built-in directives are non-repeatable). CustomIDirectiveProcessorimplementations can override the newIsRepeatableproperty (defaults tofalse).- Using a non-repeatable directive more than once at a single location (e.g.
@skip(...) @skip(...)) is now a validation error. @skip/@includeon the root field of a subscription operation is now a validation error per the spec.- Arguments and input fields can now be deprecated (
[Obsolete]on an argument property orfield.Deprecate(...)on an input field). They are hidden from introspection by default with newincludeDeprecatedarguments on__Field.args,__ Directive.argsand__Type.inputFields,__ InputValuegainsisDeprecated/deprecationReason, and the SDL output annotates them with@deprecated. Deprecating a required argument is a schema error per the spec. Implements()now validates field covariance โ a field on an implementing type must be the same type or a sub-type of the interface's field (only fields present at the time of the call are checked, so the add-fields-after-implements pattern is unaffected).- Field selection merging is now validated: selections with the same response name must be the same field with identical arguments (
{ x: id x: name }or{ project(id: 1) project(id: 2) }are errors โ use different aliases), and across mutually exclusive fragments the response shapes must be compatible (... on Cat { value: lives }with... on Dog { value: name }is an error).
__type(name: ...)now returnsnullfor an unknown type name per the GraphQL spec, instead of an execution error.Async result processing is now targeted and compiled instead of a full-result reflection walk: only branches of the result whose types can hold an async value are visited (non-async branches are returned untouched). On a 10,000-row result with one async field and non-async nested collections this cut request time from ~140ms to ~63ms in benchmarks (the no-async baseline being ~19ms). This also stops the walk reading every property of every result object, which could trigger EF lazy-loading of navigation properties the query never selected.
๐ Subscription events are no longer executed synchronously on the publisher's thread. Each subscription processes its events on an ordered background queue -
observer.OnNextreturns immediately instead of blocking while the (potentially async/service-backed) result selection executes for every subscriber. Event order per subscription is preserved, including the terminal error/complete messages.Policy evaluation is now truly async โ
PolicyOrRoleBasedAuthorizationpre-evaluates all policies used in the schema once at the start of each request (properly awaited) instead of blocking with sync-over-asyncAuthorizeAsynccalls per field/type check during compilation. New optionalIGqlAuthorizationService.PrepareForRequestAsynchook (has a default implementation โ custom implementations are unaffected).WebSocket hardening โ new
GraphQLWebSocketOptions(optional parameter onUseGraphQLWebSockets): incoming messages larger thanMaxMessageSizeBytes(default 1 MB) close the connection with1009 MessageTooBiginstead of buffering unbounded; malformed JSON closes with4400instead of throwing out of the receive loop; and clients that never sendconnection_initare closed with4408afterConnectionInitTimeout(default 10s) per the graphql-ws protocol.๐ Security (DoS) โ the GraphQL document parser now enforces a hard recursion-depth backstop of 128 (nested selection sets and nested argument/input values) during parsing, returning a
DocumentErrorbefore a maliciously deep document can exhaust the stack. This runs independently of, and before, the configurableExecutionOptions.MaxQueryDepth.๐ Hardening: the dynamic (projection) type cache now uses a full, collision-free key (field names, field types and parent type) instead of a 32-bit hash, removing an unlikely-but-possible hash collision handing a query a cached type with the wrong fields.
๐ Bug Fixes
- ๐ Fixed a compiled-delegate cache key collision when
CacheCompiledDelegatesis enabled and an operation has multiple root fields - the second root field was served the first field's compiled delegate, returning the wrong field's data. The cache key now includes the root field name. - ๐ Fixed async argument validators (
IArgumentValidatorviaAddValidator<T>()) being fire-and-forgotten โ a validator doing real async work before adding its errors raced the request and its errors could be silently lost. Validators are now stored as async delegates and genuinely awaited for mutation and subscription arguments. Note validators on query field arguments still...
- โฌ๏ธ Dropped the
Previous changes from v6.0.0-beta8
-
๐ New Features
โ Added the
UseAggregate()field extension for exposing aggregate data over a collection field โcountplusmin/max/sum/averageover its fields, computed over the whole collection and translated to a single SQL query under EF. The shape is function-first:{ count, min { fieldA }, max { fieldA }, sum { fieldA }, average { fieldA } }.min/maxapply to any orderable field (numbers, dates, strings);sum/averageto numerics (short,int,long,float,double,decimaland nullable forms). Aggregates over an empty set returnnull.- Placement is controlled by
AggregatePlacementand defaults toAuto: a paged field attaches anaggregatefield to the paging result (PagingWrapper), otherwise the field is wrapped as{ items, aggregate }(OwnWrapper), which handles every field uniformly including service-backed resolvers and custom arguments. An additive sibling{field}Aggregatefield (SiblingField) is available as an opt-in so aggregates can be added to an existing field without a breaking shape change; it carries the source field's arguments (filter and custom resolver arguments) but cannot be used on a service-backed collection resolver. - Pass a field selection to restrict the aggregatable fields, e.g.
UseAggregate((Person p) => new { p.Height, p.Age }).
- Placement is controlled by
A scalar field whose resolver reduces a collection using a service (e.g.
(db, svc) => db.People.Sum(p => svc.Score(p.Id))) now executes via the two-pass model โ the element values the service needs are projected and materialized in the first (DB) pass, and the reduction runs in memory in the second โ instead of failing to translate under EF.UseAggregatecan aggregate a service-backed element field (e.g.person.scoreresolved viaResolve<Service>(...)). The per-element service values are projected/materialized in the first pass and reduced in memory in the second, soaggregate { sum { score } }works under EF.Aggregating a bulk-resolved element field (
ResolveBulk) uses the bulk loader: the keys are projected/materialized in the first pass and all values are fetched in a single bulk call in the second, then reduced in memory โ so it doesn't fall back to one service call per element. (Sync, no-argument bulk resolvers; async/argument bulk resolvers use the per-element fallback.)
๐ Changes
- ๐ Paging performance:
hasNextPageno longer runs aCOUNT(*)query when the total isn't otherwise needed.UseOffsetPaging(whentotalItemsisn't selected) andUseConnectionPaging(forward paging with apageInfoselection of onlyhasNextPage/hasPreviousPage) answer it with a cheapEXISTSquery โ skip past the current page and check for any row. Selections needing the total (totalCount/totalItems,endCursor/startCursor, or thelast/beforearguments) behave as before. - Connection paging cursor assignment no longer evaluates the (dynamic) cursor arguments per row โ the cursor base is computed once per request (
ConnectionHelper.ApplyCursors) and each edge's cursor derived from it while enumerating.