Skip to content

Make ThenBy require an ordered source - #96

Merged
koenbeuk merged 2 commits into
mainfrom
feat/ordered-expressive-queryable
Sep 20, 2026
Merged

koenbeuk merged 2 commits into
mainfrom
feat/ordered-expressive-queryable

Conversation

@koenbeuk

Copy link
Copy Markdown
Collaborator

Three shapes compiled and then failed at runtime: .OrderBy().Include().ThenBy() (InvalidCastException), .OrderBy().AsNoTracking().ThenBy() and .Where().ThenBy() (both ArgumentException).

This introduces IOrderedExpressiveQueryable<T> mimicking EFCore's very own IOrderedQueryable

koenbeuk and others added 2 commits September 20, 2026 01:42
The interceptor emitter hard-coded `this IExpressiveQueryable<T> source` and
the `IExpressiveQueryable<T>` return in all four output templates, so a stub
declared on a derived interface could not be intercepted: Roslyn requires an
interceptor's signature to match the intercepted method's declared types.

Derive both from the stub symbol instead. `method` is the reduced extension
symbol, whose Parameters[0] is the first non-receiver parameter, so the
declared receiver comes from ReducedFrom.

No behaviour change: every existing stub declares `this IExpressiveQueryable<T>`
and returns either `IExpressiveQueryable<T>` or a scalar, so all four templates
emit byte-identical output. Verified by the snapshot suite — 474 tests, zero
baselines moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThenBy/ThenByDescending were declared on IExpressiveQueryable<T>, so the
compiler allowed them anywhere in a chain — including where no ordering
existed. Every such shape failed at runtime instead:

  .OrderBy().Include().ThenBy()       InvalidCastException
  .OrderBy().AsNoTracking().ThenBy()  ArgumentException
  .Where().ThenBy()                   ArgumentException

Model orderedness in the type system with a zero-member marker interface
that exists only to steer overload resolution, mirroring EF Core's
IIncludableQueryable. All three shapes are now compile errors, on every
provider, with no runtime machinery.

- IOrderedExpressiveQueryable<T> : IExpressiveQueryable<T>, IOrderedQueryable<T>
- OrderBy/OrderByDescending return it; ThenBy/ThenByDescending require it
- AsExpressive gains an IOrderedQueryable<T> overload, so the generated
  ordering interceptors re-wrap with no generator-side factory selection,
  and source.OrderBy(...).AsExpressive().ThenBy(...) keeps working
- ExpressiveMongoQueryable<T> implements the marker so ordering does not
  re-wrap it and drop IAsyncCursorSource<T>
- EXP0029's sibling-stub search now matches derived receivers, otherwise
  it stops deferring to EXP0026 and both fire on the same call

This makes the surface match System.Linq.Queryable and EF Core rather than
diverging from them, so it needs no user-facing documentation; only the
provider-author contract (a custom IExpressiveQueryable<T> wrapper should
implement the marker) is new, and that is documented in custom-providers.

Deliberately no ordered x includable cross-product interface: Include and
the chain-continuity operators must precede the ordering, exactly as plain
EF Core requires. An ordered overload of AsNoTracking would compile and
then throw, since its expression node is typed IQueryable<T>.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 20, 2026 01:46
@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.62500% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...iveSharp.Generator/PolyfillInterceptorGenerator.cs 88.00% 0 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

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.

Copilot review overview

🟢 Approval recommended

The change is cohesive, aligns with established LINQ ordering semantics, and is backed by targeted unit/integration/compiler/snapshot coverage across runtime, generator, analyzers, and provider integrations.

Review effort: Lite
Findings: None

What changed in this PR

This PR tightens the ordering contract of Expressive LINQ chains by introducing an ordered marker interface (IOrderedExpressiveQueryable<T>) so ThenBy/ThenByDescending become compile-time constrained to ordered sources, eliminating the previously observed runtime failures and aligning behavior with System.Linq/EF Core ordering semantics.

Changes:

  • Added IOrderedExpressiveQueryable<T> and updated ordering stubs so OrderBy* returns an ordered source and ThenBy* requires one.
  • Updated AsExpressive with an IOrderedQueryable<T> overload and adjusted wrappers/providers to preserve orderedness and provider-specific interfaces across ordering.
  • Updated the polyfill interceptor generator + analyzers accordingly, and added coverage across unit, integration, and generator snapshot tests (plus docs).
File Description
tests/​ExpressiveSharp.Tests/​Extensions/​ExpressiveQueryableExtensionsTests.cs Adds unit tests validating ordered AsExpressive wrapping/preservation behavior.
tests/​ExpressiveSharp.MongoDB.IntegrationTests/​Tests/​PolyfillInterceptorTests.cs Adds regression test ensuring ordered chains keep Mongo cursor source behavior working with ToListAsync.
tests/​ExpressiveSharp.IntegrationTests/​Tests/​OrderingInterceptorTests.cs New integration tests validating OrderBy/ThenBy composition semantics over expressive queryables.
tests/​ExpressiveSharp.Generator.Tests/​PolyfillInterceptorGenerator/​SingleLambdaQueryableTests.ThenByDescending_GeneratesInterceptor.verified.txt Updates snapshot to reflect ordered receiver/return types in generated interceptors.
tests/​ExpressiveSharp.Generator.Tests/​PolyfillInterceptorGenerator/​SingleLambdaQueryableTests.OrderByDescending_GeneratesInterceptor.verified.txt Updates snapshot to reflect ordered return type for OrderByDescending.
tests/​ExpressiveSharp.Generator.Tests/​PolyfillInterceptorGenerator/​OrderByTests.ThenBy_CastsToIOrderedQueryable.verified.txt Updates snapshot to reflect ordered receiver/return types for ThenBy.
tests/​ExpressiveSharp.Generator.Tests/​PolyfillInterceptorGenerator/​OrderByTests.OrderBy_GeneratesInterceptor.verified.txt Updates snapshot to reflect ordered return type for OrderBy.
tests/​ExpressiveSharp.Generator.Tests/​PolyfillInterceptorGenerator/​ComparerOverloadTests.OrderBy_WithComparer_GeneratesInterceptor.verified.txt Updates snapshot to reflect ordered return type for comparer overload.
tests/​ExpressiveSharp.Generator.Tests/​PolyfillInterceptorGenerator/​AnonymousElementTests.OrderByDescending_AfterAnonymousSelect_GeneratesInterceptor.verified.txt Updates snapshot to cast/wrap as ordered expressive queryable when ordering an anonymous element.
tests/​ExpressiveSharp.Generator.Tests/​PolyfillInterceptorGenerator/​OrderedChainBindingTests.cs New compilation-binding tests enforcing that ThenBy is only available on ordered expressive chains.
tests/​ExpressiveSharp.Generator.Tests/​CodeFixers/​ExpressiveQueryableDropoutAnalyzerTests.cs Adds analyzer regression test for ThenBy on ordered expressive types to avoid duplicate diagnostics.
tests/​ExpressiveSharp.EntityFrameworkCore.IntegrationTests/​Infrastructure/​IncludeTestBase.cs Adds EF Core integration tests proving Include + OrderBy + ThenBy composition executes correctly.
src/​ExpressiveSharp/​IOrderedExpressiveQueryable.cs Introduces ordered marker interface mirroring IOrderedQueryable<T> semantics for expressive chains.
src/​ExpressiveSharp/​Extensions/​ExpressiveQueryableLinqExtensions.cs Updates ordering stub signatures to return/require IOrderedExpressiveQueryable<T>.
src/​ExpressiveSharp/​Extensions/​ExpressiveQueryableExtensions.cs Adds ordered AsExpressive(IOrderedQueryable<T>) overload preserving static orderedness.
src/​ExpressiveSharp/​ExpressiveQueryableWrapper.cs Updates wrapper to serve as fallback for both AsExpressive overload shapes.
src/​ExpressiveSharp.MongoDB/​Infrastructure/​ExpressiveMongoQueryable.cs Ensures Mongo wrapper implements ordered expressive marker to avoid losing cursor-source interfaces after ordering.
src/​ExpressiveSharp.Generator/​PolyfillInterceptorGenerator.cs Emits interceptor signatures matching each stub’s declared receiver/return types (supports ordered marker receivers/returns).
src/​ExpressiveSharp.CodeFixers/​ExpressiveQueryableDropoutAnalyzer.cs Broadens sibling overload discovery to stubs declared on derived markers (e.g., ordered expressive).
docs/​guide/​integrations/​custom-providers.md Documents requirement for provider wrappers to implement ordered expressive marker to avoid re-wrapping losses.
CLAUDE.md Updates architecture notes to reflect ordered marker and declared-type-based interceptor emission.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@koenbeuk
koenbeuk merged commit 0f23e2e into main Sep 20, 2026
12 checks passed
@koenbeuk
koenbeuk deleted the feat/ordered-expressive-queryable branch September 20, 2026 01: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.

2 participants