Skip to content

Releases: EntityGraphQL/EntityGraphQL

6.2.4

Choose a tag to compare

@lukemurray lukemurray released this 08 Sep 17:45
dd0d510

Fixes

  • Fixed No generic method 'SelectWithNullCheck' for a sub-selection on a field returning IAsyncEnumerable<T> or ValueTask<TCollection> - { people { tags { name } } } where tags has a ResolveAsync returning either. MakeSelectWithDynamicType emits a SelectWithNullCheck whose overload is resolved by the exact type of the expression it projects, and only IEnumerable<T> and Task<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's CancellationToken rather than being enumerated during compilation, and ValueTask<T> is handed on as the Task<T> the rest of the pipeline already handles, next to the existing Task<TCollection> normalization.
  • Fixed Object of type 'Dynamic_...' cannot be converted to type 'Dynamic_...' when buffering an IAsyncEnumerable<T> whose items are rebuilt - the list rebuild mismatch below, one layer down. BufferAsyncEnumerable created a List<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 the IEnumerable path already did - both paths now share that step. The same guard added above for Task<T> and ValueTask<T> in GetResolvedFieldType applies to IAsyncEnumerable<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 } } } where tags has a ResolveAsync returning a list and label on 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 to List<object>. GetResolvedFieldType unwrapped Task<T> to T unconditionally, so the rebuilt parent still declared the member IEnumerable<T> and setting the resolved list on it threw. T is 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

Choose a tag to compare

@lukemurray lukemurray released this 08 Sep 17:45
991f180

Fixes

  • Fixed An item with the same key has already been added when 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)). ExpressionExtractor credited both member reads to the enclosing construction rather than to themselves, so it emitted the same Expression instance twice under the one name and ExpressionReplacer added 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. ExpressionReplacer also 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

Choose a tag to compare

@lukemurray lukemurray released this 03 Sep 02:20

Fixes

  • Fixed a field whose dotnet type is a nullable value type (Instant?, DateTime?, int?) resolving to default(T) instead of null, 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

Choose a tag to compare

@lukemurray lukemurray released this 18 Aug 11:52

Fixes

  • Fixed IsNullable() (and any other change to a field's return type) on a field whose dotnet type has an AddTypeMapping leaking to every other field of that type. The mapping's GqlTypeInfo was handed out as the field's own ReturnType, and that object is mutable - IsNullable() writes to it - so one AddField(...).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 explicit IsNullable() after AddField() applies to that field only and the mapping stays the default it describes.

6.2.0

Choose a tag to compare

@lukemurray lukemurray released this 18 Aug 10:48
85c372f

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 ResolveBulk when the data comes from another database or service. Take an IFieldSelection parameter and the engine supplies it, the way it supplies CancellationToken and QueryRequestContext; new two-service ResolveBulk/ResolveBulkAsync overloads 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/@include are applied, fragments are expanded, aliases collapse, __typename is 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/ResolveBulkAsync resolver that has to resolve per item instead of bulk loading now logs a warning to the ILogger the 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 with ExecutionOptions.WarnOnBulkResolverFallback = false. ISchemaProvider.Logger is new (exceptions keep going through LogException); 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-wide ExecutionOptions.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.FieldName names the field for report-only mode. See Query limits.
  • ToGraphQLSchemaString() takes an optional includeDescriptions argument. Pass false to leave the """...""" descriptions out of the generated SDL.

Fixes

  • Fixed an IArgumentsTracker parameter on a mutation or subscription method failing with Service IArgumentsTracker not found for dependency injection whenever a service provider is passed to ExecuteRequest - 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). Registering IArgumentsTracker in DI was not a workaround: it bound a different, empty tracker, so IsSet returned false for every argument and the mutation silently took the "nothing was supplied" path. It is now bound before the DI branch, the same as CancellationToken.
  • An IArgumentsTracker parameter now works on query fields built from methods ([GraphQLField] methods, AddFieldsFrom), not just mutations - it previously fell through to the service provider and failed with Service IArgumentsTracker not found in service provider. The field's arguments object is built deriving from ArgumentsTracker when 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 null on a query field is now reported as set by IArgumentsTracker - previously only a non-null value or a schema default counted, so IsSet could 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 from ArgumentsTracker. Mutation arguments already behaved this way.
  • AddTypeMapping now applies to every field returning the mapped dotnet type, not only the ones auto-created from the context. A mapping like AddTypeMapping<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's GqlTypeInfo. Everything else went through SchemaBuilder.MakeGraphQlType, which resolved the type name (Point) and then recomputed IsList/TypeNotNullable from the dotnet type, so AddField(), AddField().Resolve(), root fields on Query(), [GraphQLField] methods and expression fields all described the field as Point instead 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 an async [GraphQLField] method now matches the mapping too - the lookup used to be made with the declared Task<T> return type. Type mappings still need to be registered in SchemaBuilderOptions.PreBuildSchemaFromContext so they exist before the context is reflected.

6.1.7

Choose a tag to compare

@lukemurray lukemurray released this 06 Aug 05:00

Fixes

  • Fixed a single-object field with a ResolveBulk resolver whose per-item resolver queries the schema context - e.g. .Resolve<MyContext>((p, ctx) => ctx.Site.Movies.FirstOrDefault(m => m.DirectorId == p.Id)) - failing with Could 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))) with UseOffsetPaging()/UseConnectionPaging() on a nested type - failing with Could 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

Choose a tag to compare

@lukemurray lukemurray released this 04 Aug 03:32

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 occurred while the original exception - its own message and stack trace - reaches the ILogger. 6.1.5's test ran with the default IsDevelopment = true, where nothing is swallowed, so it did not check that.

6.1.5

Choose a tag to compare

@lukemurray lukemurray released this 03 Aug 23:46
dbf53cd

Fixes

  • Field-level exceptions are logged again, with the field name and stack trace, to the ILogger the 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 or AllowedExceptions - 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

Choose a tag to compare

@lukemurray lukemurray released this 03 Aug 00:57
fb469a2

Fixes

  • Fixed ResolveBulk / ResolveBulkAsync nested under a root service-resolved list (e.g. Query.apiKeys from .Resolve<TService>(...) rather than a context/DbSet property) so nested bulk fields load once instead of falling back to per-item Resolve (or previously failing with a null BulkParameter). 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 in ServiceRootListBulkTests / 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 a BeforeExecuting hook sees two executions for them (isFinal false then true) where it saw one.
  • Fixed ResolveBulk nested under a root service-resolved object that exposes a list (status-page shape: statusPage { items { bulkField } }). First pass used to return only ExtractedFieldsFromServices for root service objects, so nested list selection and bulk registration were skipped; the second pass then either N+1'd via Resolve or failed converting entity types to first-pass Dynamic_* types (No coercion operator...). Root service objects now participate in the two-pass flow (like root service lists), wrap once with ProjectWithNullCheck on the first pass, and on the second pass select from the materialized anon instead of re-invoking the service. Also rebinds the ResolveBulk DataSelector parameter (a different ParameterExpression than FieldParam) so multi-property keys no longer fail with variable 'row' ... is not defined. Nullable-nav bulk keys (row.DetexyBoard == null ? null : row.DetexyBoard.SerialNumber) are rewritten via ExpressionReplacer.VisitConditional onto the first-pass extracted field instead of leaving a second navigation MemberExpression unbound. Exact regression: HardwareSensorStatusPageBulkTests (mirrors Offline Active hardwareSensorStatusPage { items { lastSeen isOffline floor { currentFloorStatus } } }). Also: ServiceResolvedPage_ItemsWithResolveBulk_Works, ServiceResolvedPage_ItemsWithComplexResolveBulkKey_Works.

6.1.3

Choose a tag to compare

@lukemurray lukemurray released this 30 Jul 06:58

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, or ProjectWithNullCheck for a single object), which no database provider can translate. Selecting another such field below it failed the operation with The LINQ expression 'p_Thing => new Dynamic_things{...}' could not be translated. Lists keep a plain Select (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 like movie(id: 1). Fields using a service other than the query context still get the null check, as they can return null.