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
|[EXP0027](#exp0027)| Info | Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()`|[Wrap with `.AsExpressive()`](#exp0027-fix)|
35
36
|[EXP0031](#exp0031)| Error |`[ExpressiveProperty]` target name is already defined | -- |
36
37
|[EXP0032](#exp0032)| Error |`[ExpressiveProperty]` requires a partial containing type | -- |
@@ -475,12 +477,45 @@ Duplicate [ExpressiveFor] mapping for member '{0}' on type '{1}'; only one stub
475
477
476
478
---
477
479
480
+
### EXP0027 -- Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()` {#exp0027}
481
+
482
+
**Severity:** Info
483
+
**Category:** Usage
484
+
485
+
**Message:**
486
+
```
487
+
LINQ method '{0}' on a plain IQueryable<T> references the [Expressive] member '{1}'.
488
+
Without .AsExpressive(), the member's body will not be inlined into the expression tree;
489
+
the provider may evaluate the call in memory or fail to translate it. Wrap the source
490
+
with .AsExpressive().
491
+
```
492
+
493
+
**Cause:** A LINQ method on a plain `IQueryable<T>` receiver (one that is not `IExpressiveQueryable<T>`) is invoked with a lambda whose body references an `[Expressive]` member. Because the chain is not expressive-aware, the source generator does not rewrite the lambda into an expression tree that inlines the member's body — the underlying query provider receives a call to the runtime delegate. Most providers cannot translate this and will either evaluate the call client-side (silent overfetch) or throw at execution time.
494
+
495
+
**Fix:** Wrap the chain root with `.AsExpressive()` so that subsequent LINQ methods flow through the ExpressiveSharp delegate-based overloads, which inline `[Expressive]` member bodies at compile time.
496
+
497
+
```csharp
498
+
// Before — IsAdult is silently evaluated on the client.
499
+
varadults=users.Where(u=>u.IsAdult).ToList();
500
+
501
+
// After — IsAdult is inlined into the expression tree before the provider sees it.
When you intentionally want to evaluate a member at runtime (e.g., it captures process state), mark the member with `[NotExpressive]` to suppress the diagnostic at every call site.
506
+
507
+
#### Code Fix: Wrap source with `.AsExpressive()` {#exp0027-fix}
508
+
509
+
The IDE offers a single code action: **Wrap source with `.AsExpressive()`**. It walks the LINQ chain to the leftmost non-LINQ expression, wraps it with `.AsExpressive()`, and inserts `using ExpressiveSharp;` if it is not already imported.
These diagnostics apply to `[ExpressiveProperty]` stubs, which ask the generator to emit a new property on the stub's containing partial type. See [`[ExpressiveProperty]` Attribute](./expressive-property) for the full feature reference.
481
516
482
517
::: info Replacing `[Expressive(Projectable = true)]`
483
-
`[ExpressiveProperty]` replaces the now-removed `[Expressive(Projectable = true)]`. Diagnostic codes `EXP0021`--`EXP0030` were retired along with that feature and are not reused. The migration recipe is in [Migration from Projectables](../guide/migration-from-projectables#migrating-usememberbody).
518
+
`[ExpressiveProperty]` replaces the now-removed `[Expressive(Projectable = true)]`. Diagnostic codes `EXP0021`--`EXP0026` and `EXP0028`--`EXP0030` were retired along with that feature and are not reused. (EXP0027 has been reassigned to the [plain-IQueryable analyzer](#exp0027).) The migration recipe is in [Migration from Projectables](../guide/migration-from-projectables#migrating-usememberbody).
484
519
:::
485
520
486
521
### EXP0031 -- Target name is already defined {#exp0031}
@@ -611,6 +646,57 @@ on an override
611
646
612
647
---
613
648
649
+
### EXP0036 -- `IExpressiveQueryable<T>` chain dropped to plain `IQueryable<T>` {#exp0036}
650
+
651
+
**Severity:** Info
652
+
**Category:** Usage
653
+
654
+
**Message:**
655
+
```
656
+
'{0}' returns IQueryable<T> from an IExpressiveQueryable<T> receiver, dropping the expressive
657
+
chain. Downstream LINQ skips ExpressiveSharp rewriting and [Expressive] members may evaluate
658
+
on the client. Add an IExpressiveQueryable<T>-typed overload of '{0}', wrap the result with
659
+
.AsExpressive(), or mark the method [NotExpressive] if the dropout is intentional.
660
+
```
661
+
662
+
**Cause:** A method invocation is being made on a receiver that implements `IExpressiveQueryable<T>`, but the method's return type is plain `IQueryable<T>` (or some derivative that is not also expressive). The chain loses its expressive type at this call site, and every downstream LINQ operation on the result skips ExpressiveSharp's rewrite step — `[Expressive]` members in subsequent `Where` / `Select` / `Include` / etc. fall back to runtime delegate invocation, which most providers cannot translate and will evaluate on the client.
663
+
664
+
The diagnostic fires once, at the dropout point itself, regardless of how many further calls follow.
665
+
666
+
**Common dropout shapes:**
667
+
668
+
```csharp
669
+
// User-defined helper typed on plain IQueryable<T> — drops the chain.
-`.AsQueryable()` — the standard explicit downcast — is sanctioned and never reported.
696
+
- Marking the offending method with `[NotExpressive]` suppresses the diagnostic at every call site, for cases where the dropout is intentional (the helper performs work that genuinely needs to run on the client).
697
+
698
+
---
699
+
614
700
## Migration Diagnostics (EXP1001--EXP1003)
615
701
616
702
These diagnostics are emitted by the `MigrationAnalyzer` in the `ExpressiveSharp.EntityFrameworkCore.CodeFixers` package. They detect usage of the legacy `EntityFrameworkCore.Projectables` library and offer automated code fixes to migrate to ExpressiveSharp.
Use `[NotExpressive]` to mark a member that *looks* expressive-eligible (it has an expression body that the source generator could lift) but should intentionally remain runtime-evaluated. The attribute suppresses the analyzer suggestions:
142
+
143
+
-[EXP0013](./diagnostics#exp0013) — "Member could benefit from `[Expressive]`"
144
+
-[EXP0027](./diagnostics#exp0027) — "Plain `IQueryable` chain references an `[Expressive]` member without `.AsExpressive()`"
145
+
146
+
```csharp
147
+
publicclassOrder
148
+
{
149
+
publicGuidId { get; set; }
150
+
151
+
// Always evaluated in-memory — captures process-local state that would not
152
+
// survive translation. Suppress the "could be [Expressive]" suggestion.
title:"IExpressiveQueryable<T> chain dropped to plain IQueryable<T>",
28
+
messageFormat:"'{0}' returns IQueryable<T> from an IExpressiveQueryable<T> receiver, dropping the expressive chain. Downstream LINQ skips ExpressiveSharp rewriting and [Expressive] members may evaluate on the client. Add an IExpressiveQueryable<T>-typed overload of '{0}', wrap the result with .AsExpressive(), or mark the method [NotExpressive] if the dropout is intentional.",
29
+
category:"Usage",
30
+
defaultSeverity:DiagnosticSeverity.Info,
31
+
isEnabledByDefault:true,
32
+
description:"Once an IExpressiveQueryable<T> chain is upcast back to plain IQueryable<T>, the ExpressiveSharp rewrite layer is no longer applied to subsequent calls. User-defined helpers that take and return IQueryable<T> are the most common cause; sibling overloads typed on IExpressiveQueryable<T> preserve the chain.");
0 commit comments