Skip to content

Issue #223 AsNoTracking before Select Throws Argument Exception - #225

Open
zmerdev wants to merge 2 commits into
EFNext:masterfrom
zmerdev:fix/root-rewrite-type-guard
Open

zmerdev wants to merge 2 commits into
EFNext:masterfrom
zmerdev:fix/root-rewrite-type-guard

Conversation

@zmerdev

@zmerdev zmerdev commented Sep 25, 2026 •

Copy link
Copy Markdown

Fix for issue #223

This pull request improves the handling of query root rewrites in the ProjectableExpressionReplacer by ensuring that projection rewrites are only applied when the query result is exactly the entity type, preventing invalid rewrites for projections to anonymous or DTO types. It also adds comprehensive tests to verify these scenarios across multiple .NET versions.

Query root rewrite correctness

  • Added a type guard (_IsSequenceOfEntity) in _AddProjectableSelect to ensure that the projectable select is only appended when the queryable sequence is exactly of the entity type, preventing errors when projecting to anonymous or DTO types. [1] [2]
  • Introduced a cache (_queryableElementTypeCache) and helper methods to efficiently determine the element type of IQueryable<T> sequences. [1] [2]

Test coverage

  • Added new functional tests to verify correct behavior when using AsNoTracking() and projections to both anonymous types and entity types, ensuring the rewrite is only applied when appropriate.

zmerdev and others added 2 commits September 24, 2026 16:27
_AddProjectableSelect appends Select<TEntity, TEntity>, so it only composes
when the node really is a sequence of TEntity. It is not, whenever the caller
already projected away from the entity root, and appending the rewrite anyway
throws instead of no-oping:

  ArgumentException: Expression of type 'IQueryable<Dto>' cannot be used for
  parameter of type 'IQueryable<TEntity>' of method Select[TEntity,TEntity]

_disableRootRewrite is meant to prevent this -- a Select sets it -- but EFNext#132
gave that same field a second job, the tracking decision. ExpressionVisitor
walks outside-in, so in

  db.Set<T>().AsNoTracking().Where(...).Select(x => new Dto { ... }).ToList()

the AsNoTracking node is visited last and clears the flag the Select set,
re-enabling the rewrite against an IQueryable<Dto>. Any query that puts
AsNoTracking before a projection to a DTO or an anonymous type throws, which
is a common repository shape. Checking the node's own type is independent of
visit order, so it holds whichever of the two flags wrote last.

Deliberately an exact element-type match rather than an assignability check:
IQueryable<out T> is covariant, so IQueryable<TEntity> is assignable from
IQueryable<TDerived>, and rewriting there would re-project a derived entity as
its base type.

Tests: three new QueryRootTests cases covering AsNoTracking before a
projection to an anonymous type, the same via the context's default tracking
behaviour, and a projection that stays on the entity type (still rewritten).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@zmerdev
zmerdev marked this pull request as ready for review September 25, 2026 20:49
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