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.1 target. EntityGraphQL now targets net8.0, net9.0 and net10.0. This also removes the System.Text.Json, Microsoft.CSharp and System.ComponentModel.Annotations package 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, and IsAuthorized requires Identity.IsAuthenticated. In EntityGraphQL.AspNet, a policy-protected field is now denied (rather than allowed through) when no IAuthorizationService is registered or the user is null. 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.MaxQueryConcurrency now 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). Set MaxQueryConcurrency = null to restore unlimited. Note this does not make non-thread-safe services (e.g. a scoped DbContext) safe in async list fields โ€” use ServiceConcurrencyLimits or maxConcurrency: 1 for those. Also fixed the concurrency wrapper for field shapes it previously failed on (value-type service parameters such as CancellationToken, rebound per-element parameters, nested list results โ€” errors like unbound variable or No coercion operator), and IAsyncEnumerable fields are no longer wrapped (the stream's enumeration, not its creation, is the async work).
    • API surface โ€” LinqRuntimeTypeBuilder and ExpressionReplacer are now internal; ExpressionUtil.ListToSingleMethods is no longer public (it was a mutable static). GraphQLHelper.InjectServices gained optional parameters and accepts a null service provider. ConcurrencyLimitFieldExtension.ExecuteWithConcurrencyLimitAsync is 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/@include on 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 the schema.AddQueryFieldsFrom<T>() shortcut - group related field definitions into classes using [GraphQLField] marked methods instead of many AddField() 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 returning IQueryable<T> compose with EF - filters and selections translate to SQL.
    • โšก๏ธ Updated dependencies: Humanizer.Core 2.14.1 -> 3.0.10 (used for singularizing names when building schemas - inflection rule changes could alter a generated name in rare cases), Parlot 1.4.3 -> 1.5.7, System.Runtime.Caching and Microsoft.Extensions.Logging.Abstractions 9.0.9 -> 10.0.9.
    • ๐Ÿšš ParameterReplacer.ReplaceByType is 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's CancellationToken can cancel the in-flight query. No EF dependency was added; in-memory data is enumerated synchronously as before. Note for ExecutionOptions.BeforeExecuting users: root list fields on the database-bound pass no longer end in an in-tree ToList() 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.isOneOf is now exposed per the spec (previously the non-standard name oneField). It is false for input object types, true for @oneOf types and null for other types.
      • __Directive.isRepeatable added to introspection (all built-in directives are non-repeatable). Custom IDirectiveProcessor implementations can override the new IsRepeatable property (defaults to false).
      • Using a non-repeatable directive more than once at a single location (e.g. @skip(...) @skip(...)) is now a validation error.
      • @skip/@include on 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 or field.Deprecate(...) on an input field). They are hidden from introspection by default with new includeDeprecated arguments on __Field.args, __ Directive.args and __Type.inputFields, __ InputValue gains isDeprecated/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 returns null for 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.OnNext returns 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 โ€” PolicyOrRoleBasedAuthorization pre-evaluates all policies used in the schema once at the start of each request (properly awaited) instead of blocking with sync-over-async AuthorizeAsync calls per field/type check during compilation. New optional IGqlAuthorizationService.PrepareForRequestAsync hook (has a default implementation โ€” custom implementations are unaffected).

    • WebSocket hardening โ€” new GraphQLWebSocketOptions (optional parameter on UseGraphQLWebSockets): incoming messages larger than MaxMessageSizeBytes (default 1 MB) close the connection with 1009 MessageTooBig instead of buffering unbounded; malformed JSON closes with 4400 instead of throwing out of the receive loop; and clients that never send connection_init are closed with 4408 after ConnectionInitTimeout (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 DocumentError before a maliciously deep document can exhaust the stack. This runs independently of, and before, the configurable ExecutionOptions.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 CacheCompiledDelegates is 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 (IArgumentValidator via AddValidator<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...

Previous changes from v6.0.0-beta8

  • ๐Ÿ†• New Features

    • โž• Added the UseAggregate() field extension for exposing aggregate data over a collection field โ€” count plus min/max/sum/average over 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/max apply to any orderable field (numbers, dates, strings); sum/average to numerics (short, int, long, float, double, decimal and nullable forms). Aggregates over an empty set return null.

      • Placement is controlled by AggregatePlacement and defaults to Auto: a paged field attaches an aggregate field 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}Aggregate field (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 }).
    • 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.

    • UseAggregate can aggregate a service-backed element field (e.g. person.score resolved via Resolve<Service>(...)). The per-element service values are projected/materialized in the first pass and reduced in memory in the second, so aggregate { 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: hasNextPage no longer runs a COUNT(*) query when the total isn't otherwise needed. UseOffsetPaging (when totalItems isn't selected) and UseConnectionPaging (forward paging with a pageInfo selection of only hasNextPage/hasPreviousPage) answer it with a cheap EXISTS query โ€” skip past the current page and check for any row. Selections needing the total (totalCount/totalItems, endCursor/startCursor, or the last/before arguments) 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.