Skip to content

docs: add LINQKit migration guide - #98

Draft
koenbeuk wants to merge 1 commit into
mainfrom
docs/migration-from-linqkit
Draft

koenbeuk wants to merge 1 commit into
mainfrom
docs/migration-from-linqkit

Conversation

@koenbeuk

Copy link
Copy Markdown
Collaborator

Closes the LINQKit half of #94. The Projectables half already exists as docs/guide/migration-from-projectables.md, so this adds the missing guide.

What's here

  • New page docs/guide/migration-from-linqkit.md
  • Sidebar entry in docs/.vitepress/config.mts
  • Cross-link from the Projectables guide

The guide is framed around the fact that the two libraries work from opposite directions: LINQKit composes Expression<Func<...>> objects at runtime, ExpressiveSharp generates trees at compile time from ordinary members. So it is an incremental migration, not an all-or-nothing one, and the guide says which parts stay on LINQKit.

Converts to [Expressive] Stays on LINQKit
Expression fields used via Invoke PredicateBuilder / ExpressionStarter<T>
[Expandable] stub + Impl method pairs Helpers that take an Expression<...> parameter
Shared selector expressions

Verification

The issue author's concern was runtime edge cases, so the coexistence claims were checked in a throwaway project (outside this repo) against LinqKit.Microsoft.EntityFrameworkCore 8.1.11 / LinqKit.Core 1.2.11, EF Core 8, SQLite, referencing ExpressiveSharp from source.

  • Converted Invoke filter and DTO projection produce byte-identical SQL before and after conversion.
  • PredicateBuilder output works under UseExpressives() without AsExpandable(), including predicates that reference [Expressive] members and ExpressionPolyfill.Create lambdas.
  • AsExpandable() and AsExpressive() compose on one query in either order.

One real failure, documented

LINQKit's global WithExpressionExpanding() alongside UseExpressives() fails when an [Expressive] member is referenced inside an invoked expression variable:

options.UseSqlite(conn).UseExpressives().WithExpressionExpanding();   // either order

Expression<Func<Customer, bool>> isBig = c => c.IsBigSpender;         // [Expressive] inside

db.Customers.Where(c => isBig.Invoke(c)).ToList();
// InvalidOperationException: The LINQ expression
//   'DbSet<Customer>().Where(c => c.IsBigSpender)' could not be translated.

ExpressiveSharp expands first, while isBig is still an opaque captured variable; LINQKit then inlines it, and nothing expands the newly exposed member. Registration order makes no difference. Everything else works under the global hook: plain Invoke, [Expandable] methods, and [Expressive] members written directly in the query.

Four workarounds are documented (per-query AsExpandable(), Expand() up front, pre-expanding with ExpandExpressives(), or converting the call site). Expand() and ExpandExpressives() were both run and confirmed; the other two were exercised only without the global hook, which is the recommended setup anyway.

This looks fixable in ExpressiveSharp itself by expanding through captured expression constants under Invoke, but that is a behaviour change outside the scope of a docs issue — happy to open it separately.

Notes

  • Plain csharp blocks rather than ::: expressive-sample, since the sample renderer cannot reference LinqKit.
  • npx vitepress build passes with no dead links; the new page's anchors resolve in the rendered HTML.

🤖 Generated with Claude Code

Closes the LINQKit half of #94; the Projectables guide already exists.

Covers the concept mapping (Invoke/[Expandable] -> [Expressive],
AsExpandable -> UseExpressives), what stays on LINQKit (PredicateBuilder,
expression-parameter helpers), and the runtime edge cases.

All coexistence claims were verified against LinqKit.Microsoft.EntityFrameworkCore
8.1.11 on EF Core 8 / SQLite. Notably, LINQKit's global WithExpressionExpanding()
alongside UseExpressives() fails to expand an [Expressive] member referenced
inside an invoked expression variable, in either registration order —
ExpressiveSharp expands before LINQKit inlines the variable. Four workarounds
are documented.

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

Copy link
Copy Markdown
Collaborator Author

Had AI give a shot in closing #94

This needs a review from someone that has recently used linqkit

@koenbeuk koenbeuk added the help wanted Extra attention is needed label Sep 20, 2026
@koenbeuk
koenbeuk marked this pull request as draft September 20, 2026 02:48

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

🟡 Changes recommended

The migration guide contains unresolved documentation inaccuracies and examples that do not compile.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 Low severity

Open (3)
What changed in this PR

Adds a LINQKit migration guide describing incremental migration and coexistence with ExpressiveSharp.

Changes:

  • Adds migration guidance, examples, and runtime workarounds.
  • Links the new guide from the Projectables guide.
  • Adds the guide to the VitePress sidebar.
File Summary and review notes
docs/​guide/​migration-from-linqkit.md New migration guide. Nit comments identify undeclared outer and isDomestic examples, inaccurate .AsExpressive() and .AsExpandable() guidance, incomplete removal criteria, and an incorrect EF Core InMemory statement.
docs/​guide/​migration-from-projectables.md Adds a cross-link to the LINQKit migration guide.
docs/​.vitepress/​config.mts Adds the migration guide to the sidebar.

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

|---|---|---|
| `Expression<Func<T, R>>` field + `.Invoke(x)` | `[Expressive]` property or method, called normally | [Details](#invoke-to-expressive-members) |
| `[Expandable(nameof(Impl))]` stub method | `[Expressive]` on the method itself | [Details](#expandable-attribute) |
| `.AsExpandable()` per query | Nothing -- `UseExpressives()` expands globally | Outside EF Core: `.AsExpressive()` |
.UseExpressives();
```

Do not remove `LinqKit.Microsoft.EntityFrameworkCore` (or `LinqKit.Core`) yet. Remove it at the end, and only if no `PredicateBuilder`, `Invoke`, or `Expand` usage is left.
| Fix | Code |
|---|---|
| Use per-query `AsExpandable()` -- LINQKit then inlines *before* ExpressiveSharp expands | `db.Customers.AsExpandable().Where(c => isBig.Invoke(c))` |
| Call `Expand()` up front | `db.Customers.Where(outer.Expand())` |

This branch has not been deployed

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

Labels

help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants