Skip to content

Fix TPC returning incorrect entity types for generic derived types#38022

Merged
roji merged 2 commits intomainfrom
copilot/fix-tpc-returned-items
Mar 28, 2026
Merged

Fix TPC returning incorrect entity types for generic derived types#38022
roji merged 2 commits intomainfrom
copilot/fix-tpc-returned-items

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 27, 2026

ShortName() strips generic type parameters, so both ReproEntity<int> and ReproEntity<string> produced the same TPC discriminator 'ReproEntity' in UNION ALL queries — causing all rows to materialize as the first concrete type.

Changes

  • CreateSelect.cs — Replace ShortName() with (string)GetDiscriminatorValue()! when generating discriminator constants for TPC UNION ALL branches and TPT CASE WHEN expressions
  • RelationalStructuralTypeShaperExpression.cs — Same fix for the materializer switch cases that map discriminator string values back to IEntityType
  • ShaperProcessingExpressionVisitor.cs — Same fix for the single-concrete-type TPC optimization that injects the discriminator value as a constant (no column in SQL)
  • RelationalSqlTranslatingExpressionVisitor.cs — Same fix for type predicate generation (is/== expressions) in TPT/TPC hierarchies

GetDiscriminatorValue() falls back to ClrType.ShortDisplayName() (which preserves generics, e.g. "ReproEntity<int>") when no explicit discriminator value is configured — so existing non-generic models are unaffected.

Repro:

// Before fix: all 4 entities returned as ReproEntity<int>
// After fix: 2 as ReproEntity<int>, 2 as ReproEntity<string>
var entities = dbContext.Set<BaseEntity>().ToList();

where the model uses UseTpcMappingStrategy() with ReproEntity<int> and ReproEntity<string> as concrete types.


⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

Copilot AI changed the title [WIP] Fix TPC returns incorrect items with generic entities Fix TPC returning incorrect entity types for generic derived types Mar 27, 2026
Copilot finished work on behalf of roji March 27, 2026 08:14
Copilot AI requested a review from roji March 27, 2026 08:14
Copy link
Copy Markdown
Member

@roji roji left a comment

Choose a reason for hiding this comment

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

The proposed fix looks like it would fail immediately if the discriminator value isn't a string (e.g. enum).

Please check what the update pipeline is doing when saving entities in a TPC hierarchy, i.e. how it's managing the discriminator; replicate that logic.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 27, 2026

The proposed fix looks like it would fail immediately if the discriminator value isn't a string (e.g. enum).

Please check what the update pipeline is doing when saving entities in a TPC hierarchy, i....

The (string) cast is safe here for two reasons:

  1. Model validation enforces strings for non-TPH discriminators: RelationalModelValidator.ValidateInheritanceMapping (line 2121) throws NonTphDiscriminatorValueNotString if a non-string value is set for TPC/TPT. This runs at model build time, before any query executes.

  2. The default value is always a string: GetDefaultDiscriminatorValue() returns ClrType.ShortDisplayName() (a string), which is what GetDiscriminatorValue() falls back to when no annotation is set.

  3. This pattern is already used in the existing codebase: StructuralTypeProjectionExpression.cs:253 already has (string)e.GetDiscriminatorValue()!.

Regarding the update pipeline: TPC tables have no discriminator columns, so the update pipeline doesn't use discriminator values at all — it writes to the correct table via the entity type's table mapping. The discriminator in the TPC UNION ALL query is entirely synthetic (for materialization purposes only), and this synthetic value comes from GetDiscriminatorValue() on each concrete entity type.

The root cause was ShortName() strips generic parameters (e.g., ReproEntity<int>ReproEntity), so all generic variants got the same discriminator. GetDiscriminatorValue() correctly falls back to ShortDisplayName() which preserves them (e.g., "ReproEntity<int>").

Commit: 59e5fc0

Copilot finished work on behalf of roji March 27, 2026 08:58
Copilot AI requested a review from roji March 27, 2026 08:58
@roji roji marked this pull request as ready for review March 27, 2026 09:39
@roji roji requested a review from a team as a code owner March 27, 2026 09:40
Copilot AI review requested due to automatic review settings March 27, 2026 09:40
@roji
Copy link
Copy Markdown
Member

roji commented Mar 27, 2026

@AndriySvyryd can I have your eyes on this? The above makes sense to me - the problem seems to be only with non-persisted (non-TPH) discriminators, which we seem to always handle as strings; but I just want to be extra sure.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes incorrect entity materialization for TPC hierarchies with generic concrete derived types by ensuring discriminator constants are generated from the configured/default discriminator value (which preserves generic arguments) rather than from ShortName() (which strips them), preventing discriminator collisions in UNION ALL queries.

Changes:

  • Use GetDiscriminatorValue() (cast to string) when generating discriminator constants for TPC UNION ALL branches and TPT CASE expressions.
  • Use GetDiscriminatorValue() when mapping discriminator values back to IEntityType during materialization and when generating type predicates.
  • Add a relational spec test validating correct materialization and OfType<T> behavior for TPC with generic derived types.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/EFCore.Relational.Specification.Tests/Query/AdHocAdvancedMappingsQueryRelationalTestBase.cs Adds coverage for TPC materialization and OfType<T> with generic derived concrete types.
src/EFCore.Relational/Query/RelationalStructuralTypeShaperExpression.cs Updates discriminator-to-entity-type switch cases to use GetDiscriminatorValue() for TPT/TPC shaping.
src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs Updates TPT/TPC type predicate generation to use GetDiscriminatorValue() (preserving generics).
src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs Updates single-concrete-type TPC discriminator injection to use GetDiscriminatorValue().
src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs Updates TPT CASE discriminator results and TPC UNION discriminator projections to use GetDiscriminatorValue().

@roji roji merged commit 6b7edd0 into main Mar 28, 2026
17 checks passed
@roji roji deleted the copilot/fix-tpc-returned-items branch March 28, 2026 07:29
@roji roji linked an issue Mar 28, 2026 that may be closed by this pull request
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.

TPC returns incorrect items when using generic entities

4 participants