You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c8ddf4e
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,7 +45,7 @@ CI targets both .NET 8.0 and .NET 10.0 SDKs.
45
45
-`ExpressiveGenerator` — finds `[Expressive]` members, validates them via `ExpressiveInterpreter`, emits expression trees via `ExpressionTreeEmitter`, and builds a runtime registry via `ExpressionRegistryEmitter`
46
46
-`PolyfillInterceptorGenerator` — uses C# 13 `[InterceptsLocation]` to rewrite `ExpressionPolyfill.Create()` and `IExpressiveQueryable<T>` LINQ call sites from delegate form to expression tree form. Supports all standard `Queryable` methods, multi-lambda methods (Join, GroupJoin, GroupBy overloads), non-lambda-first methods (Zip, ExceptBy, etc.), and custom target types via `[PolyfillTarget]` (e.g., EF Core's `EntityFrameworkQueryableExtensions` for async methods)
47
47
48
-
2.**Runtime:**`ExpressiveResolver` looks up generated expressions by (DeclaringType, MemberName, ParameterTypes). `ExpressiveReplacer` is an `ExpressionVisitor` that substitutes `[Expressive]` member accesses with the generated expression trees. Transformers (in `Transformers/`) post-process trees for provider compatibility.
48
+
2.**Runtime:**`ExpressiveResolver` looks up generated expressions by (DeclaringType, MemberName, ParameterTypes). `ExpressiveReplacer` is an `ExpressionVisitor` that substitutes `[Expressive]` member accesses with the generated expression trees. For `virtual`/`override``[Expressive]` members it dispatches **polymorphically**: it discovers derived `[Expressive]` overrides across loaded assemblies (cached, refreshed on assembly load) and emits a runtime type-test chain `instance is Derived ? <derived body> : <base body>` (EF Core translates this to a TPH discriminator `CASE`). Opt out globally via `ExpressiveOptions.DisablePolymorphicDispatch()` (EF: `UseExpressives(o => o.DisablePolymorphicDispatch())`); per-override `[NotExpressive]` opts a single type out. Transformers (in `Transformers/`) post-process trees for provider compatibility.
34 diagnostic codes: a contiguous `EXP0001–EXP0031` plus the migration band `EXP1001–EXP1003`. Generator diagnostics live in `src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs` (`EXP0001–EXP0012` core `[Expressive]`, `EXP0013–EXP0017``[ExpressiveFor]`, `EXP0018–EXP0022``[ExpressiveProperty]`, `EXP0023` ignored operation, `EXP0024` virtual member); analyzer diagnostics `EXP0025–EXP0029` in `ExpressiveSharp.CodeFixers`; window-function diagnostics `EXP0030`/`EXP0031` in `WindowFunctionLiteralArgsAnalyzer` (`EntityFrameworkCore.CodeFixers`); migration `EXP1001–EXP1003` in `MigrationAnalyzer`. The canonical reference is `docs/reference/diagnostics.md`. Key ones: EXP0001 (requires body), EXP0004 (block body requires opt-in), EXP0008 (unsupported operation, default value used), EXP0023 (unsupported operation ignored, e.g. alignment specifiers), EXP0016 (`[ExpressiveFor]` conflicts with `[Expressive]`), EXP0030 (`Ntile` non-positive literal), EXP0031 (`Lag`/`Lead` negative literal offset).
122
+
Diagnostic codes span `EXP0001–EXP0032` (with `EXP0024` **retired** — see below — so the ID is reserved but unused) plus the migration band `EXP1001–EXP1003`. Generator diagnostics live in `src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs` (`EXP0001–EXP0012` core `[Expressive]`, `EXP0013–EXP0017` `[ExpressiveFor]`, `EXP0018–EXP0022` `[ExpressiveProperty]`, `EXP0023` ignored operation); analyzer diagnostics `EXP0025–EXP0029` and `EXP0032` in `ExpressiveSharp.CodeFixers`; window-function diagnostics `EXP0030`/`EXP0031` in `WindowFunctionLiteralArgsAnalyzer` (`EntityFrameworkCore.CodeFixers`); migration `EXP1001–EXP1003` in `MigrationAnalyzer`. The canonical reference is `docs/reference/diagnostics.md`. Key ones: EXP0001 (requires body), EXP0004 (block body requires opt-in), EXP0008 (unsupported operation, default value used), EXP0023 (unsupported operation ignored, e.g. alignment specifiers), EXP0016 (`[ExpressiveFor]` conflicts with `[Expressive]`), EXP0030 (`Ntile` non-positive literal), EXP0031 (`Lag`/`Lead` negative literal offset), EXP0032 (override of an `[Expressive]` member missing `[Expressive]`). **EXP0024 (retired):** virtual/`abstract`/`override` `[Expressive]` members used to warn that they would not dispatch polymorphically; they now **do** dispatch polymorphically at runtime (`ExpressiveReplacer` emits an `is Derived ? … : base` chain, translated to a TPH discriminator `CASE`), so the warning was removed and EXP0032 took over the derived-side guidance.
## Virtual and Polymorphic Members {#virtual-polymorphic-members}
115
115
116
-
Expression-tree expansion happens at **compile time** and works purely from the **static (declared) type** of each receiver. It has no runtime instance to inspect, so it cannot honor C# virtual dispatch.
117
-
118
-
If you mark a `virtual`, `abstract`, or `override` member `[Expressive]` (a default interface member counts too -- it is implicitly virtual), the generator reports [EXP0024](../reference/diagnostics#exp0024). When the member is expanded for a query provider (EF Core, MongoDB), the call is resolved against the declared type and the **base** body is always inlined -- an overridden body in a derived type is never used:
116
+
Virtual, `abstract`, and `override``[Expressive]` members **dispatch polymorphically at runtime**. When such a member is expanded for a query provider, `ExpressiveReplacer` discovers the derived `[Expressive]` overrides across the loaded assemblies and emits a runtime type-test chain, so each row uses its own runtime type's body:
This is by design: a query provider translates the expression to SQL/MQL and never materializes a CLR object, so there is no runtime type to dispatch on. (Contrast this with compiling the expression to a delegate and invoking it in memory, where the CLR *does* dispatch on the runtime type.)
137
+
EF Core translates `a is Dog` to a table-per-hierarchy discriminator check, so the query emits a `CASE` over the discriminator column and returns the right text per row. In-memory delegates (`.Compile()`) evaluate the same `is` test against the CLR runtime type, so behavior converges with provider translation.
140
138
141
-
### Recommended: test the runtime type explicitly
139
+
### Mark every override `[Expressive]`
142
140
143
-
Branch on the concrete type so each arm has a statically-typed receiver. Every branch then expands to the correct body and the provider emits a `CASE`:
144
-
145
-
```csharp
146
-
db.Animals.AsExpressive().Select(a=>aswitch
147
-
{
148
-
Dogd=>d.Describe(), // expands Dog.Describe
149
-
_=>a.Describe(), // expands Animal.Describe
150
-
});
151
-
```
141
+
Only overrides that are themselves `[Expressive]` participate. An override that forgets the attribute is invisible to expansion — instances of that type silently fall back to the **base** body — so the analyzer reports [EXP0032](../reference/diagnostics#exp0032) with an "Add `[Expressive]`" fix. If an override is intentionally client-only, mark it `[NotExpressive]` to opt out.
152
142
153
-
### Recommended: use a non-virtual static/extension method
143
+
### Opting out
154
144
155
-
Move the logic into a single non-virtual `[Expressive]` method that performs the type test itself. This keeps the polymorphic shape in one place and produces no EXP0024:
145
+
Polymorphic dispatch is on by default. To turn it off for an entire context (e.g. a provider that cannot translate type tests), call `DisablePolymorphicDispatch()` — virtual members then expand using the static (declared) type only:
Per-override `[NotExpressive]` is independent of this switch.
158
+
159
+
### Caveats
160
+
161
+
-**Discovery is runtime and best-effort.** Overrides are found in the assemblies loaded when the query first runs (the plan is cached and refreshed when new assemblies load). For EF Core this is a non-issue: entity types are registered when the model is built, before any query. An override whose assembly loads later and is not referenced by the query falls back to the base body.
162
+
-**Provider must translate type tests.** EF Core relational providers translate `is`/discriminator checks for TPH; some mappings (TPC) or providers may not. If a provider cannot translate the conditional, the query throws at translation time — the same failure mode as any untranslatable expression.
163
+
-**Interfaces are not in scope.** Default interface members keep static interface-implementation resolution; only class virtual members dispatch polymorphically.
164
+
171
165
::: tip
172
-
Declaring entity members `virtual` is common in EF Core because it enables lazy-loading proxies. That remains fine for plain navigation and scalar properties -- EXP0024 only concerns members you *also* mark `[Expressive]`.
166
+
Declaring entity members `virtual` is common in EF Core because it enables lazy-loading proxies. That remains fine for plain navigation and scalar properties; for `[Expressive]` members it now additionally enables polymorphic dispatch.
|[EXP0024](#exp0024)|Warning|`[Expressive]` member is virtual and will not dispatch polymorphically | -- |
36
+
| EXP0024 |_(retired)_|`[Expressive]` member is virtual and will not dispatch polymorphically — virtual members now dispatch polymorphically at runtime| -- |
37
37
|[EXP0025](#exp0025)| Warning | Referenced member could benefit from `[Expressive]`|[Add `[Expressive]`](#exp0025-fix)|
38
38
|[EXP0026](#exp0026)| Warning |`IExpressiveQueryable<T>` LINQ method resolves to `Queryable`|[Add `using ExpressiveSharp;`](#exp0026-fix)|
39
39
|[EXP0027](#exp0027)| Info | No `IExpressiveQueryable<T>` overload for `Queryable` method | -- |
40
40
|[EXP0028](#exp0028)| Info | Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()`|[Wrap with `.AsExpressive()`](#exp0028-fix)|
41
41
|[EXP0029](#exp0029)| Info |`IExpressiveQueryable<T>` chain dropped to plain `IQueryable<T>`| -- |
@@ -599,48 +600,13 @@ surrounding expression emitted without it.
599
600
600
601
---
601
602
602
-
### EXP0024 -- Virtual member will not dispatch polymorphically {#exp0024}
603
+
### EXP0024 -- Virtual member will not dispatch polymorphically _(retired)_{#exp0024}
603
604
604
-
**Severity:** Warning
605
-
**Category:** Design
606
-
607
-
**Message:**
608
-
```
609
-
[Expressive] member '{0}' is virtual, abstract, or an override. When it is expanded into an
610
-
expression tree (e.g. for EF Core or MongoDB), the call is resolved using the static (declared)
611
-
type, so an overridden body in a derived type is never used. Test the runtime type explicitly
612
-
(e.g. 'x switch { Derived d => d.Member, _ => x.Member }'), or move the logic into a non-virtual
613
-
[Expressive] static/extension method.
614
-
```
615
-
616
-
**Cause:** An `[Expressive]` member is declared `virtual`, `abstract`, or `override` (a default interface member counts -- it is implicitly virtual). Expression-tree expansion happens at compile time and only sees the **static (declared) type** of the receiver, so it cannot honor C# virtual dispatch. When the member is expanded for a query provider, the **base** body is always inlined; an overridden body in a derived type is never used.
617
-
618
-
This differs from compiling the expression to a delegate and invoking it in memory, where the CLR dispatches on the runtime type.
619
-
620
-
**Fix:** Branch on the runtime type so each arm has a statically-typed receiver, or move the logic into a single non-virtual `[Expressive]` static/extension method. See [Limitations: virtual and polymorphic members](../advanced/limitations#virtual-polymorphic-members) for full examples.
**Status:** Retired. This warning no longer exists.
626
606
627
-
// Fix 1: test the runtime type explicitly
628
-
db.Animals.AsExpressive().Select(a=>aswitch
629
-
{
630
-
Dogd=>d.Describe(),
631
-
_=>a.Describe(),
632
-
});
633
-
634
-
// Fix 2: non-virtual [Expressive] extension method that does the type test once
635
-
[Expressive]
636
-
publicstaticstringDescribe(thisAnimala) =>aswitch
637
-
{
638
-
Dogd=>$"Dog: {d.Name}",
639
-
_=>$"Animal: {a.Name}",
640
-
};
641
-
```
607
+
Virtual, `abstract`, and `override``[Expressive]` members now **dispatch polymorphically at runtime**. When a virtual `[Expressive]` member is expanded for a query provider, `ExpressiveReplacer` discovers the derived `[Expressive]` overrides and emits a runtime type-test chain — `entity is Dog ? <Dog body> : <base body>` — which EF Core translates to a table-per-hierarchy discriminator `CASE`. Each row therefore uses its runtime type's body. See [Limitations: virtual and polymorphic members](../advanced/limitations#virtual-polymorphic-members).
642
608
643
-
Ifavirtualmemberisintentional (youonlyevercompileittoanin-memorydelegate, nevertranslateitthroughaprovider), suppressthewarningwith `#pragmawarningdisableEXP0024` or `<NoWarn>$(NoWarn);EXP0024</NoWarn>`.
609
+
The ID `EXP0024`is reserved (not reused). Its derived-side successor is [EXP0032](#exp0032), which flags a derived override that forgets `[Expressive]` and would silently fall back to the base body.
### EXP0032 -- Override of an `[Expressive]` member is missing `[Expressive]` {#exp0032}
774
+
775
+
**Severity:** Warning
776
+
**Category:** Design
777
+
778
+
**Message:**
779
+
```
780
+
'{0}' overrides an [Expressive] member but is not itself marked [Expressive]. In expression-tree
781
+
expansion (e.g. EF Core, MongoDB) instances of this type fall back to the base body instead of
782
+
this override. Add [Expressive] so it participates in polymorphic dispatch, or [NotExpressive] to
783
+
silence this.
784
+
```
785
+
786
+
**Cause:** A `virtual`/`abstract``[Expressive]` member is overridden, but the override is **not** itself marked `[Expressive]`. Runtime polymorphic dispatch can only inline overrides that are registered as `[Expressive]`, so for instances of the overriding type the query silently falls back to the **base** body — almost always a bug. The check walks *up* the override chain (cheap and cross-assembly), unlike the derived-type discovery that expansion performs at runtime.
787
+
788
+
**Fix:** Add `[Expressive]` to the override (the [Add `[Expressive]`](#exp0025-fix) code fix does this), so it participates in dispatch:
If the override intentionally should not be translated (it stays client-only), mark it `[NotExpressive]` to silence the diagnostic.
805
+
806
+
---
807
+
807
808
## Window Function Diagnostics (EXP0030--EXP0031)
808
809
809
810
These diagnostics are emitted by the `WindowFunctionLiteralArgsAnalyzer` in the `ExpressiveSharp.EntityFrameworkCore.CodeFixers` package (shipped with `ExpressiveSharp.EntityFrameworkCore`). They validate constant literal arguments to `WindowFunction.*` calls before they reach the database. Only compile-time constant arguments are checked; a variable count or offset is never flagged. See [Window Functions](../guide/window-functions) for the full feature reference.
0 commit comments