Skip to content

Fix projections skipping fields inherited from interface types#9852

Open
Tommsy64 wants to merge 2 commits into
ChilliCream:mainfrom
Tommsy64:fix/9848-interface-field-projection
Open

Fix projections skipping fields inherited from interface types#9852
Tommsy64 wants to merge 2 commits into
ChilliCream:mainfrom
Tommsy64:fix/9848-interface-field-projection

Conversation

@Tommsy64

@Tommsy64 Tommsy64 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

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, not Employee.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 the Select, returning default values.

The check exists in two places:

Fix

Accept resolver members whose reflected type is assignable from the runtime type, in both the classic UseProjection handlers and the QueryContext selector builder. Unbound extension resolvers (#5072) remain excluded, since their declaring class is not assignable from the entity type.

Copilot AI review requested due to automatic review settings June 4, 2026 20:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Comment on lines +140 to +142
if (File.Exists(_fileName))
{
File.Delete(_fileName);
Comment on lines +36 to +42
// 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;
}
Comment on lines +305 to +309
// 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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I switched to ResolverMember.DeclaringType, but I'm keeping the ReflectedType == RuntimeType equality above it because it's a common case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EF Projections don't work when using interface types

2 participants