Fix projections skipping fields inherited from interface types#9852
Fix projections skipping fields inherited from interface types#9852Tommsy64 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds coverage and fixes for projecting fields that are inherited via GraphQL interface bindings when default binding behavior is Explicit, ensuring member-based projection works for interface-declared members in both “classic” projections and QueryContext pipelines.
Changes:
- Added new unit tests (in-memory + EF Core) validating projection of interface-bound fields under explicit binding.
- Updated projection “pure member resolver” detection to treat interface-declared members as projectable on implementing runtime types.
- Updated comments to reflect the broadened projection rules around resolver members.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/HotChocolate/Data/test/Data.Tests/InterfaceFieldProjectionTests.cs | Adds in-memory tests for interface-bound field projection with explicit binding. |
| src/HotChocolate/Data/test/Data.EntityFramework.Tests/InterfaceFieldProjectionTests.cs | Adds EF Core tests for interface-bound field projection with explicit binding and sqlite seeding/cleanup. |
| src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionHandlerBase.cs | Allows projecting members whose resolver member is declared on an interface implemented by the runtime type. |
| src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs | Aligns “pure resolver” detection with interface-inherited members using IsAssignableFrom. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| public class InterfaceFieldProjectionTests : IDisposable | ||
| { | ||
| private readonly string _fileName = Guid.NewGuid().ToString("N") + ".db"; |
|
|
||
| var executor = await new ServiceCollection() | ||
| .AddDbContext<CompanyDbContext>( | ||
| b => b.UseSqlite("Data Source=" + _fileName)) |
| private async Task SeedAsync() | ||
| { | ||
| var options = new DbContextOptionsBuilder<CompanyDbContext>() | ||
| .UseSqlite("Data Source=" + _fileName) |
| if (File.Exists(_fileName)) | ||
| { | ||
| File.Delete(_fileName); |
| // Fields inherited from an interface carry the interface member as their resolver | ||
| // member; the runtime type implements that interface, so the member can be projected. | ||
| if (resolverMember.ReflectedType?.IsAssignableFrom( | ||
| selection.Field.DeclaringType.RuntimeType) == true) | ||
| { | ||
| return true; | ||
| } |
| // declared on the parent runtime type or on an interface it implements) or if it | ||
| // explicitly replaces that member (fluent ResolveWith / [BindMember]). | ||
| var isPureMemberResolver = field.PureResolver is not null | ||
| && field.ResolverMember?.ReflectedType == field.DeclaringType.RuntimeType; | ||
| && field.ResolverMember?.ReflectedType?.IsAssignableFrom( | ||
| field.DeclaringType.RuntimeType) == true; |
There was a problem hiding this comment.
Good point. I switched to ResolverMember.DeclaringType, but I'm keeping the ReflectedType == RuntimeType equality above it because it's a common case.
Fixes #9848 and adds associated tests (in-memory, and also with EntityFramework).
Root Cause
Fields inherited from an interface carry the interface member as their resolver member (e.g.
IPerson.Id, notEmployee.Id). The projectability checks compare the resolver member's reflected type to the parent runtime type with strict equality, so interface-declared members are rejected and silently dropped from theSelect, returning default values.The check exists in two places:
UseProjection— introduced in Fix #5072 projection for unbound extension resolvers #9224 (here).This is the v15 → v16 regression reported in EF Projections don't work when using interface types #9848.
QueryContext/AsSelector— this check has existed since DataLoader projections were introduced in Added DataLoader Projections #7389, and was later reshaped in Fix projection of underlying ResolveWith member #9742 (current form). Not a regression, but the same limitation.Fix
Accept resolver members whose reflected type is assignable from the runtime type, in both the classic
UseProjectionhandlers and theQueryContextselector builder. Unbound extension resolvers (#5072) remain excluded, since their declaring class is not assignable from the entity type.