Repository navigation
Releases: EntityGraphQL/EntityGraphQL
Releases · EntityGraphQL/EntityGraphQL
Release list
6.3.0
Changes
ToGraphQLSchemaStringAsync(ClaimsPrincipal? user)(andToGraphQLSchemaString(QueryRequestContext? requestContext)) output SDL containing only what that user may access, using the schema'sAuthorizationServiceas query execution does. The result is self-consistent: a type the filtering leaves empty is hidden along with the fields returning it, a field is hidden when an argument's type is hidden, hidden interfaces and union members are dropped fromimplementsand union lists, and a directive with an argument of a hidden type is hidden. Useful for handing a schema to a user or AI agent (e.g. an MCP server tool) where SDL is more compact than introspection.ToGraphQLSchemaString()without a user still outputs the full schema. Both are default interface methods onISchemaProvider, so custom implementations do not break.- Introspection for a user now hides the same things the SDL does. Previously a type with no type-level authorization but every field protected was still listed (with
fields: []), a field taking an argument of a protected enum or input type was listed, protected interfaces still appeared ininterfaces, and a directive with an argument of a protected type was listed. These are now hidden, so a client building a schema from introspection gets a valid one. Introspection without a user is unchanged. [Required]on an argument (a property/field of an arguments object or a method parameter) now behaves likeArgumentHelper.Required<T>()- the argument has no default value and must be provided by the query. Previously a value type kept its implicitdefault(T)as the schema default, so[Required] int Takeprintedtake: Int! = 0(optional for clients) and a query leaving it out ran with0. It is nowtake: Int!and leaving it out is amissing required argumenterror. Remove[Required]if you relied on the old default.- Introspection
__InputValue.defaultValueno longer strips the quotes off string-like defaults. The spec defines it as the GraphQL literal, so a string default ofhellois now returned as"hello"- the same text as in the SDL. Clients building a schema from introspection (buildClientSchema, GraphiQL, codegen against an endpoint) previously dropped such defaults or failed to parse them (Guid, strings with spaces). If you readdefaultValuedirectly as text rather than parsing it as GraphQL, string defaults now include their quotes. - Added
IFieldExtension.ProcessExpressionPostSelection(a default interface method, and virtual onBaseFieldExtension), called with a list field's projected result so an extension can wrap it. ConnectionEdgeExtension(Type listType, bool isQueryable)is obsolete -isQueryableis not used. UseConnectionEdgeExtension(Type listType). It will be removed in 7.0.
Fixes
- #562 - Fixed invalid SDL for argument default values of value types.
Guid,DateTime,DateTimeOffset,DateOnly,TimeOnly,TimeSpanandchardefaults were printed bare (runId: ID! = 00000000-0000-0000-0000-000000000000), which tools like GraphQL Code Generator reject. They are now quoted string literals (dates in ISO format). Numeric defaults are printed with the invariant culture (3.14, not3,14underde-DE) and string defaults are escaped (",\, newlines). - Fixed
UseOffsetPaging()on a field that is not on the root query type when its collection comes from EF - e.g.schema.Type<Actor>().ReplaceField("movies", new { }, (a, _) => a.Movies.OrderBy(m => m.Released)).UseOffsetPaging()- failing withThe LINQ expression 'p_Movie => new Dynamic_items...' could not be translated. The collection is part of the parent's projection, so EF has to translate the paging, anditemswas paged with EntityGraphQL's ownSkip/Take(int?)helpers, which EF does not know. A collection that is not anIQueryableis now paged withSystem.Linq'sSkip/Take(a nullskipis0, a nulltakeis the rest of the collection -int.MaxValue - skip, as EF pages a nested collection withrow <= skip + take, which would overflow aninton SQL Server and Postgres).hasNextPageon such a field is translated too: EF evaluated EntityGraphQL'sPageHasNexthelper in memory, loading the whole collection for every parent, and it is now anEXISTSquery. Paging on root query fields was not affected. - #574 - Fixed
UseConnectionPaging()on a field that is not on the root query type when its collection comes from EF - e.g.schema.Type<Actor>().ReplaceField("movies", a => a.Movies.OrderByDescending(m => m.Released)).UseConnectionPaging()- failing withThe LINQ expression 'edgeNode => ...' could not be translated. As with offset paging above, the edges are now paged withSystem.Linq'sSkip/Take. Cursors were assigned byConnectionHelper.ApplyCursorswhile enumerating the page, which EF can not translate inside a parent's projection, so for such a collection they are now set on the edges after EF materializes them.last(withoutbefore) on such a collection pages from the end of each parent's own collection - it relied on one total count shared by the whole request - and is taken asReverse().Take(last)so EF can translate it. Withaftertoo, the rows up toafterare then dropped from that page.lastwith or withoutbeforeon such a collection needs it to be ordered (e.g.a.Movies.OrderBy(m => m.Released)) when it comes from EF, which can notReverse()an unordered collection - it fails with EF'scould not be translatederror. An in-memory collection does not need to be ordered.pageInfo { hasNextPage }alone is anEXISTSquery rather than loading the whole collection. Paging on root query fields is unchanged. - Fixed a connection field paging a collection of the same type at the root (
IQueryable) and on a parent object sharing oneConnectionEdgeExtension, which used whether the first configured field wasIQueryablefor both (No generic method 'Skip' on type 'QueryableExtensions'). It is now decided per use. - Fixed connection paging
pageInfo.hasPreviousPagebeingfalseforlastwithoutbeforewhen exactly one item comes before the page (e.g.last: 2of 3 items). - Fixed connection paging with
lastand abeforecursor past the end of the collection - e.g.last: 3, before: <cursor 5>of 3 items - returning too few items (2and3rather than all 3), as the page was taken as ending at the cursor. It is now the last items of the collection, with matching cursors andpageInfo. On a nested collection the cursor is shared by every parent and can be past the end of only some of them: each parent's page is chosen from its own count, at the cost of also selecting the lastlastitems of each parent alongside the page. - Fixed connection paging
pageInfo.hasNextPagebeingfalseforbeforewhen the cursor is the last item (e.g.last: 3, before: <cursor 6>of 6 items), although the item at the cursor comes after the page. - A connection's
pageInfois no longer null checked - it can't be null. The check repeated the whole connection expression, so EF translated it twice -pageInfo { hasNextPage }on a nested connection ran itsEXISTSsubquery twice per parent.
6.2.4
Fixes
- Fixed
No generic method 'SelectWithNullCheck'for a sub-selection on a field returningIAsyncEnumerable<T>orValueTask<TCollection>-{ people { tags { name } } }wheretagshas aResolveAsyncreturning either.MakeSelectWithDynamicTypeemits aSelectWithNullCheckwhose overload is resolved by the exact type of the expression it projects, and onlyIEnumerable<T>andTask<IEnumerable<T>>had one.IAsyncEnumerable<T>now has an overload of its own that projects lazily, so the stream is still buffered by the engine with the request'sCancellationTokenrather than being enumerated during compilation, andValueTask<T>is handed on as theTask<T>the rest of the pipeline already handles, next to the existingTask<TCollection>normalization. - Fixed
Object of type 'Dynamic_...' cannot be converted to type 'Dynamic_...'when buffering anIAsyncEnumerable<T>whose items are rebuilt - the list rebuild mismatch below, one layer down.BufferAsyncEnumerablecreated aList<T>from the declared element type before resolving anything and added each resolved item to it, so an item rebuilt to carry an awaited member no longer fit. Items are now resolved first and the list type chosen from them, which is what theIEnumerablepath already did - both paths now share that step. The same guard added above forTask<T>andValueTask<T>inGetResolvedFieldTypeapplies toIAsyncEnumerable<T>, which is reachable now that these shapes compile. - Fixed
Object of type 'System.Collections.Generic.List1[System.Object]' cannot be converted to type 'System.Collections.Generic.IEnumerable1[Dynamic_...]'for a query selecting an async service field below an async service list field -{ people { tags { name label } } }wheretagshas aResolveAsyncreturning a list andlabelon the item type has one of its own. Resolving an item of the outer list rebuilds it, because the item projection holds an async member, so the finished list no longer holds the item type the outer field was declared with and correctly falls back toList<object>.GetResolvedFieldTypeunwrappedTask<T>toTunconditionally, so the rebuilt parent still declared the memberIEnumerable<T>and setting the resolved list on it threw.Tis now only kept when the resolved value still is one, otherwise the resolved value's own type is used - which is what the non-async path in the same method already did.ValueTask<T>had the same hole and takes the same guard.
6.2.3
Fixes
- Fixed
An item with the same key has already been addedwhen a service field's resolver builds an object from more than one context member as an argument to the service call -(ctx, srv) => srv.Get(new Key(ctx.A, ctx.B)).ExpressionExtractorcredited both member reads to the enclosing construction rather than to themselves, so it emitted the sameExpressioninstance twice under the one name andExpressionReplaceradded each of them to a dictionary keyed by node identity. A construction is now walked through rather than treated as a leaf, so each read is extracted on its own.ExpressionReplaceralso tolerates a repeated expression, which the extractor still emits for other shapes (a conditional argument, where the branch reads are both credited to the conditional).
6.2.2
Fixes
- Fixed a field whose dotnet type is a nullable value type (
Instant?,DateTime?,int?) resolving todefault(T)instead ofnull, for a query that also selects an async field. Results are rebuilt after any async field is awaited, and each member's new type was taken from the resolved value's runtime type.
6.2.1
Fixes
- Fixed
IsNullable()(and any other change to a field's return type) on a field whose dotnet type has anAddTypeMappingleaking to every other field of that type. The mapping'sGqlTypeInfowas handed out as the field's ownReturnType, and that object is mutable -IsNullable()writes to it - so oneAddField(...).IsNullable(true)rewrote the registered mapping itself, changing every field already using it and every field added afterwards. Fields now get a copy, so an explicitIsNullable()afterAddField()applies to that field only and the mapping stays the default it describes.
6.2.0
Changes
- A resolver can now be told what the engine will read off the objects it returns, so it can fetch only that instead of everything - the point of batching with
ResolveBulkwhen the data comes from another database or service. Take anIFieldSelectionparameter and the engine supplies it, the way it suppliesCancellationTokenandQueryRequestContext; new two-serviceResolveBulk/ResolveBulkAsyncoverloads let a loader take it alongside its own service. It is not the caller's selection set: a selected field that is itself resolved from a service is replaced by the member its resolver reads, nested objects come through as paths (Address.City),@skip/@includeare applied, fragments are expanded, aliases collapse,__typenameis excluded, and one load is told the union of every place the field is selected. See Fetching only the fields that will be used. - A field with a
ResolveBulk/ResolveBulkAsyncresolver that has to resolve per item instead of bulk loading now logs a warning to theILoggerthe schema was built with, naming the field and why the bulk load could not run - the query shape usually controls it, so it is something a caller can act on. Turn it off withExecutionOptions.WarnOnBulkResolverFallback = false.ISchemaProvider.Loggeris new (exceptions keep going throughLogException); it returns null by default so existing implementations are unaffected. - New
field.SetMaxAliases(n)caps how many times a single field may be aliased in one operation, alongside the document-wideExecutionOptions.MaxFieldAliases. Useful for the few fields a batched-alias attack would target (a login mutation, an expensive report) without tightening the limit for everything. Counts aliased selections of that field, fragment contents included.QueryLimitExceededContext.FieldNamenames the field for report-only mode. See Query limits. ToGraphQLSchemaString()takes an optionalincludeDescriptionsargument. Passfalseto leave the"""..."""descriptions out of the generated SDL.
Fixes
- Fixed an
IArgumentsTrackerparameter on a mutation or subscription method failing withService IArgumentsTracker not found for dependency injectionwhenever a service provider is passed toExecuteRequest- i.e. in any real app. The engine builds and populates the tracker per call, but the branch binding it to the parameter sat below the dependency-injection catch-all, so it was only reachable with a null service provider (which is how the existing tests exercised it). RegisteringIArgumentsTrackerin DI was not a workaround: it bound a different, empty tracker, soIsSetreturned false for every argument and the mutation silently took the "nothing was supplied" path. It is now bound before the DI branch, the same asCancellationToken. - An
IArgumentsTrackerparameter now works on query fields built from methods ([GraphQLField]methods,AddFieldsFrom), not just mutations - it previously fell through to the service provider and failed withService IArgumentsTracker not found in service provider. The field's arguments object is built deriving fromArgumentsTrackerwhen a parameter asks for one, and the parameter binds to that object, so the tracker is not a service and a field that has no other services stays on the database-bound pass. - An argument explicitly supplied as
nullon a query field is now reported as set byIArgumentsTracker- previously only a non-null value or a schema default counted, soIsSetcould not tell an explicit null from an omitted argument, which is the distinction it exists to make. This also applies to[GraphQLArguments]/ input types deriving fromArgumentsTracker. Mutation arguments already behaved this way. AddTypeMappingnow applies to every field returning the mapped dotnet type, not only the ones auto-created from the context. A mapping likeAddTypeMapping<NpgsqlPolygon>("[Point!]!")describes the whole GraphQL type - list-ness and nullability come from the mapping string, not from the dotnet type - but only the auto-populated path returned the mapping'sGqlTypeInfo. Everything else went throughSchemaBuilder.MakeGraphQlType, which resolved the type name (Point) and then recomputedIsList/TypeNotNullablefrom the dotnet type, soAddField(),AddField().Resolve(), root fields onQuery(),[GraphQLField]methods and expression fields all described the field asPointinstead of[Point!]!. Introspection, the SDL and any client generated from them were wrong; execution is unchanged. Two notes: the[GraphQLField]method case is a regression in 6.0 (the other paths never honoured the mapping), and anasync[GraphQLField]method now matches the mapping too - the lookup used to be made with the declaredTask<T>return type. Type mappings still need to be registered inSchemaBuilderOptions.PreBuildSchemaFromContextso they exist before the context is reflected.
6.1.7
Fixes
- Fixed a single-object field with a
ResolveBulkresolver whose per-item resolver queries the schema context - e.g..Resolve<MyContext>((p, ctx) => ctx.Site.Movies.FirstOrDefault(m => m.DirectorId == p.Id))- failing withCould not find extension method Select on types System.Linq.Enumerable. A regression in 6.1.3: such a field was moved onto the collection selection path (to keep it translatable), but a bulk resolver's value on that pass is the loaded dictionary's entry, a single object, so there is no collection to select through. Bulk resolved fields stay on the single-object projection. Reported in #543 - thanks @soilidokay for the reproduction. - Fixed a service field selected on the items of a paging field whose own resolver uses a service - e.g.
.Resolve<MyService>((p, srv) => p.Tasks.Where(t => srv.Include(t)))withUseOffsetPaging()/UseConnectionPaging()on a nested type - failing withCould not find field egql__x_Id on type X. Such a paging field cannot be split across the two passes so it is built in one go on the services pass, from the entity rather than from a first-pass projection; a service field on its items had nowhere to read its own extracted dependency from. It now reads that dependency straight off the entity.
6.1.6
Fixes
- The field-error logging added in 6.1.5 is now covered for the case it exists for: outside development mode the caller gets only
Field 'x' - Error occurredwhile the original exception - its own message and stack trace - reaches theILogger. 6.1.5's test ran with the defaultIsDevelopment = true, where nothing is swallowed, so it did not check that.
6.1.5
Fixes
- Field-level exceptions are logged again, with the field name and stack trace, to the
ILoggerthe schema was built with. A regression in 6.0: partial results turned a field failure into a GraphQL error instead of rethrowing it, so nothing reached the request-level log and the only way to see why a field failed was development mode orAllowedExceptions- both of which return the detail to the caller. Responses are unchanged. Document and validation errors, whose message the caller already gets in full, are not logged.
6.1.4
Fixes
- Fixed
ResolveBulk/ResolveBulkAsyncnested under a root service-resolved list (e.g.Query.apiKeysfrom.Resolve<TService>(...)rather than a context/DbSetproperty) so nested bulk fields load once instead of falling back to per-itemResolve(or previously failing with a nullBulkParameter). Root service list fields now participate in the two-pass flow.UseFilter()/UseSort()on those root lists still apply on the first pass and are not re-applied against the first-pass Dynamic on the second. Regression tests inServiceRootListBulkTests/ServiceBackedCollectionExtensionsTests. A root service list/object only takes the two-pass path when the selection actually has a bulk resolver in it (including through a fragment spread) - otherwise it stays on the single-pass path, as the extra pass costs a compile and a projection of every row for nothing. Note that for the fields that do take it, the service now runs during the first pass, so aBeforeExecutinghook sees two executions for them (isFinalfalse then true) where it saw one. - Fixed
ResolveBulknested under a root service-resolved object that exposes a list (status-page shape:statusPage { items { bulkField } }). First pass used to return onlyExtractedFieldsFromServicesfor root service objects, so nested list selection and bulk registration were skipped; the second pass then either N+1'd viaResolveor failed converting entity types to first-passDynamic_*types (No coercion operator...). Root service objects now participate in the two-pass flow (like root service lists), wrap once withProjectWithNullCheckon the first pass, and on the second pass select from the materialized anon instead of re-invoking the service. Also rebinds theResolveBulkDataSelectorparameter (a differentParameterExpressionthanFieldParam) so multi-property keys no longer fail withvariable 'row' ... is not defined. Nullable-nav bulk keys (row.DetexyBoard == null ? null : row.DetexyBoard.SerialNumber) are rewritten viaExpressionReplacer.VisitConditionalonto the first-pass extracted field instead of leaving a second navigationMemberExpressionunbound. Exact regression:HardwareSensorStatusPageBulkTests(mirrors Offline ActivehardwareSensorStatusPage { items { lastSeen isOffline floor { currentFloorStatus } } }). Also:ServiceResolvedPage_ItemsWithResolveBulk_Works,ServiceResolvedPage_ItemsWithComplexResolveBulkKey_Works.