Releases: EntityGraphQL/EntityGraphQL
Releases · EntityGraphQL/EntityGraphQL
Release list
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.
6.1.3
Fixes
- Fixed a field resolved from the query context - e.g.
.Resolve<MyDbContext>((c, db) => db.Things.Where(t => t.ParentId == c.Id)), the pattern for a child collection that is not a navigation property - being projected with one of EntityGraphQL's own methods (SelectWithNullCheck, orProjectWithNullCheckfor a single object), which no database provider can translate. Selecting another such field below it failed the operation withThe LINQ expression 'p_Thing => new Dynamic_things{...}' could not be translated. Lists keep a plainSelect(a regression from 6.0.0-beta3), and a single object is projected inside the query -Where().Select().FirstOrDefault(), as it already was for a field likemovie(id: 1). Fields using a service other than the query context still get the null check, as they can returnnull.