Skip to content

Project abstract types instead of falling back to includes - #1437

Merged
SimonCropp merged 2 commits into
mainfrom
project-abstract-types
Oct 8, 2026
Merged

SimonCropp merged 2 commits into
mainfrom
project-abstract-types

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

Problem

An abstract type cannot be created by a member init, so a query for an abstract type, or a navigation to one, fell back to Include. That loads every column of the entity and of every entity under it, whatever the request selected, and builds include chains as deep as the selection:

.Include("MinisterialRoleOwner.MinisterialRole.Ministers.Member.AbbreviationView")

Change

Abstract types are projected

No row is ever of the abstract type itself: each is one of its concrete derived types. So an abstract type is now projected as the chain of type tests already used for a concrete type with derived types, ending in null instead of a member init of its own.

-- before
select b.Id, b.Discriminator, b.Property, b.Status
-- after
select case when b.Discriminator = N'DerivedWithNavigationEntity' then cast (1 as bit) else cast (0 as bit) end,
       b.Id,
       b.Property,
       case when b.Discriminator = N'DerivedEntity' then cast (1 as bit) else cast (0 as bit) end

An abstract type with no concrete derived type in the model still falls back, as does one where a derived type has a selected property a select cannot bind.

Navigations on a type with derived types are bound once

Each type test creates its own member init, so a navigation bound in them was repeated once per concrete type. EF does not recognise a repeated collection subquery as the same one, so the collection was joined, or split out, once for every type. A hierarchy under a hierarchy also multiplied the size of the expression.

They are now bound once, to whichever type the chain created, through HoistedNavigation.Set, a call EF evaluates on the client as it does the member inits. This applies to a concrete type with derived types too, which had the same repetition.

Foreign keys of derived types

A query for a base type now loads the foreign keys its derived types declare, as it loads the base type's own. They used to arrive only because the whole entity did.

Connection count

The count query no longer carries the select projection. EF drops it from the SQL, but only after compiling the whole projection, and every collection in it, as part of the count.

Behaviour change

A resolver on an abstract type that reads a property of its source without declaring a projection used to work, because the whole entity was loaded. It now sees a default value, with no exception, exactly as it would on a concrete type. It needs a projection:

// reads ReturningOn and Status, neither of which is a key
Field<NonNullGraphType<BooleanGraphType>>("isExpired")
    .Resolve(context => RequestExpiry.IsExpired(context.Source));

Tests

  • Abstract_root_type_query_is_projected and Navigation_to_abstract_type_is_projected, renamed from ..._falls_back_to_include and ..._falls_back_to_full_include.
  • Abstract_root_type_collection_is_loaded_once: a collection on an abstract root with two concrete types is joined once.
  • Navigation_under_abstract_navigation_is_loaded now covers the same under a reference navigation.
  • TphRootEntity has a read only Summary, selected by the four include guard tests so they stay on the includes path they exist to cover.
  • The other snapshot changes are SQL only: type tests in place of whole entity columns.

An abstract type cannot be created by a member init, so a query for one,
or a navigation to one, was loaded whole through includes: every column
of it and of every entity under it, whatever was selected. No row is
ever of the abstract type itself, so it is now projected as the chain of
type tests already used for a concrete type with derived types, ending
in null instead of a member init of its own.

Navigations on a type with derived types are bound once, to whichever
type the chain created. Bound inside each type's member init they were
repeated once per concrete type, so a collection was joined, or split
out, once for every type, and a hierarchy under a hierarchy multiplied
the size of the expression.

Foreign keys declared on derived types are loaded by a query for the
base type, as the keys of the base type are.

The count query of a connection no longer carries the select projection.

A resolver on an abstract type that reads a property of its source
without declaring a projection used to work, since the whole entity was
loaded. It now needs the projection, as one on a concrete type does.

Bump version to 35.5.0
@SimonCropp SimonCropp added this to the 35.5.0 milestone Oct 8, 2026
@SimonCropp
SimonCropp merged commit 1f12b9d into main Oct 8, 2026
2 checks passed
@SimonCropp
SimonCropp deleted the project-abstract-types branch October 8, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant