Skip to content

Commit d5cc987

Browse files
authored
Improve UseMemberBody and add new diagnostics (#174)
* Improve UseMemberBody and add new diagnostics Fixes #134 * Fix diagnostic reports with Expression member bodies * Handle explicit getters for UseMemberBody * Split ProjectableInterpreter class file * Fix extension methdods resolving and fix docs
1 parent e5cde6f commit d5cc987

29 files changed

Lines changed: 1984 additions & 704 deletions

File tree

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,92 @@ Multiple `[Projectable]` constructors (overloads) per class are fully supported.
314314

315315
> **Note:** If the delegated constructor's source is not available in the current compilation, the generator reports **EFP0009** and skips the projection.
316316

317+
#### Can I redirect the expression body to a different member with `UseMemberBody`?
318+
319+
Yes! The `UseMemberBody` property on `[Projectable]` lets you redirect the source of the generated expression to a *different* member on the same type (and in the same file).
320+
321+
This is useful when you want to:
322+
323+
- keep a regular C# implementation for in-memory use while maintaining a separate, cleaner expression for EF Core
324+
- supply the body as a pre-built `Expression<Func<...>>` property for full control over the generated tree
325+
326+
##### Delegating to a method or property body
327+
328+
The simplest case — point `UseMemberBody` at another method or property that has the **same return type and parameter signature**. The generator uses the body of the target member instead:
329+
330+
```csharp
331+
public class Entity
332+
{
333+
public int Id { get; set; }
334+
335+
// EF-side: generates an expression from ComputedImpl
336+
[Projectable(UseMemberBody = nameof(ComputedImpl))]
337+
public int Computed => Id; // original body is ignored
338+
339+
// In-memory implementation (or a different algorithm)
340+
private int ComputedImpl => Id * 2;
341+
}
342+
```
343+
344+
The generated expression is `(@this) => @this.Id * 2`, so `Computed` projects as `Id * 2` in SQL even though the arrow body says `Id`.
345+
346+
##### Using an `Expression<Func<...>>` property as the body
347+
348+
For even more control you can supply the body as a typed `Expression<Func<...>>` property. This lets you write the expression once and reuse it from both the `[Projectable]` member and any runtime code that needs the expression tree directly:
349+
350+
```csharp
351+
public class Entity
352+
{
353+
public int Id { get; set; }
354+
355+
[Projectable(UseMemberBody = nameof(Computed4))]
356+
public int Computed3 => Id; // body is replaced at compile time
357+
358+
// The expression tree is picked up by the generator and by the runtime resolver
359+
private static Expression<Func<Entity, int>> Computed4 => x => x.Id * 3;
360+
}
361+
```
362+
363+
> **Note:** When the projectable member is a *property*, the `Expression<Func<...>>` property body is handled entirely by the runtime resolver — no extra source is generated. This works transparently.
364+
365+
For **instance methods**, name the lambda parameter `@this` so that it matches the generator's own naming convention:
366+
367+
```csharp
368+
public class Entity
369+
{
370+
public int Value { get; set; }
371+
372+
[Projectable(UseMemberBody = nameof(IsPositiveExpr))]
373+
public bool IsPositive() => Value > 0;
374+
375+
private static Expression<Func<Entity, bool>> IsPositiveExpr => @this => @this.Value > 0;
376+
}
377+
```
378+
379+
##### Static extension methods
380+
381+
`UseMemberBody` works equally well on static extension methods. Name the lambda parameters to match the method's parameter names:
382+
383+
```csharp
384+
public static class FooExtensions
385+
{
386+
[Projectable(UseMemberBody = nameof(NameEqualsExpr))]
387+
public static bool NameEquals(this Foo a, Foo b) => a.Name == b.Name;
388+
389+
private static Expression<Func<Foo, Foo, bool>> NameEqualsExpr =>
390+
(a, b) => a.Name == b.Name;
391+
}
392+
```
393+
394+
The generated expression is `(Foo a, Foo b) => a.Name == b.Name` — the same lambda that EF Core receives at query time. The two implementations are kept in sync in one place.
395+
396+
##### Diagnostics
397+
398+
| Code | Severity | Cause |
399+
|-------------|----------|------------------------------------------------------------------------------------------------------|
400+
| **EFP0010** | Error | The name given to `UseMemberBody` does not match any member on the containing type |
401+
| **EFP0011** | Error | A member with that name exists but its type or signature is incompatible with the projectable member |
402+
317403
#### Can I use pattern matching in projectable members?
318404

319405
Yes! As of version 6.x, the generator supports a rich set of C# pattern-matching constructs and rewrites them into expression-tree-compatible ternary/binary expressions that EF Core can translate to SQL CASE expressions.

src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ EFP0006 | Design | Error | Method or property should expose a body definiti
1111
EFP0007 | Design | Error | Unsupported pattern in projectable expression
1212
EFP0008 | Design | Error | Target class is missing a parameterless constructor
1313
EFP0009 | Design | Error | Delegated constructor cannot be analyzed for projection
14+
EFP0010 | Design | Error | UseMemberBody target member not found
15+
EFP0011 | Design | Error | UseMemberBody target member is incompatible
1416

1517
### Changed Rules
1618

src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,20 @@ public static class Diagnostics
7676
DiagnosticSeverity.Error,
7777
isEnabledByDefault: true);
7878

79+
public static readonly DiagnosticDescriptor UseMemberBodyNotFound = new DiagnosticDescriptor(
80+
id: "EFP0010",
81+
title: "UseMemberBody target member not found",
82+
messageFormat: "Member '{1}' referenced by UseMemberBody on '{0}' was not found on type '{2}'",
83+
category: "Design",
84+
DiagnosticSeverity.Error,
85+
isEnabledByDefault: true);
86+
87+
public static readonly DiagnosticDescriptor UseMemberBodyIncompatible = new DiagnosticDescriptor(
88+
id: "EFP0011",
89+
title: "UseMemberBody target member is incompatible",
90+
messageFormat: "Member '{1}' referenced by UseMemberBody on '{0}' has an incompatible type or signature",
91+
category: "Design",
92+
DiagnosticSeverity.Error,
93+
isEnabledByDefault: true);
7994
}
8095
}

0 commit comments

Comments
 (0)